Repository navigation
fix(sse): a normal empty stop is a valid empty turn, not a provider failure (#16072) - #16091
Merged
diegosouzapw merged 1 commit intoOct 10, 2026
Conversation
diegosouzapw
merged commit Oct 10, 2026
7f1f2d7
into
diegosouzapw:release/v3.8.52
26 of 51 checks passed
Owner
|
Thanks @skygunner — a client-visible normal stop with empty content is a valid empty turn. A stream that dies before any terminal stays an error. This is in the upcoming release. |
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.
Fixes #16072
Summary
When the model's correct answer is an empty assistant turn ("no reply needed"), upstreams end the turn normally with
finish_reason: "stop"(OpenAI) orstop_reason: "end_turn"/"stop_sequence"(Claude) and no content. The #8649 empty-content guard did not recognise a normal terminal:LEGIT_EMPTY_TERMINAL_REASONSlists only length/tool_calls/content_filter/max_tokens/tool_use, so the stream was rewritten into "Provider returned empty content", the combo failed over and retried to its cap, and the client got 502 — the reporter measured 22 upstream attempts and ~5.18M input tokens for one 235K-token turn.This teaches the shared stream content watcher to recognise a NORMAL stop terminal (
sawNormalStopTerminal()), andresolveSilentCloseOutcomenow passes the empty turn through only when the client actually received that terminal frame (clientTerminalSeen). A stream dropped before any terminal never delivered the upstream's verdict and keeps the #8649 empty-content error — the empty-shell/drop protection is unchanged.This mirrors the philosophy upstream already accepted: #15505 passes a clean empty
end_turnon Claude-format streams, and #14160 trusts an empty normal stop for first-party APIs. Scope is the streaming path; the non-streaming predicate keeps itsTRUSTED_EMPTY_STOP_PROVIDERSmechanism untouched.Validation
tests/unit/empty-stop-turn-16072.test.ts(red → green): OpenAI stop pass-through, Claude end_turn/stop_sequence pass-through, dropped-stream-without-terminal still errors, reasoning-only stop pass-through, tool-call regressiontests/unit/empty-stream-no-content-8649.test.tscontract updated for the narrowed guard; adjacent stream suites (8649/3685/15260/15489/abort): 46/46npm run typecheck:coreclean;check-api-typecheck.mjswithin frozen baseline; eslint cleanchaosVirtualCombo.test.tsand thecheck:mutation-test-coverage --strictdrift fortests/unit/search-internal-error-sanitization-15159.test.ts(fix(security): route /v1/search's internal-exception branch through buildErrorBody #15951 introduced the test;tap.testFileswas never updated)