moeru-ai/airi · error

OpenRouter audio response has no body

Error message

OpenRouter audio response has no body

What it means

After a successful (ok) response, the OpenRouter adapter still requires a streaming body to collect base64 PCM audio chunks. If response.body is null/undefined despite status ok, this error is thrown — the response cannot be decoded into audio.

Source

Thrown at packages/provider-inference/src/providers/cloud/openrouter-audio-speech/index.ts:116

    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, 'openrouter-audio-speech'>({
  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'],
  icon: 'i-lobe-icons:openrouter',
  createProviderConfig: () => openRouterAudioConfigSchema,

View on GitHub (pinned to f679616c34)

Solutions

  1. Ensure fetch is not mocked/intercepted in a way that drops the response body.
  2. Test outside service workers/proxies that may consume the stream.
  3. Confirm the runtime supports streaming response bodies (ReadabilityStream on Response); upgrade Node/browser if needed.
  4. Check that no middleware calls response.text()/arrayBuffer() before the adapter.

Example fix

// before (test mock)
fetch.mockResolvedValue(new Response(null, { status: 200 }))
// after
fetch.mockResolvedValue(new Response(sseStream, { status: 200 }))
Defensive patterns

Strategy: fallback

Validate before calling

const res = await fetchOnce()
if (res.ok && !res.body) throw new Error('Runtime does not support streaming response bodies')

Type guard

const hasBody = (res: Response): res is Response & { body: ReadableStream } => res.body != null

Try / catch

try { await speak(text) } catch (e) { if (e.message.includes('no body')) { /* disable stream interception/mocks or switch runtime */ } else throw e }

Prevention

When it happens

Trigger: A runtime or polyfilled fetch returning a body-less Response (e.g. some service-worker intercepts, mocks, or HTTP/1 environments without streaming support), or an interceptor that consumed the body before the adapter reads it.

Common situations: Testing with a fetch mock that returns Response without a body; service workers or proxies consuming the stream; older runtimes lacking ReadableStream response bodies.

Related errors


AI-assisted analysis of moeru-ai/airi@f679616c34 (2026-09-08). Data as JSON: /api/errors/9f8b11d65b10af51. Report an issue: GitHub.