moeru-ai/airi · error

Invalid request body

Error message

Invalid request body

What it means

The OpenRouter audio-speech adapter intercepts the speech fetch and requires init.body to be a JSON string so it can extract 'input' (text) and 'voice'. A missing or non-string body throws this error before any network call is made.

Source

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

      }
    }
  }

  return chunks
}

function decodeBase64Pcm(chunks: string[]) {
  const binary = atob(chunks.join(''))
  const bytes = new Uint8Array(binary.length)
  for (let index = 0; index < binary.length; index++)
    bytes[index] = binary.charCodeAt(index)
  return bytes
}

function createAudioFetch(apiKey: string, baseUrl: string, model: string) {
  return async (_input: RequestInfo | URL, init?: RequestInit) => {
    if (!init?.body || typeof init.body !== 'string')
      throw new Error('Invalid request body')

    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)

View on GitHub (pinned to f679616c34)

Solutions

  1. Use the provider through its speech API so the client sends JSON.stringify'd { input, voice }.
  2. If calling fetch directly, pass body: JSON.stringify({ model, input, voice, modalities: ['text','audio'] }).
  3. Remove or audit middleware that changes the body type.
  4. Confirm Content-Type is application/json on the outgoing request.

Example fix

// before
await audioFetch(url, { method: 'POST', body: new URLSearchParams({ input: text }) })
// after
await audioFetch(url, { method: 'POST', body: JSON.stringify({ input: text, voice: 'alloy' }) })
Defensive patterns

Strategy: validation

Validate before calling

const body = JSON.stringify({ input: text, voice })
if (typeof body !== 'string' || !text?.trim()) throw new TypeError('OpenRouter audio request needs a JSON string body with input')

Type guard

const hasStringBody = (init?: RequestInit): init is RequestInit & { body: string } => typeof init?.body === 'string'

Try / catch

try { await audioFetch(url, req) } catch (e) { if (e.message === 'Invalid request body') { /* re-issue with JSON.stringify({ input, voice }) */ } else throw e }

Prevention

When it happens

Trigger: Issuing the speech request with FormData, a ReadableStream, URLSearchParams, or no body at all instead of a serialized JSON string.

Common situations: Calling the provider's fetch manually with multipart data; an SDK/client configuration that streams bodies; wrapping the provider with middleware that transforms the request body.

Understand the failure class

Background: "Invalid ... format", "must be in format X", "does not look like a ..." — invalid argument format errors across CLI tools and libraries — this error's family across 17 libraries.

Related errors


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