nexu-io/open-design · error · Error

${tag} speech ${resp.status}: ${truncate(text, 240)}

Error message

${tag} speech ${resp.status}: ${truncate(text, 240)}

What it means

Thrown by the OpenAI/Azure text-to-speech renderer when `POST /v1/audio/speech` (or the Azure deployment equivalent at `…/openai/deployments/{id}/audio/speech`) returns a non-2xx status. The message embeds the provider tag (`openai` or `azure-openai`), the HTTP status, and up to 240 chars of the response body (`truncate`) so the underlying API cause — auth, model access, moderation, rate limit — reaches the agent instead of an opaque fetch failure.

Source

Thrown at apps/daemon/src/media/index.ts:1369

  }

  const headers: Record<string, string> = {
    authorization: `Bearer ${credentials.apiKey}`,
    'content-type': 'application/json',
  };
  if (azure) {
    headers['api-key'] = credentials.apiKey;
  }

  const resp = await fetch(url, withMediaRequestInit(ctx, {
    method: 'POST',
    headers,
    body: JSON.stringify(body),
  }));
  if (!resp.ok) {
    const text = await resp.text();
    const tag = azure ? 'azure-openai' : 'openai';
    throw new Error(`${tag} speech ${resp.status}: ${truncate(text, 240)}`);
  }
  const arr = await resp.arrayBuffer();
  const bytes = Buffer.from(arr);
  if (bytes.length === 0) {
    throw new Error('openai speech returned zero bytes');
  }
  const tag = azure ? 'azure-openai' : 'openai';
  const noteBits = [`${tag}/${ctx.wireModel}`, voiceId, `${format}`, `${bytes.length} bytes`];
  if (instructions) noteBits.splice(2, 0, 'styled');
  return {
    bytes,
    providerNote: noteBits.join(' · '),
    suggestedExt: format === 'opus' ? '.ogg' : `.${format}`,
  };
}

// ---------------------------------------------------------------------------
// Provider: Volcengine Ark — Doubao Seedance 2.0 video.

View on GitHub (pinned to 5be4028344)

Solutions

  1. Read the embedded status+body: 401/403 → fix the key (set `OPENAI_API_KEY` or the Azure key in Settings); 404 → create the Azure deployment for the model; 429 → back off or upgrade tier; 400 naming `voice` → pick a voice from `OPENAI_TTS_VOICES` (alloy/ash/ballad/coral/echo/fable/onyx/nova/sage/shimmer) or switch the model to `gpt-4o-mini-tts` to use it as `instructions`.
  2. Verify `credentials.apiKey` and the resolved `baseUrl` via Settings (or `od media config`) before rendering.
  3. Shorten the input text to stay under the 4096-character TTS limit and reword content moderation may flag.

Example fix

// before
const body = { input: text, voice: requestedVoice, response_format: format };
// arbitrary voice ids rejected by non-gpt-4o-mini-tts models → 400

// after
let voiceId = 'nova';
let instructions: string | undefined;
if (requestedVoice && OPENAI_TTS_VOICES.has(requestedVoice)) {
  voiceId = requestedVoice;
} else if (requestedVoice && ctx.model === 'gpt-4o-mini-tts') {
  instructions = requestedVoice; // free-form speaking style
} else if (requestedVoice) {
  throw new Error(`voice '${requestedVoice}' is not one of: ${[...OPENAI_TTS_VOICES].join(', ')}`);
}
Defensive patterns

Strategy: try-catch

Validate before calling

// Pre-validate the voice id and input length before the speech fetch.
const OPENAI_TTS_VOICES = new Set(['alloy','ash','ballad','coral','echo','fable','nova','onyx','sage','shimmer']);
function validateSpeechInput(model: string, voice: string | undefined, input: string): void {
  if (Buffer.byteLength(input, 'utf8') > 4096) {
    throw new Error(`speech input exceeds 4096-byte limit (${Buffer.byteLength(input, 'utf8')})`);
  }
  if (voice && !OPENAI_TTS_VOICES.has(voice) && model !== 'gpt-4o-mini-tts') {
    throw new Error(`voice '${voice}' not in ${[...OPENAI_TTS_VOICES].join(',')} (use gpt-4o-mini-tts for free-form instructions)`);
  }
}

Try / catch

// Distinguish recoverable rate-limit/auth from hard validation failures.
try {
  return await renderOpenAISpeech(ctx, credentials);
} catch (err) {
  const msg = err instanceof Error ? err.message : String(err);
  if (/speech 429/.test(msg)) { /* back off + retry once */ }
  if (/speech 40[13]/.test(msg)) { throw new Error('OpenAI auth failed — check OPENAI_API_KEY'); }
  throw err;
}

Prevention

When it happens

Trigger: `resp.ok === false` on the speech `fetch`. Concretely: 401/403 from a missing/revoked key, 404 because an Azure deployment wasn't provisioned for the TTS model, 400 from an unknown `voice` id (a value not in `OPENAI_TTS_VOICES` sent to a model other than `gpt-4o-mini-tts`), 429 org rate limit, or a 400/flagged-response when the input text trips OpenAI moderation or exceeds the 4096-char limit.

Common situations: (1) `OPENAI_API_KEY` unset or belonging to an org without audio-model access; (2) a UI voice value treated as free-form `instructions` on a non-`gpt-4o-mini-tts` model; (3) an Azure `baseUrl` whose deployment name doesn't match the requested TTS model; (4) long/disallowed input text hitting moderation; (5) shared org rate limits exhausted.

Related errors


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