fix(sse): answer tiny-budget reasoning probes with a truncated 200 (#10281) - #10284
Conversation
…iegosouzapw#10284) 🤖 Generated with Codebuff Co-Authored-By: Codebuff <noreply@codebuff.com>
fe6714d to
e0d3105
Compare
…iegosouzapw#10281) Claude Code's /model capability check sends max_tokens: 1. Reasoning models burn the whole probe on thinking, and some upstreams (e.g. api.cline.bot for deepseek-v4-flash) answer the empty outcome with a 5xx "empty response content" instead of a truncated 200. The relayed failure also marked the connection unavailable and poisoned fallback/cooldown bookkeeping for what is only a probe. Detect tiny-budget reasoning probes in the non-streaming providerFailure path and synthesize a valid truncated response (200, empty content, finish_reason "length") — the same semantics errorClassifier.ts already grants to length-truncated empty 200s. Probes no longer poison connection health. Refs diegosouzapw#10281.
e0d3105 to
31ea697
Compare
|
Thanks for this — nicely scoped fix, and the root-cause writeup in the PR description made it easy to review end-to-end. I traced the fix through I ran the new test file locally and it's 3/3 green, and ESLint on the touched files is clean. Everything here looks solid to me. Two small, entirely optional things you could consider (not blockers):
No action needed from you right now — this just needs to go through our CI run (first-time external PRs need a maintainer to approve the workflow run) before it merges. Thanks again for tracking this down and for the clean repro + tests. |
|
Validated in local merge-train |
Summary
Fixes #10281 — Claude Code's
/modelcapability check sendsmax_tokens: 1when switching models. For reasoning-capable models (e.g.cl/deepseek/deepseek-v4-flashon theclineprovider), the whole tiny budget is consumed by thinking, and the upstream (api.cline.bot) answers the non-streaming probe with HTTP 500{"error":{"message":"empty response content"}}.OmniRoute relayed that as
[500]: empty response content, so Claude Code failed the model switch. Worse, the 500 marked the connectionunavailable (500)and poisoned fallback/cooldown bookkeeping for what is only a probe (Account bd3c26b7... unavailable (500), trying fallback→Preserving last upstream error after credential exhaustion).Fix
In the non-streaming
providerFailurepath ofhandleChatCore, when a tiny-budget reasoning probe (max_tokens < REASONING_BUFFER_MIN_TRIGGERon a thinking-capable model) hits an empty-content 5xx from the upstream, answer with a valid truncated response (200, empty content,finish_reason: "length", usage populated) instead of relaying the failure — and skip all failure bookkeeping so the probe never poisons connection health.This mirrors the semantics OmniRoute already grants to
finish_reason: "length"empty 200s inerrorClassifier.ts(LEGIT_EMPTY_OPENAI_FINISH, explicitly "a Claude Codemax_tokens: 1connectivity ping").Why not bump the forwarded budget instead?
resolveReasoningBufferedMaxTokensdeliberately forwards tiny budgets verbatim (REASONING_BUFFER_MIN_TRIGGER = 256, see #6274 — "max_tokens=1 sent as 1001"). Inflating probes would reopen that regression; synthesizing a truncated 200 keeps the probe verbatim upstream while making it succeed for the client.Scope
finish_reason: "length").reasoning_content → contentwhen content is empty" behavior for streams/real responses is tracked separately in feat(providers): per-model compatibility option to copy reasoning_content → content when content is empty #8700 (already open, backlog).Repro
Tests
tests/unit/reasoning-probe-truncated-response-10281.test.ts— probe detection, empty-content 5xx detection, and the truncated-response builder (including the invariant that the synthetic body is not re-flagged byisEmptyContentResponse).tests/unit/reasoning-token-buffer-6274.test.tsstill passes (probe budgets remain verbatim).tsc --project tsconfig.typecheck-core.jsonand ESLint on the changed files are clean.