{"record":{"id":"72fff2c41761461b","repo":"moeru-ai/airi","slug":"openrouter-audio-request-failed-response-status","errorCode":null,"errorMessage":"OpenRouter audio request failed: ${response.status} ${await response.text()}","messagePattern":"OpenRouter audio request failed: (.+?) (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/stage-ui/src/libs/providers/providers/openrouter-audio-speech/index.ts","lineNumber":114,"sourceCode":"\n    const body = JSON.parse(init.body) as { input?: string, voice?: string }\n    const response = await globalThis.fetch(new URL('chat/completions', baseUrl), {\n      method: 'POST',\n      headers: {\n        'Authorization': `Bearer ${apiKey}`,\n        'Content-Type': 'application/json',\n        ...OPENROUTER_ATTRIBUTION_HEADERS,\n      },\n      body: JSON.stringify({\n        model,\n        messages: [{ role: 'user', content: ttsPrompt(body.input ?? '') }],\n        modalities: ['text', 'audio'],\n        audio: { voice: body.voice, format: 'pcm16' },\n        stream: true,\n      }),\n    })\n    if (!response.ok)\n      throw new Error(`OpenRouter audio request failed: ${response.status} ${await response.text()}`)\n    if (!response.body)\n      throw new Error('OpenRouter audio response has no body')\n\n    const wav = toWavFromPCM16(decodeBase64Pcm(await collectAudioChunks(response.body)), 24000)\n    return new Response(new Blob([wav], { type: 'audio/wav' }), {\n      status: 200,\n      headers: { 'Content-Type': 'audio/wav' },\n    })\n  }\n}\n\nexport const providerOpenRouterAudioSpeech = defineProvider<OpenRouterAudioConfig>({\n  id: 'openrouter-audio-speech',\n  name: 'OpenRouter',\n  nameLocalize: ({ t }) => t('settings.pages.providers.provider.openrouter-audio-speech.title'),\n  description: 'openrouter.ai',\n  descriptionLocalize: ({ t }) => t('settings.pages.providers.provider.openrouter-audio-speech.description'),\n  tasks: ['text-to-speech'],","sourceCodeStart":96,"sourceCodeEnd":132,"githubUrl":"https://github.com/moeru-ai/airi/blob/677329427f32468c74b17f3ec47eeca4e05bec65/packages/stage-ui/src/libs/providers/providers/openrouter-audio-speech/index.ts#L96-L132","documentation":"Thrown by the OpenRouter audio speech provider when the chat-completions request with `modalities: ['text','audio']`, `audio: { voice, format: 'pcm16' }`, `stream: true` gets a non-2xx response. Unlike sibling providers it awaits and embeds the full `response.text()` error body, so OpenRouter's own message (credit, model, moderation) is part of the thrown string.","triggerScenarios":"401 (invalid OpenRouter API key), 402 (insufficient credits), 400 (model does not support audio output or the voice id is invalid for it), 404 (unknown model id), 429 (rate limit), plus OpenRouter attribution headers being rejected by a proxy.","commonSituations":"Free-tier key without credits used for an audio-capable model; model id of a text-only model configured for TTS; voice name from a different provider family; stale key after rotation; egress proxy stripping the attribution headers or the streaming response.","solutions":["Read the embedded response body in the message — OpenRouter states the exact reason (auth, credits, modality).","Verify the API key and account credits on openrouter.ai.","Confirm the configured model supports the `audio` output modality and the configured `voice` is valid for that model.","For 429, back off and retry; for 404, re-list models and pick an audio-capable id."],"exampleFix":"// before\nconst audio = await speechProvider.tts({ input: text, voice })\n// after — fail with a typed, readable error and only for expected statuses\nlet audio\ntry {\n  audio = await speechProvider.tts({ input: text, voice })\n}\ncatch (error) {\n  if (String(error).includes('402'))\n    throw new Error('OpenRouter credits exhausted — top up the account')\n  throw error\n}","handlingStrategy":"try-catch","validationCode":"if (!config.apiKey?.trim())\n  throw new Error('OpenRouter API key is required before TTS calls')\nif (!model)\n  throw new Error('Select an audio-capable OpenRouter model first')","typeGuard":null,"tryCatchPattern":"try {\n  const wav = await provider.tts(body)\n}\ncatch (error) {\n  const message = errorMessageFrom(error)\n  if (message.includes('401'))\n    // invalid key\n  else if (message.includes('402'))\n    // out of credits\n  else if (message.includes('400'))\n    // model lacks audio modality or voice invalid\n  else\n    throw error\n}","preventionTips":["Confirm the account has credits before wiring audio TTS.","Use model ids documented to support the `audio` output modality.","Use voice ids from OpenRouter's audio-voice list for the chosen model.","Keep the attribution headers intact through proxies."],"tags":["network","api","tts","openrouter","speech"],"backgroundTag":"upstream-api-error","analyzedSha":"677329427f32468c74b17f3ec47eeca4e05bec65","analyzedAt":"2026-08-18T17:29:58.153Z","contentChangedAt":"2026-08-18T17:29:58.153Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}