nexu-io/open-design · error · Error

elevenlabs voices ${resp.status}: ${errText.slice(0, 240)}

Error message

elevenlabs voices ${resp.status}: ${errText.slice(0, 240)}

What it means

Thrown by listElevenLabsVoiceOptions() when the GET to {baseUrl}/v2/voices?page_size=N returns a non-ok response. The first 240 chars of the response body are embedded so the operator can see ElevenLabs' error detail (rate limit, invalid key, wrong base URL).

Source

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

    pageSize,
  });
  const cached = voiceOptionsCache.get(cacheKey);
  const now = Date.now();
  if (cached && cached.expiresAt > now) {
    return cloneVoiceOptions(cached.voices);
  }

  const resp = await fetch(`${baseUrl}/v2/voices?page_size=${pageSize}`, {
    ...options.requestInit,
    method: 'GET',
    headers: {
      'xi-api-key': credentials.apiKey,
      accept: 'application/json',
    },
  });
  if (!resp.ok) {
    const errText = await resp.text();
    throw new Error(`elevenlabs voices ${resp.status}: ${errText.slice(0, 240)}`);
  }

  const payload = await resp.json() as unknown;
  const rawVoices = isRecord(payload) && Array.isArray(payload.voices)
    ? payload.voices
    : [];
  const voices = rawVoices
    .map((voice) => normalizeVoice(voice))
    .filter((voice): voice is ElevenLabsVoiceOption => voice !== null);
  voiceOptionsCache.set(cacheKey, {
    expiresAt: now + ELEVENLABS_VOICE_CACHE_TTL_MS,
    voices: cloneVoiceOptions(voices),
  });
  return voices;
}

View on GitHub (pinned to 5be4028344)

Solutions

  1. Inspect the embedded status/body: 401 → rotate the key, 429 → back off and cache longer, 404 → fix baseUrl.
  2. Confirm baseUrl (credentials.baseUrl or https://api.elevenlabs.io) actually serves /v2/voices.
  3. Rely on the in-memory voice cache (TTL 10 min) to ride through short outages instead of re-fetching on every call.

Example fix

// before
const resp = await fetch(`${baseUrl}/v2/voices?page_size=${pageSize}`, ...);
if (!resp.ok) throw new Error(`elevenlabs voices ${resp.status}: ${errText.slice(0, 240)}`);

// after
if (!resp.ok) {
  const cached = voiceOptionsCache.get(cacheKey);
  if (cached && cached.expiresAt > Date.now()) return cached.voices;
  throw new Error(`elevenlabs voices ${resp.status}: ${errText.slice(0, 240)}`);
}
Defensive patterns

Strategy: retry

Type guard

function isElevenLabsHttpError(err: unknown): boolean {
  return err instanceof Error && /^elevenlabs voices \d{3}:/.test(err.message);
}

function extractStatus(err: unknown): number | null {
  const m = err instanceof Error ? err.message.match(/^elevenlabs voices (\d{3}):/) : null;
  return m ? Number(m[1]) : null;
}

Try / catch

import { voiceOptionsCache } from '../integrations/elevenlabs-voices.js';

try {
  return await listElevenLabsVoiceOptions(projectRoot);
} catch (err) {
  const status = extractStatus(err);
  if (status && status >= 500) {
    // transient — caller may retry; cache serves stale if present
  }
  throw err;
}

Prevention

When it happens

Trigger: ElevenLabs returns 401 (invalid xi-api-key), 429 (rate limit / quota), 404 (custom baseUrl that does not host /v2/voices), or 5xx. Also when a proxy baseUrl (e.g. an OpenAI-compatible gateway) is set for elevenlabs but does not implement the /v2/voices route.

Common situations: Expired or revoked API key; free-tier rate limit during catalogue refresh; baseUrl pointing at a ElevenLabs-compatible proxy that lacks the voices route; transient 5xx during an ElevenLabs incident.

Related errors


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