{"record":{"id":"a2f7b231cba4b40d","repo":"nexu-io/open-design","slug":"tag-speech-resp-status-truncate-text-240","errorCode":null,"errorMessage":"${tag} speech ${resp.status}: ${truncate(text, 240)}","messagePattern":"(.+?) speech (.+?): (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"apps/daemon/src/media/index.ts","lineNumber":1369,"sourceCode":"  }\n\n  const headers: Record<string, string> = {\n    authorization: `Bearer ${credentials.apiKey}`,\n    'content-type': 'application/json',\n  };\n  if (azure) {\n    headers['api-key'] = credentials.apiKey;\n  }\n\n  const resp = await fetch(url, withMediaRequestInit(ctx, {\n    method: 'POST',\n    headers,\n    body: JSON.stringify(body),\n  }));\n  if (!resp.ok) {\n    const text = await resp.text();\n    const tag = azure ? 'azure-openai' : 'openai';\n    throw new Error(`${tag} speech ${resp.status}: ${truncate(text, 240)}`);\n  }\n  const arr = await resp.arrayBuffer();\n  const bytes = Buffer.from(arr);\n  if (bytes.length === 0) {\n    throw new Error('openai speech returned zero bytes');\n  }\n  const tag = azure ? 'azure-openai' : 'openai';\n  const noteBits = [`${tag}/${ctx.wireModel}`, voiceId, `${format}`, `${bytes.length} bytes`];\n  if (instructions) noteBits.splice(2, 0, 'styled');\n  return {\n    bytes,\n    providerNote: noteBits.join(' · '),\n    suggestedExt: format === 'opus' ? '.ogg' : `.${format}`,\n  };\n}\n\n// ---------------------------------------------------------------------------\n// Provider: Volcengine Ark — Doubao Seedance 2.0 video.","sourceCodeStart":1351,"sourceCodeEnd":1387,"githubUrl":"https://github.com/nexu-io/open-design/blob/5be4028344c2eb4c667c5a97bda8f750c5597ef7/apps/daemon/src/media/index.ts#L1351-L1387","documentation":"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.","triggerScenarios":"`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.","commonSituations":"(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.","solutions":["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`.","Verify `credentials.apiKey` and the resolved `baseUrl` via Settings (or `od media config`) before rendering.","Shorten the input text to stay under the 4096-character TTS limit and reword content moderation may flag."],"exampleFix":"// before\nconst body = { input: text, voice: requestedVoice, response_format: format };\n// arbitrary voice ids rejected by non-gpt-4o-mini-tts models → 400\n\n// after\nlet voiceId = 'nova';\nlet instructions: string | undefined;\nif (requestedVoice && OPENAI_TTS_VOICES.has(requestedVoice)) {\n  voiceId = requestedVoice;\n} else if (requestedVoice && ctx.model === 'gpt-4o-mini-tts') {\n  instructions = requestedVoice; // free-form speaking style\n} else if (requestedVoice) {\n  throw new Error(`voice '${requestedVoice}' is not one of: ${[...OPENAI_TTS_VOICES].join(', ')}`);\n}","handlingStrategy":"try-catch","validationCode":"// Pre-validate the voice id and input length before the speech fetch.\nconst OPENAI_TTS_VOICES = new Set(['alloy','ash','ballad','coral','echo','fable','nova','onyx','sage','shimmer']);\nfunction validateSpeechInput(model: string, voice: string | undefined, input: string): void {\n  if (Buffer.byteLength(input, 'utf8') > 4096) {\n    throw new Error(`speech input exceeds 4096-byte limit (${Buffer.byteLength(input, 'utf8')})`);\n  }\n  if (voice && !OPENAI_TTS_VOICES.has(voice) && model !== 'gpt-4o-mini-tts') {\n    throw new Error(`voice '${voice}' not in ${[...OPENAI_TTS_VOICES].join(',')} (use gpt-4o-mini-tts for free-form instructions)`);\n  }\n}","typeGuard":null,"tryCatchPattern":"// Distinguish recoverable rate-limit/auth from hard validation failures.\ntry {\n  return await renderOpenAISpeech(ctx, credentials);\n} catch (err) {\n  const msg = err instanceof Error ? err.message : String(err);\n  if (/speech 429/.test(msg)) { /* back off + retry once */ }\n  if (/speech 40[13]/.test(msg)) { throw new Error('OpenAI auth failed — check OPENAI_API_KEY'); }\n  throw err;\n}","preventionTips":["Always validate the voice id against `OPENAI_TTS_VOICES` unless the model is `gpt-4o-mini-tts`.","Keep input text under the 4096-byte TTS limit; truncate upstream, not at the provider.","Confirm the Azure deployment name matches the TTS model before pointing `baseUrl` at Azure."],"tags":["openai","tts","azure","audio","provider-error"],"backgroundTag":null,"analyzedSha":"5be4028344c2eb4c667c5a97bda8f750c5597ef7","analyzedAt":"2026-08-12T12:03:58.812Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}