Skip to content

fix(sse): answer tiny-budget reasoning probes with a truncated 200 (#10281) - #10284

Merged
diegosouzapw merged 2 commits into
diegosouzapw:release/v3.8.50from
harkaranbrar7:fix/reasoning-probe-truncated-200
Aug 16, 2026
Merged

fix(sse): answer tiny-budget reasoning probes with a truncated 200 (#10281)#10284
diegosouzapw merged 2 commits into
diegosouzapw:release/v3.8.50from
harkaranbrar7:fix/reasoning-probe-truncated-200

Conversation

@harkaranbrar7

Copy link
Copy Markdown
Contributor

Summary

Fixes #10281 — Claude Code's /model capability check sends max_tokens: 1 when switching models. For reasoning-capable models (e.g. cl/deepseek/deepseek-v4-flash on the cline provider), 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 connection unavailable (500) and poisoned fallback/cooldown bookkeeping for what is only a probe (Account bd3c26b7... unavailable (500), trying fallbackPreserving last upstream error after credential exhaustion).

Fix

In the non-streaming providerFailure path of handleChatCore, when a tiny-budget reasoning probe (max_tokens < REASONING_BUFFER_MIN_TRIGGER on 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 in errorClassifier.ts (LEGIT_EMPTY_OPENAI_FINISH, explicitly "a Claude Code max_tokens: 1 connectivity ping").

Why not bump the forwarded budget instead?

resolveReasoningBufferedMaxTokens deliberately 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

Repro

curl http://localhost:20128/v1/chat/completions \
  -H "Authorization: Bearer $OMNIROUTE_KEY" \
  -H "Content-Type: application/json" \
  -d '{"model":"cl/deepseek/deepseek-v4-flash","messages":[{"role":"user","content":"ping"}],"max_tokens":1,"stream":false}'
# before: {"error":{"message":"[500]: empty response content",...}}
# after:  200 with {choices:[{message:{content:""},finish_reason:"length"}],usage:{completion_tokens:1}}

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 by isEmptyContentResponse).
  • Existing tests/unit/reasoning-token-buffer-6274.test.ts still passes (probe budgets remain verbatim).
  • tsc --project tsconfig.typecheck-core.json and ESLint on the changed files are clean.

harkaranbrar7 added a commit to harkaranbrar7/OmniRoute that referenced this pull request Aug 13, 2026
…iegosouzapw#10284)

🤖 Generated with Codebuff
Co-Authored-By: Codebuff <noreply@codebuff.com>
@harkaranbrar7
harkaranbrar7 force-pushed the fix/reasoning-probe-truncated-200 branch from fe6714d to e0d3105 Compare August 13, 2026 19:20
@harkaranbrar7
harkaranbrar7 changed the base branch from main to release/v3.8.50 August 13, 2026 19:20
@harkaranbrar7
harkaranbrar7 marked this pull request as draft August 13, 2026 19:21
@harkaranbrar7
harkaranbrar7 marked this pull request as ready for review August 13, 2026 19:27
…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.
@diegosouzapw

Copy link
Copy Markdown
Owner

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 handleChatCore's non-streaming failure path and it correctly reuses the existing providerFailure: / break providerFailure pattern the codebase already uses for the signature-recovery case, so it cleanly skips the connection-health bookkeeping that was getting poisoned by these probes, and falls through to the normal success/parsing path. The synthetic finish_reason: "length" response also correctly passes the existing isEmptyContentResponse() legitimacy check in errorClassifier.ts — good catch keeping that invariant, and nice that you asserted it directly in the test.

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):

  • buildReasoningProbeTruncatedResponse hardcodes usage.prompt_tokens: 0 — could reflect the real prompt token count if it's cheaply available at that call site.
  • The EMPTY_CONTENT_FAILURE_RE regex matches a couple of message phrasings beyond the one you actually saw upstream ("empty response content") — fine to leave as-is given it's gated behind the tiny-budget-probe check first, but worth knowing it's slightly broader than the observed repro.

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.

@diegosouzapw
diegosouzapw merged commit 710e43e into diegosouzapw:release/v3.8.50 Aug 16, 2026
3 checks passed
@diegosouzapw

Copy link
Copy Markdown
Owner

Validated in local merge-train .claude/worktrees/merge-train-20260816-000002-suite.log on devbox-vm-06-dev002 @ 8be62df65f61cc4cf29cdc3e6bc38173ffafa4e3 (FAST gates green: static + changed tests + vitest; daily full-suite run still required). Merged under the batch merge-train protocol (merge-gates §7). ⚠️ base-red inherited: #10523 (ServiceSupervisor order-dependent test, not touched by this batch).

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(api): answer tiny-budget reasoning probes (max_tokens:1) with truncated 200 — no 500 empty response, no cooldown poisoning

2 participants