Skip to content

fix(v1/messages): default to non-stream for Claude format when ambiguous - #2326

Merged
diegosouzapw merged 1 commit into
cheaper-inference:release/v3.8.0from
thepigdestroyer:fix/v1-messages-non-stream-default
May 17, 2026
Merged

diegosouzapw merged 1 commit into
cheaper-inference:release/v3.8.0from
thepigdestroyer:fix/v1-messages-non-stream-default

Conversation

@thepigdestroyer

Copy link
Copy Markdown
Contributor

Summary

POST /v1/messages without a stream field and with a wildcard Accept header (curl default */*) returns HTTP 502 STREAM_EARLY_EOF in ~1.6s. Stream version (stream:true) works correctly.

Root cause

resolveStreamFlag(undefined, "*/*") falls through to !clientWantsJsonResponse("*/*") which returns true. OmniRoute then takes the streaming branch in chatCore.ts and invokes ensureStreamReadiness on the upstream response.

But Anthropic's Messages API treats stream in the body as the source of truth (Accept header is ignored). Since the body has no stream field, Anthropic returns a single JSON document with Content-Type: application/json. ensureStreamReadiness parses this JSON body as SSE, finds no useful data: {...} frame, hits EOF on the next read, and returns 502.

The bug is OpenAI-format-centric: OpenAI clients omitting stream typically expect SSE by default, but Anthropic's spec is stream: false by default.

Fix

resolveStreamFlag now takes an optional sourceFormat argument:

Explicit body stream: true|false still wins. Explicit Accept: text/event-stream is honored as an SSE opt-in for /v1/messages.

chatCore.ts passes the already-resolved sourceFormat (from detectFormatFromEndpoint) to the new third argument.

Tests

tests/unit/t26-ai-sdk-accept-header-compat.test.ts (11/11 pass):

  • 9 existing tests unchanged.
  • New: sourceFormat=claude applies Anthropic non-stream default (3 ambiguous cases default false; 3 explicit cases still win; Accept text/event-stream still streams).
  • New: non-claude sourceFormats (openai, gemini, codex) preserve pre-[BUG] no response on startup #2325 streaming default; omitting sourceFormat reproduces legacy two-arg behavior exactly.

Repro

Before:

curl -X POST :20128/v1/messages \
  -H 'Authorization: Bearer ...' \
  -H 'Content-Type: application/json' \
  -d '{"model":"claude/claude-opus-4-7","max_tokens":40,
       "messages":[{"role":"user","content":"hi"}]}'

→ HTTP 502 {"error":{"message":"Stream ended before producing useful content",
              "type":"stream_early_eof","code":"STREAM_EARLY_EOF"}}

After: HTTP 200 JSON in 2.4s (matches the stream:false explicit path).

Out of scope

A separate, narrower edge case remains for clients sending Accept: text/event-stream with no body stream field: upstream Anthropic still returns JSON because body lacks stream. That bug existed pre-#2325 and is not regressed here. A future PR can sync body.stream = true into the upstream passthrough when SSE is being requested.

Files

  • open-sse/utils/aiSdkCompat.ts (+23 / -1)
  • open-sse/handlers/chatCore.ts (+4 / -1)
  • tests/unit/t26-ai-sdk-accept-header-compat.test.ts (+36)

Clients posting POST /v1/messages without a `stream` field and with
the curl-default wildcard Accept header were routed through OmniRoute's
streaming branch. The Anthropic Messages API treats `stream` in the
body as the source of truth (ignoring Accept), so upstream returned
JSON; ensureStreamReadiness then parsed the JSON body as SSE, found no
useful frame, and returned HTTP 502 STREAM_EARLY_EOF in ~1.6s.

This commit adds an optional sourceFormat argument to resolveStreamFlag.
When sourceFormat === "claude" and stream is omitted, the default is
now `false` (matching the Anthropic spec). An explicit
`Accept: text/event-stream` is still honored as an SSE opt-in. Existing
non-Claude callers (openai, gemini, codex) keep the pre-existing
streaming default, so the cheaper-inference#656 fix is preserved.

Tests:
- tests/unit/t26-ai-sdk-accept-header-compat.test.ts: 11/11 pass
  (9 existing + 2 new for sourceFormat=claude and non-claude paths).

Repro pre-fix:
  curl -X POST :20128/v1/messages -H 'Authorization: Bearer ...' \
    -H 'Content-Type: application/json' \
    -d '{"model":"claude/claude-opus-4-7","max_tokens":40,
         "messages":[{"role":"user","content":"hi"}]}'
  -> HTTP 502 stream_early_eof in 1.4s

Post-fix: HTTP 200 JSON in 2.4s (matches stream:false explicit path).

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a sourceFormat parameter to the resolveStreamFlag utility to handle provider-specific streaming defaults. Specifically, it implements the Anthropic Messages API specification where requests default to non-streaming unless an explicit text/event-stream Accept header is provided, resolving issues with STREAM_EARLY_EOF errors on /v1/messages. The changes include updates to the chat handler, the compatibility utility, and new unit tests to verify the behavior for both Claude and other providers. I have no feedback to provide as there were no review comments.

@diegosouzapw
diegosouzapw changed the base branch from main to release/v3.8.0 May 17, 2026 22:36
@diegosouzapw
diegosouzapw merged commit 0f15797 into cheaper-inference:release/v3.8.0 May 17, 2026
3 checks passed
@diegosouzapw

Copy link
Copy Markdown
Collaborator

Obrigado @thepigdestroyer pela contribuição! 🙌

Seu fix para o bug de STREAM_EARLY_EOF no POST /v1/messages foi integrado na release/v3.8.0. A abordagem de adicionar sourceFormat como parâmetro opcional em resolveStreamFlag é elegante e completamente backward-compatible — preserva o comportamento dos clientes OpenAI/Gemini/Codex enquanto aplica o padrão correto da spec Anthropic.

Este fix estará no OmniRoute v3.8.0. 🚀

// Only honor an explicit text/event-stream Accept header as a streaming opt-in
// for /v1/messages — otherwise default to non-stream so upstream JSON responses
// are surfaced correctly instead of triggering stream_early_eof.
if (sourceFormat === "claude") {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: Hardcoded string literal "claude" instead of FORMATS.CLAUDE constant

The rest of chatCore.ts consistently uses FORMATS.CLAUDE (lines 545, 556, 580, 587, 941, 2409, 2599, 4102, 4176) to check for the Claude format. If FORMATS.CLAUDE is ever renamed in open-sse/translator/formats.ts, this hardcoded string will silently break without a compile-time error.

Consider using FORMATS.CLAUDE here or at least typing sourceFormat as a union of known format values to catch mismatches.

export function resolveStreamFlag(
bodyStream: unknown,
acceptHeader: unknown,
sourceFormat?: string

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SUGGESTION: sourceFormat parameter typed as string is too loose

The parameter accepts any string value, making it easy to accidentally pass an incorrect format identifier. Consider typing it more strictly, e.g. sourceFormat?: keyof typeof FORMATS | string or importing a dedicated format type from formats.ts. This would improve type safety and IDE autocomplete for callers.

@kilo-code-bot

kilo-code-bot Bot commented May 21, 2026 •

Copy link
Copy Markdown

Code Review Summary

Status: 2 Issues Found (1 WARNING, 1 SUGGESTION) | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 1
SUGGESTION 1
Issue Details (click to expand)

WARNING

File Line Issue
open-sse/utils/aiSdkCompat.ts 42 Hardcoded string literal "claude" instead of FORMATS.CLAUDE constant used everywhere else in codebase. If FORMATS.CLAUDE is renamed, this silently breaks.

SUGGESTION

File Line Issue
open-sse/utils/aiSdkCompat.ts 32 sourceFormat parameter typed as string — too loose, consider union type or imported format enum for better type safety and IDE autocomplete.
Other Observations (not in diff)

No issues found outside the diff.

Files Reviewed (3 files)
  • open-sse/utils/aiSdkCompat.ts - 2 issues
  • open-sse/handlers/chatCore.ts - No issues (change is correct — passes sourceFormat from detectFormatFromEndpoint)
  • tests/unit/t26-ai-sdk-accept-header-compat.test.ts - No issues (comprehensive coverage of new behavior and backward compatibility)

Reviewed by qwen3.6-plus · 424,354 tokens

HouMinXi pushed a commit to HouMinXi/OmniRoute that referenced this pull request Aug 2, 2026
…ous (cheaper-inference#2326)

Integrated into release/v3.8.0 — fixes STREAM_EARLY_EOF on POST /v1/messages when stream is omitted
Poid-ZA pushed a commit to Poid-ZA/OmniRoute that referenced this pull request Aug 5, 2026
…ous (cheaper-inference#2326)

Integrated into release/v3.8.0 — fixes STREAM_EARLY_EOF on POST /v1/messages when stream is omitted
muhamadgalihsaputra pushed a commit to niyatna/NiyatnaRoute that referenced this pull request Sep 27, 2026
…ous (cheaper-inference#2326)

Integrated into release/v3.8.0 — fixes STREAM_EARLY_EOF on POST /v1/messages when stream is omitted
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants