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

  1. Read the embedded response body in the message — OpenRouter states the exact reason (auth, credits, modality).
  2. Verify the API key and account credits on openrouter.ai.
  3. Confirm the configured model supports the `audio` output modality and the configured `voice` is valid for that model.
  4. 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

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


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)