Repository navigation
fix(v1/messages): default to non-stream for Claude format when ambiguous - #2326
Conversation
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).
There was a problem hiding this comment.
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.
0f15797
into
cheaper-inference:release/v3.8.0
|
Obrigado @thepigdestroyer pela contribuição! 🙌 Seu fix para o bug de STREAM_EARLY_EOF no 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") { |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
Code Review SummaryStatus: 2 Issues Found (1 WARNING, 1 SUGGESTION) | Recommendation: Address before merge Overview
Issue Details (click to expand)WARNING
SUGGESTION
Other Observations (not in diff)No issues found outside the diff. Files Reviewed (3 files)
Reviewed by qwen3.6-plus · 424,354 tokens |
…ous (cheaper-inference#2326) Integrated into release/v3.8.0 — fixes STREAM_EARLY_EOF on POST /v1/messages when stream is omitted
…ous (cheaper-inference#2326) Integrated into release/v3.8.0 — fixes STREAM_EARLY_EOF on POST /v1/messages when stream is omitted
…ous (cheaper-inference#2326) Integrated into release/v3.8.0 — fixes STREAM_EARLY_EOF on POST /v1/messages when stream is omitted
Summary
POST /v1/messageswithout astreamfield and with a wildcardAcceptheader (curl default*/*) returns HTTP 502STREAM_EARLY_EOFin ~1.6s. Stream version (stream:true) works correctly.Root cause
resolveStreamFlag(undefined, "*/*")falls through to!clientWantsJsonResponse("*/*")which returnstrue. OmniRoute then takes the streaming branch inchatCore.tsand invokesensureStreamReadinesson the upstream response.But Anthropic's Messages API treats
streamin the body as the source of truth (Accept header is ignored). Since the body has nostreamfield, Anthropic returns a single JSON document withContent-Type: application/json.ensureStreamReadinessparses this JSON body as SSE, finds no usefuldata: {...}frame, hits EOF on the next read, and returns 502.The bug is OpenAI-format-centric: OpenAI clients omitting
streamtypically expect SSE by default, but Anthropic's spec isstream: falseby default.Fix
resolveStreamFlagnow takes an optionalsourceFormatargument:sourceFormat === "claude"+ ambiguous stream + no explicittext/event-streamin Accept → default to false (matches Anthropic spec).stream: trueignored when client sendsAccept: application/jsonheader #656 fix and OpenAI streaming default are preserved.Explicit body
stream: true|falsestill wins. ExplicitAccept: text/event-streamis honored as an SSE opt-in for/v1/messages.chatCore.tspasses the already-resolvedsourceFormat(fromdetectFormatFromEndpoint) to the new third argument.Tests
tests/unit/t26-ai-sdk-accept-header-compat.test.ts(11/11 pass):sourceFormat=claudeapplies Anthropic non-stream default (3 ambiguous cases default false; 3 explicit cases still win; Accept text/event-stream still streams).openai,gemini,codex) preserve pre-[BUG] no response on startup #2325 streaming default; omitting sourceFormat reproduces legacy two-arg behavior exactly.Repro
Before:
After: HTTP 200 JSON in 2.4s (matches the
stream:falseexplicit path).Out of scope
A separate, narrower edge case remains for clients sending
Accept: text/event-streamwith no bodystreamfield: upstream Anthropic still returns JSON because body lacksstream. That bug existed pre-#2325 and is not regressed here. A future PR can syncbody.stream = trueinto 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)