Skip to content

Give a routed turn back the reasoning its target produced - #1173

Merged
edwin-zvs merged 1 commit into
mainfrom
router-echo-reasoning
Aug 2, 2026
Merged

Give a routed turn back the reasoning its target produced#1173
edwin-zvs merged 1 commit into
mainfrom
router-echo-reasoning

Conversation

@edwin-zvs

Copy link
Copy Markdown
Contributor

The bug

A session pinned to DeepSeek from the native picker fails mid-task with:

API Error: 400 The `reasoning_content` in the thinking mode must be passed back to the API.

Root cause

Measured against the live API, not read from the docs:

assistant turn replayed result
tool calls + reasoning_content 200
tool calls + empty reasoning_content 200
tool calls, no reasoning_content, tool-call id DeepSeek issued and still resolves 200
tool calls, no reasoning_content 400
plain text, no reasoning_content 200

DeepSeek v4 is thinking-by-default, and it refuses a replayed tool-calling turn whose reasoning it cannot see. Every such turn needs the field — reasoning on the last turn only still 400s.

The router could not supply it. In the chat-completions dialect, reasoning was dropped in both directions: reasoning_content was never decoded from a target's stream, and canonical thinking blocks were discarded when emitting a request. The harness cannot cover for it either — Claude Code speaks Anthropic, which has no field for another vendor's reasoning, so it is gone by the time the conversation is replayed.

That leaves DeepSeek's own recovery from a recently issued tool-call id, which holds early in a conversation and lapses later — exactly the "worked for a few turns, then failed permanently" shape reported.

The fix

The router is the only participant that sees both the target's response and the request that replays it, so it keeps the reasoning:

  • Capture — reasoning a target streams is recorded against that turn's tool-call ids, bounded per session.
  • Restore — a replayed tool-calling turn gets its own reasoning back in the field the target's dialect defines.
  • Harness first — a turn whose reasoning the harness carried itself is left alone.
  • Never invented — a turn no longer remembered is sent with an empty reasoning, which the target accepts. Nothing is fabricated, and reasoning is never re-encoded into what the harness reads.

Marked per target and measured rather than assumed (spec 0160's rule); an arm that needs the echo always rebuilds the request body, since byte-forwarding cannot add a field. Recorded as specs/0181-routed-turns-keep-their-reasoning.md.

Verification

The exact body the patched router emits for a Claude-Code-shaped conversation was posted to the live DeepSeek API:

router body WITH restored reasoning (fixed)    200  content='Both words are: alpha beta'
same body WITHOUT it (today's behavior)        400  The `reasoning_content` in the thinking mode must be passed back to the API.

The restored turn carries the reasoning DeepSeek actually produced; a turn outside the memo carries "".

Full workspace suite green (one unrelated CLI timing test flaked under concurrent build load and passes on its own).

Binaries

Only crates/daemon changes → the relevant binary is construct (the daemon lives in it):
/Users/moon/agentd/.claude/worktrees/router-echo-reasoning/target/debug/construct

No user-visible surface changed, so no recording.

A DeepSeek-routed session dies mid-task with "The `reasoning_content` in
the thinking mode must be passed back to the API." DeepSeek's thinking
mode refuses a replayed assistant turn that carries tool calls without
the reasoning it produced for that turn — measured: the same turn is
accepted the moment the field is present, even when it is empty.

The router could not supply it. The chat-completions dialect dropped
reasoning in both directions: responses were decoded without it, and
canonical thinking blocks were discarded when emitting a request. The
harness cannot cover for that either — Anthropic-speaking Claude Code
has no field for another vendor's reasoning, so by the time it replays
the conversation the reasoning is gone. DeepSeek recovers it from a
recently issued tool-call id for a while, which is why a session works
for several turns and then fails for good.

So the router keeps it. Reasoning a target streams is captured against
that turn's tool-call ids and restored onto the turn when the harness
replays it. A turn the harness carried reasoning for is left alone; a
turn no longer remembered is sent with an empty reasoning, which the
target accepts — nothing is invented. The memory is bounded per session
and the reasoning is never re-encoded into what the harness reads.

Marked per target, measured rather than assumed (spec 0160's rule), and
an arm that needs it always rebuilds the request body, since byte
forwarding cannot add a field.

Verified against the live API: the exact body the router now emits is
accepted, and the same body with the reasoning stripped reproduces the
400.
@edwin-zvs
edwin-zvs merged commit 7759f86 into main Aug 2, 2026
1 check passed
@edwin-zvs
edwin-zvs deleted the router-echo-reasoning branch August 2, 2026 18:45
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.

1 participant