moeru-ai/airi · warning

Skipping malformed SSE chunk from OpenRouter audio stream:

Error message

Skipping malformed SSE chunk from OpenRouter audio stream:

What it means

The OpenRouter audio speech provider parses a streamed response as server-sent events and JSON.parse()s each data line to extract base64 PCM audio chunks. When a line is not valid JSON (or does not match the expected shape it is skipped), the parse error is caught per-chunk and logged with this warning; the rest of the stream continues. This is loss-tolerant by design but can hide truncated output.

Source

Thrown at packages/stage-ui/src/libs/providers/providers/openrouter-audio-speech/index.ts:76

      if (!line.startsWith('data: '))
        continue

      const data = line.slice('data: '.length).trim()
      if (data === '[DONE]') {
        done = true
        break
      }

      try {
        const event = JSON.parse(data) as {
          choices?: Array<{ delta?: { audio?: { data?: string } } }>
        }
        const audio = event.choices?.[0]?.delta?.audio?.data
        if (audio)
          chunks.push(audio)
      }
      catch (error) {
        console.warn('Skipping malformed SSE chunk from OpenRouter audio stream:', data, error)
      }
    }
  }

  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')

View on GitHub (pinned to 677329427f)

Solutions

  1. Log the skipped line (already included) and check whether it is '[DONE]' — if so, filter it explicitly before JSON.parse.
  2. Disable proxy buffering for the SSE route (proxy_buffering off; flush immediately) so chunks arrive intact.
  3. If many chunks are skipped, compare the raw stream (devtools) with what the client sees to find the mangling hop.
  4. Verify the model actually supports audio output via OpenRouter's /api/v1 chat completions with modalities: ['audio'].

Example fix

// before
const event = JSON.parse(data) as { choices?: ... }

// after — skip sentinels before parsing
if (data === '[DONE]')
  break
const event = JSON.parse(data) as { choices?: ... }
Defensive patterns

Strategy: try-catch

Validate before calling

function isSseDataLine(line: string): boolean {
  return line.startsWith('data:') && !line.includes('[DONE]')
}

Type guard

function isOpenRouterAudioChunk(event: unknown): event is { choices: Array<{ delta?: { audio?: { data?: string } } }> } {
  return typeof event === 'object'
    && event !== null
    && Array.isArray((event as any).choices)
}

Try / catch

for (const data of dataLines) {
  if (data === '[DONE]') break
  try {
    const event = JSON.parse(data)
    if (isOpenRouterAudioChunk(event) && event.choices[0]?.delta?.audio?.data)
      chunks.push(event.choices[0].delta.audio.data)
  }
  catch {
    // skip malformed chunk; count skips and warn if ratio is high
  }
}

Prevention

When it happens

Trigger: An SSE data line contains the '[DONE]' sentinel or a keep-alive/comment payload, the proxy mangles chunk boundaries (split multi-byte/bracketed JSON across lines), or the endpoint emits a non-JSON error frame inside the SSE stream.

Common situations: Streaming audio from an OpenRouter-compatible model through nginx/Cloudflare with buffering or fragmenting; model emits usage/final chunks with a different schema; trailing '[DONE]' line reaching the parser; upstream returning an HTML error page mid-stream.

Understand the failure class

Related errors


AI-assisted analysis of moeru-ai/airi@677329427f (2026-08-18). Data as JSON: /api/errors/cf89933953e61caa. Report an issue: GitHub.