Skip to content

fix(sse): a normal empty stop is a valid empty turn, not a provider failure (#16072) - #16091

Merged
diegosouzapw merged 1 commit into
diegosouzapw:release/v3.8.52from
skygunner:fix/16072-empty-stop-turn
Oct 10, 2026
Merged

diegosouzapw merged 1 commit into
diegosouzapw:release/v3.8.52from
skygunner:fix/16072-empty-stop-turn

Conversation

@skygunner

Copy link
Copy Markdown
Contributor

⚠️ base-red inherited: #15306

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) or stop_reason: "end_turn" / "stop_sequence" (Claude) and no content. The #8649 empty-content guard did not recognise a normal terminal: LEGIT_EMPTY_TERMINAL_REASONS lists 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()), and resolveSilentCloseOutcome now 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_turn on Claude-format streams, and #14160 trusts an empty normal stop for first-party APIs. Scope is the streaming path; the non-streaming predicate keeps its TRUSTED_EMPTY_STOP_PROVIDERS mechanism untouched.

Validation

  • new 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 regression
  • tests/unit/empty-stream-no-content-8649.test.ts contract updated for the narrowed guard; adjacent stream suites (8649/3685/15260/15489/abort): 46/46
  • all 30 stream-related unit suites: 321/321
  • npm run typecheck:core clean; check-api-typecheck.mjs within frozen baseline; eslint clean
  • inherited base failures, reproduced on the untouched tip before pushing: chaosVirtualCombo.test.ts and the check:mutation-test-coverage --strict drift for tests/unit/search-internal-error-sanitization-15159.test.ts (fix(security): route /v1/search's internal-exception branch through buildErrorBody #15951 introduced the test; tap.testFiles was never updated)

@diegosouzapw
diegosouzapw merged commit 7f1f2d7 into diegosouzapw:release/v3.8.52 Oct 10, 2026
26 of 51 checks passed
@diegosouzapw

Copy link
Copy Markdown
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.

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.

fix(resilience): a valid empty assistant turn (finish_reason stop) becomes 502 "Provider returned empty content" and walks the whole combo

2 participants