Repository navigation
fix(sse): surface an error for a truly empty Claude stream (#12398) - #13285
Merged
Merged
Conversation
Extend the Claude empty-response detector in open-sse/utils/stream.ts to also catch an upstream connection that opens (HTTP 200) and closes having sent literally zero bytes -- no message_start at all. The existing hasClaudeAssistantLifecycle() gate only fired once a lifecycle event had been observed, so this shape silently completed the client stream with a 200 and no content instead of surfacing a 502, matching the reported symptom for claude-fable-5-max past ~1800 messages. New streamClaudeEmptyBody.ts module keeps the frozen stream.ts file size unchanged while adding the combined partial-lifecycle + truly-empty check.
Githab-capibara
added a commit
to Githab-capibara/OmniRoute
that referenced
this pull request
Sep 17, 2026
…nference#12398) (cheaper-inference#13285) Merged as part of the 39-PR owner batch of 2026-09-11, validated as a unit. Boarded into one consolidated worktree cut from `release/v3.8.51` with the other 38 — zero conflicts between them. - ESLint over every changed file: no errors (the only finding was one suppression entry the batch emptied, pruned on cheaper-inference#13243) - `typecheck:core` clean; `check:dashboard-typecheck` OK (206 pre-existing, within baseline); `check:changelog-integrity` OK - complexity 2821 / baseline 3218 and cognitive-complexity 1272 / baseline 1437 — both under baseline - 256 assertions green: 246 under node:test and 10 under vitest, which is where `tests/unit/**/*.test.tsx` actually runs - `check-file-size`: `chatCore.ts` rebaselined 6144 → 6146 for cheaper-inference#13278 and cheaper-inference#13276, annotated and landed on cheaper-inference#13243⚠️ base-red inherited: cheaper-inference#12732 — the provider count (356 in the docs vs the 358 the modules define) and `open-sse/utils/stream.ts` at 3115 > frozen 3098 both reproduce on the pure tip with zero contribution from this batch.
muhamadgalihsaputra
pushed a commit
to niyatna/NiyatnaRoute
that referenced
this pull request
Sep 27, 2026
…nference#12398) (cheaper-inference#13285) Merged as part of the 39-PR owner batch of 2026-09-11, validated as a unit. Boarded into one consolidated worktree cut from `release/v3.8.51` with the other 38 — zero conflicts between them. - ESLint over every changed file: no errors (the only finding was one suppression entry the batch emptied, pruned on cheaper-inference#13243) - `typecheck:core` clean; `check:dashboard-typecheck` OK (206 pre-existing, within baseline); `check:changelog-integrity` OK - complexity 2821 / baseline 3218 and cognitive-complexity 1272 / baseline 1437 — both under baseline - 256 assertions green: 246 under node:test and 10 under vitest, which is where `tests/unit/**/*.test.tsx` actually runs - `check-file-size`: `chatCore.ts` rebaselined 6144 → 6146 for cheaper-inference#13278 and cheaper-inference#13276, annotated and landed on cheaper-inference#13243⚠️ base-red inherited: cheaper-inference#12732 — the provider count (356 in the docs vs the 358 the modules define) and `open-sse/utils/stream.ts` at 3115 > frozen 3098 both reproduce on the pure tip with zero contribution from this batch.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #12398
Root cause
open-sse/utils/stream.ts's Claude empty-response detector(
hasClaudeAssistantLifecycle()) only fired once at least one Claude SSElifecycle event (
message_start/message_delta/message_stop) hadbeen observed. When an upstream Claude-format response closes having sent
literally zero bytes (HTTP 200, not even a
message_start— the shapea connection held open for ~14.5s and then closed with nothing on it would
produce),
transform()is never even invoked, so none of the lifecycleflags get set and the flush path silently completed the client stream with
a 200 and no content instead of surfacing a 502. This matches the reported
symptom for
claude-fable-5-maxpast ~1800 messages ("the request does noterror; it completes with no content").
The other variant (
message_startarrives, thenmessage_delta/message_stopwith no content block) was already correctly detected andturned into a 502 — that path (#3685) is unchanged and still covered by
tests/unit/claude-empty-stream-error-3685.test.ts.Fix
Extended the detector so a Claude-format client that never saw a single
upstream chunk (
timing.firstByteAt === nullat flush time) is alsoaborted with the existing
emitClaudeEmptyStreamErrorAndAbort()502 path,at both flush call sites (PASSTHROUGH and TRANSLATE). The combined check
lives in a new
open-sse/utils/streamClaudeEmptyBody.tsmodule so thefrozen
open-sse/utils/stream.tsfile size does not grow (net line countunchanged — an old single-purpose helper was folded into the new combined
one).
Per the plan's own risk note, this fix does not attempt an automatic
upstream retry-on-empty-stream — that needs response buffering before any
bytes reach the client and is a materially larger change; left as a
follow-up if wanted.
Regression test
tests/unit/claude-stream-truly-empty-body.test.ts— RED before the fix,GREEN after:
(RED confirmed by running the test against
origin/release/v3.8.51'sunmodified
open-sse/utils/stream.tsviagit show, then restoring thefix — never via
git stash.)Gates run
node scripts/check/check-file-size.mjs— OK (stream.tsnet line countunchanged: 3097 → 3097)
node scripts/check/check-complexity.mjs— OK, 2798 violations (baseline 3218)node scripts/check/check-cognitive-complexity.mjs— OK, 1265 violations (baseline 1437)npm run typecheck:core— cleannpx eslint --suppressions-location config/quality/eslint-suppressions.jsonon all changed files — cleannode --import tsx/esm --test tests/unit/claude-stream-truly-empty-body.test.ts— 2/2 passclaude-empty-stream-error-3685,stream-claude-delta-contract,claude-stream-reconstructed-body,empty-choices-no-inject,stream-onfailure-callback-logging,stream-utils,stream-utilities,responses-continuation-*,stream-numeric-ids,stream-passthrough-error-redaction,sse-comments-optout-9305,sse-finish-reason-synthesis-7800,stream-non-json-sse,10017-sse-control-lines-leak-openai-clients,stream-translate-usage-trailing,nvidia-tool-compatibility-2840,stream-prompt-tokens-zero-upstream,codex-tool-handoff-disconnect-499,stream-passthrough-usage-estimation,transform-stream-hwm,responses-commentary-*— all 179 pass, includingthe idle-timeout test (confirms the new check does not race the idle watchdog).
VPS follow-up (not part of this PR)
The plan flags a separate VPS validation to confirm why Anthropic's
upstream produces this specific shape for
claude-fable-5-maxat ~1800messages — that requires live OAuth traffic at scale and doesn't change
the code-level fix, which is scoped to always surfacing an error instead
of a silent empty 200 regardless of the exact upstream trigger.