moeru-ai/airi · error · Error
OpenRouter audio request failed
Error message
OpenRouter audio request failed: ${response.status} ${await response.text()} What it means
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.
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.
Example fix
// before
const audio = await speechProvider.tts({ input: text, voice })
// after — fail with a typed, readable error and only for expected statuses
let audio
try {
audio = await speechProvider.tts({ input: text, voice })
}
catch (error) {
if (String(error).includes('402'))
throw new Error('OpenRouter credits exhausted — top up the account')
throw error
} Defensive patterns
Strategy: try-catch
Validate before calling
if (!config.apiKey?.trim())
throw new Error('OpenRouter API key is required before TTS calls')
if (!model)
throw new Error('Select an audio-capable OpenRouter model first') Try / catch
try {
const wav = await provider.tts(body)
}
catch (error) {
const message = errorMessageFrom(error)
if (message.includes('401'))
// invalid key
else if (message.includes('402'))
// out of credits
else if (message.includes('400'))
// model lacks audio modality or voice invalid
else
throw error
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- MiniMax TTS request failed
- audio models upstream
- audio voices upstream
- OpenRouter audio response has no body
- streaming models upstream
AI-assisted analysis of moeru-ai/airi@677329427f (2026-08-18).
Data as JSON: /api/errors/72fff2c41761461b.
Report an issue: GitHub.
Appendix: source
Thrown at packages/stage-ui/src/libs/providers/providers/openrouter-audio-speech/index.ts:114
const body = JSON.parse(init.body) as { input?: string, voice?: string }
const response = await globalThis.fetch(new URL('chat/completions', baseUrl), {
method: 'POST',
headers: {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json',
...OPENROUTER_ATTRIBUTION_HEADERS,
},
body: JSON.stringify({
model,
messages: [{ role: 'user', content: ttsPrompt(body.input ?? '') }],
modalities: ['text', 'audio'],
audio: { voice: body.voice, format: 'pcm16' },
stream: true,
}),
})
if (!response.ok)
throw new Error(`OpenRouter audio request failed: ${response.status} ${await response.text()}`)
if (!response.body)
throw new Error('OpenRouter audio response has no body')
const wav = toWavFromPCM16(decodeBase64Pcm(await collectAudioChunks(response.body)), 24000)
return new Response(new Blob([wav], { type: 'audio/wav' }), {
status: 200,
headers: { 'Content-Type': 'audio/wav' },
})
}
}
export const providerOpenRouterAudioSpeech = defineProvider<OpenRouterAudioConfig>({
id: 'openrouter-audio-speech',
name: 'OpenRouter',
nameLocalize: ({ t }) => t('settings.pages.providers.provider.openrouter-audio-speech.title'),
description: 'openrouter.ai',
descriptionLocalize: ({ t }) => t('settings.pages.providers.provider.openrouter-audio-speech.description'),
tasks: ['text-to-speech'],View on GitHub (pinned to 677329427f)