{"record":{"id":"529a9c00c1cc205f","repo":"moeru-ai/airi","slug":"invalid-request-body-529a9c","errorCode":null,"errorMessage":"Invalid request body","messagePattern":"Invalid request body","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/provider-inference/src/providers/cloud/openrouter-audio-speech/index.ts","lineNumber":95,"sourceCode":"      }\n    }\n  }\n\n  return chunks\n}\n\nfunction decodeBase64Pcm(chunks: string[]) {\n  const binary = atob(chunks.join(''))\n  const bytes = new Uint8Array(binary.length)\n  for (let index = 0; index < binary.length; index++)\n    bytes[index] = binary.charCodeAt(index)\n  return bytes\n}\n\nfunction createAudioFetch(apiKey: string, baseUrl: string, model: string) {\n  return async (_input: RequestInfo | URL, init?: RequestInit) => {\n    if (!init?.body || typeof init.body !== 'string')\n      throw new Error('Invalid request body')\n\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)","sourceCodeStart":77,"sourceCodeEnd":113,"githubUrl":"https://github.com/moeru-ai/airi/blob/f679616c34f1cf6d282c8d64264242af944b3fed/packages/provider-inference/src/providers/cloud/openrouter-audio-speech/index.ts#L77-L113","documentation":"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.","triggerScenarios":"Issuing the speech request with FormData, a ReadableStream, URLSearchParams, or no body at all instead of a serialized JSON string.","commonSituations":"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.","solutions":["Use the provider through its speech API so the client sends JSON.stringify'd { input, voice }.","If calling fetch directly, pass body: JSON.stringify({ model, input, voice, modalities: ['text','audio'] }).","Remove or audit middleware that changes the body type.","Confirm Content-Type is application/json on the outgoing request."],"exampleFix":"// before\nawait audioFetch(url, { method: 'POST', body: new URLSearchParams({ input: text }) })\n// after\nawait audioFetch(url, { method: 'POST', body: JSON.stringify({ input: text, voice: 'alloy' }) })","handlingStrategy":"validation","validationCode":"const body = JSON.stringify({ input: text, voice })\nif (typeof body !== 'string' || !text?.trim()) throw new TypeError('OpenRouter audio request needs a JSON string body with input')","typeGuard":"const hasStringBody = (init?: RequestInit): init is RequestInit & { body: string } => typeof init?.body === 'string'","tryCatchPattern":"try { await audioFetch(url, req) } catch (e) { if (e.message === 'Invalid request body') { /* re-issue with JSON.stringify({ input, voice }) */ } else throw e }","preventionTips":["Always serialize the body with JSON.stringify","Use the provider's speech API, not raw fetch","Avoid middleware that mutates body type","Include required fields input and voice"],"tags":["tts","speech","request-body","validation"],"backgroundTag":"invalid-argument-format","analyzedSha":"f679616c34f1cf6d282c8d64264242af944b3fed","analyzedAt":"2026-09-08T13:18:15.005Z","contentChangedAt":"2026-09-08T13:18:15.005Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}