{"record":{"id":"eb681ea1e6ea27ed","repo":"nexu-io/open-design","slug":"no-openrouter-api-key-configure-it-in-settings-o","errorCode":null,"errorMessage":"no OpenRouter API key — configure it in Settings or set OPENROUTER_API_KEY","messagePattern":"no OpenRouter API key — configure it in Settings or set OPENROUTER_API_KEY","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"apps/daemon/src/media/index.ts","lineNumber":1815,"sourceCode":"//\n// ---------------------------------------------------------------------------\n// OpenRouter image generation via Chat Completions API\n// ---------------------------------------------------------------------------\n// Unlike the dedicated /videos endpoint (async polling), image generation\n// goes through /chat/completions with `modalities: [\"image\"]` (or\n// `[\"image\", \"text\"]` for multi-modal models like Gemini).  The response\n// embeds generated images as base64 data URLs in\n// `choices[0].message.images[].image_url.url`.\n//\n// Model IDs follow the same `openrouter/`-prefix convention as video.\n// ---------------------------------------------------------------------------\n\nasync function renderOpenRouterImage(\n  ctx: MediaContext,\n  credentials: ProviderConfig,\n): Promise<RenderResult> {\n  if (!credentials.apiKey) {\n    throw new Error(\n      'no OpenRouter API key — configure it in Settings or set OPENROUTER_API_KEY',\n    );\n  }\n  const baseUrl = (credentials.baseUrl || 'https://openrouter.ai/api/v1').replace(/\\/$/, '');\n\n  // Respect model-alias contract: credentials.model (from stored config)\n  // overrides ctx.wireModel (from OD_MEDIA_MODEL_ALIASES / resolveModelAlias).\n  // Then strip the `openrouter/` catalogue prefix so the wire model name\n  // matches OpenRouter's canonical slug.\n  const resolved = (credentials.model || ctx.wireModel).trim();\n  const wireModel = resolved.startsWith('openrouter/')\n    ? resolved.slice('openrouter/'.length)\n    : resolved;\n\n  // Multi-modal models (Gemini variants) accept both image and text\n  // output; image-only models (Flux, Recraft, Sourceful) only accept\n  // [\"image\"]. We use a simple heuristic on the slug.\n  const modalities: string[] = wireModel.includes('gemini')","sourceCodeStart":1797,"sourceCodeEnd":1833,"githubUrl":"https://github.com/nexu-io/open-design/blob/5be4028344c2eb4c667c5a97bda8f750c5597ef7/apps/daemon/src/media/index.ts#L1797-L1833","documentation":"Thrown by renderOpenRouterImage before any network call when credentials.apiKey is empty. OpenRouter routes image-capable models through its /api/v1/chat/completions endpoint and requires a bearer token; without one the request cannot authenticate. The message names both the Settings UI and the OPENROUTER_API_KEY env var as remediation paths.","triggerScenarios":"User selects an OpenRouter image model in Settings but never pasted a key, OPENROUTER_API_KEY is unset in the daemon environment, or the credentials store was reset. The provider dispatch reaches renderOpenRouterImage and immediately fails this guard.","commonSituations":"Fresh install where the OpenRouter row in Settings has no key; CI/daemon run without the env var exported; key wiped by a config migration.","solutions":["Open Settings, find the OpenRouter provider, and paste the API key from openrouter.ai/keys.","Or export OPENROUTER_API_KEY in the daemon's environment and restart.","If the key was entered but the error persists, verify the credentials loader is binding the key into credentials.apiKey for this provider id."],"exampleFix":"// before\nif (!credentials.apiKey) {\n  throw new Error('no OpenRouter API key — configure it in Settings or set OPENROUTER_API_KEY');\n}\n\n// after — env fallback for headless setups\nconst apiKey = credentials.apiKey || process.env.OPENROUTER_API_KEY;\nif (!apiKey) {\n  throw new Error('no OpenRouter API key — configure it in Settings or set OPENROUTER_API_KEY');\n}","handlingStrategy":"validation","validationCode":"function openRouterImageConfigured(creds: ProviderConfig, env = process.env): boolean {\n  return Boolean((creds.apiKey && creds.apiKey.trim()) || env.OPENROUTER_API_KEY);\n}\n\nif (ctx.providerId === 'openrouter' && ctx.kind === 'image' && !openRouterImageConfigured(credentials)) {\n  return { kind: 'config-error', message: 'Add your OpenRouter API key in Settings or set OPENROUTER_API_KEY.' };\n}","typeGuard":"function hasOpenRouterKey(creds: ProviderConfig): creds is ProviderConfig & { apiKey: string } {\n  return typeof creds.apiKey === 'string' && creds.apiKey.trim().length > 0;\n}","tryCatchPattern":"try {\n  await renderOpenRouterImage(ctx, credentials);\n} catch (e) {\n  if (/no OpenRouter API key/.test(String(e))) {\n    return presentConfigPrompt('openrouter');\n  }\n  throw e;\n}","preventionTips":["Run a credential pre-flight in the provider registry before dispatching.","Allow OPENROUTER_API_KEY env fallback at the call site.","Surface a Settings deep-link on missing-key errors.","Re-validate credentials after config migrations."],"tags":["openrouter","image-generation","credentials","config","provider-api"],"backgroundTag":null,"analyzedSha":"5be4028344c2eb4c667c5a97bda8f750c5597ef7","analyzedAt":"2026-08-12T12:03:58.812Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}