nexu-io/open-design · error · Error

no ElevenLabs API key - configure it in Settings or set OD_E

Error message

no ElevenLabs API key - configure it in Settings or set OD_ELEVENLABS_API_KEY

What it means

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.

Source

Thrown at apps/daemon/src/integrations/elevenlabs-voices.ts:105

}

function cloneVoiceOptions(voices: ElevenLabsVoiceOption[]): ElevenLabsVoiceOption[] {
  return voices.map((voice) => ({
    ...voice,
    ...(voice.labels ? { labels: { ...voice.labels } } : {}),
  }));
}

export async function listElevenLabsVoiceOptions(
  projectRoot: string,
  options: {
    limit?: number;
    requestInit?: Pick<RequestInit, 'dispatcher'>;
  } = {},
): Promise<ElevenLabsVoiceOption[]> {
  const credentials = await resolveProviderConfig(projectRoot, 'elevenlabs');
  if (!credentials.apiKey) {
    throw new Error(
      'no ElevenLabs API key - configure it in Settings or set OD_ELEVENLABS_API_KEY',
    );
  }

  const baseUrl = (credentials.baseUrl || ELEVENLABS_DEFAULT_BASE_URL).replace(
    /\/$/,
    '',
  );
  const pageSize = clampLimit(options.limit);
  const cacheKey = voiceCacheKey({
    projectRoot,
    baseUrl,
    apiKey: credentials.apiKey,
    pageSize,
  });
  const cached = voiceOptionsCache.get(cacheKey);
  const now = Date.now();
  if (cached && cached.expiresAt > now) {

View on GitHub (pinned to 5be4028344)

Solutions

  1. Configure ElevenLabs in Settings (writes the per-project media config), or set OD_ELEVENLABS_API_KEY in the daemon env.
  2. At the call site, resolve the config first and skip / show a setup CTA when credentials.apiKey is empty instead of calling the listing.
  3. If running in a different deployment, set a per-project media-config.json with elevenlabs.apiKey and baseUrl.

Example fix

// before
const voices = await listElevenLabsVoiceOptions(projectRoot);

// after
const credentials = await resolveProviderConfig(projectRoot, 'elevenlabs');
if (!credentials.apiKey) {
  return { voices: [], requiresSetup: true };
}
const voices = await listElevenLabsVoiceOptions(projectRoot);
Defensive patterns

Strategy: validation

Validate before calling

import { resolveProviderConfig } from '../media/config.js';

async function hasElevenLabsKey(projectRoot: string): Promise<boolean> {
  const credentials = await resolveProviderConfig(projectRoot, 'elevenlabs');
  return Boolean(credentials.apiKey && credentials.apiKey.trim());
}

// usage
if (!(await hasElevenLabsKey(projectRoot))) {
  return { voices: [], requiresSetup: true };
}

Type guard

function hasElevenLabsApiKey(c: { apiKey?: string }): c is { apiKey: string } {
  return typeof c.apiKey === 'string' && c.apiKey.trim().length > 0;
}

Try / catch

try {
  return await listElevenLabsVoiceOptions(projectRoot);
} catch (err) {
  if (err instanceof Error && /no ElevenLabs API key/.test(err.message)) {
    return { voices: [], requiresSetup: true };
  }
  throw err;
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of nexu-io/open-design@5be4028344 (2026-08-12). Data as JSON: /api/errors/d1808170b3d2f99d. Report an issue: GitHub.