{"record":{"id":"d1808170b3d2f99d","repo":"nexu-io/open-design","slug":"no-elevenlabs-api-key-configure-it-in-settings-o","errorCode":null,"errorMessage":"no ElevenLabs API key - configure it in Settings or set OD_ELEVENLABS_API_KEY","messagePattern":"no ElevenLabs API key - configure it in Settings or set OD_ELEVENLABS_API_KEY","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"apps/daemon/src/integrations/elevenlabs-voices.ts","lineNumber":105,"sourceCode":"}\n\nfunction cloneVoiceOptions(voices: ElevenLabsVoiceOption[]): ElevenLabsVoiceOption[] {\n  return voices.map((voice) => ({\n    ...voice,\n    ...(voice.labels ? { labels: { ...voice.labels } } : {}),\n  }));\n}\n\nexport async function listElevenLabsVoiceOptions(\n  projectRoot: string,\n  options: {\n    limit?: number;\n    requestInit?: Pick<RequestInit, 'dispatcher'>;\n  } = {},\n): Promise<ElevenLabsVoiceOption[]> {\n  const credentials = await resolveProviderConfig(projectRoot, 'elevenlabs');\n  if (!credentials.apiKey) {\n    throw new Error(\n      'no ElevenLabs API key - configure it in Settings or set OD_ELEVENLABS_API_KEY',\n    );\n  }\n\n  const baseUrl = (credentials.baseUrl || ELEVENLABS_DEFAULT_BASE_URL).replace(\n    /\\/$/,\n    '',\n  );\n  const pageSize = clampLimit(options.limit);\n  const cacheKey = voiceCacheKey({\n    projectRoot,\n    baseUrl,\n    apiKey: credentials.apiKey,\n    pageSize,\n  });\n  const cached = voiceOptionsCache.get(cacheKey);\n  const now = Date.now();\n  if (cached && cached.expiresAt > now) {","sourceCodeStart":87,"sourceCodeEnd":123,"githubUrl":"https://github.com/nexu-io/open-design/blob/5be4028344c2eb4c667c5a97bda8f750c5597ef7/apps/daemon/src/integrations/elevenlabs-voices.ts#L87-L123","documentation":"Thrown by listElevenLabsVoiceOptions() when resolveProviderConfig(projectRoot, 'elevenlabs') returns credentials with no apiKey. The function builds the voice-picker catalogue from the ElevenLabs /v2/voices endpoint, which requires a valid xi-api-key header, so a missing key is a hard precondition.","triggerScenarios":"Calling listElevenLabsVoiceOptions() for a project whose media config has no elevenlabs.apiKey, and OD_ELEVENLABS_API_KEY is unset for that project root. The Settings UI voice-picker and any daemon route listing voices both hit this.","commonSituations":"User opened the voice picker before configuring ElevenLabs in Settings; per-project media-config.json missing the elevenlabs.apiKey field; env var spelled differently (e.g. ELEVENLABS_API_KEY without the OD_ prefix); key field present but empty string.","solutions":["Configure ElevenLabs in Settings (writes the per-project media config), or set OD_ELEVENLABS_API_KEY in the daemon env.","At the call site, resolve the config first and skip / show a setup CTA when credentials.apiKey is empty instead of calling the listing.","If running in a different deployment, set a per-project media-config.json with elevenlabs.apiKey and baseUrl."],"exampleFix":"// before\nconst voices = await listElevenLabsVoiceOptions(projectRoot);\n\n// after\nconst credentials = await resolveProviderConfig(projectRoot, 'elevenlabs');\nif (!credentials.apiKey) {\n  return { voices: [], requiresSetup: true };\n}\nconst voices = await listElevenLabsVoiceOptions(projectRoot);","handlingStrategy":"validation","validationCode":"import { resolveProviderConfig } from '../media/config.js';\n\nasync function hasElevenLabsKey(projectRoot: string): Promise<boolean> {\n  const credentials = await resolveProviderConfig(projectRoot, 'elevenlabs');\n  return Boolean(credentials.apiKey && credentials.apiKey.trim());\n}\n\n// usage\nif (!(await hasElevenLabsKey(projectRoot))) {\n  return { voices: [], requiresSetup: true };\n}","typeGuard":"function hasElevenLabsApiKey(c: { apiKey?: string }): c is { apiKey: string } {\n  return typeof c.apiKey === 'string' && c.apiKey.trim().length > 0;\n}","tryCatchPattern":"try {\n  return await listElevenLabsVoiceOptions(projectRoot);\n} catch (err) {\n  if (err instanceof Error && /no ElevenLabs API key/.test(err.message)) {\n    return { voices: [], requiresSetup: true };\n  }\n  throw err;\n}","preventionTips":["Resolve credentials once and short-circuit the picker when apiKey is empty.","Use the OD_ELEVENLABS_API_KEY env var name exactly (with the OD_ prefix).","In Settings, write the per-project media-config so other code paths see the key."],"tags":["config","integration","credentials","elevenlabs"],"backgroundTag":null,"analyzedSha":"5be4028344c2eb4c667c5a97bda8f750c5597ef7","analyzedAt":"2026-08-12T12:03:58.812Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}