Echo reasoning_content exactly as the provider sent it #817
No reviewers
Labels
No labels
move-to-repo
coilyco-bridge-deploy
move-to-repo
coilyco-flight-deck-agent-compose
move-to-repo
coilyco-gaming-eco-app
move-to-repo
coilysiren-inbox
move-to-repo
unknown
🔒⚠️📦⚠️🔒 SANDBOXED 🔒⚠️📦⚠️🔒
autonomy
async-consult
autonomy
epic
autonomy
headless
autonomy
live-collab
c#
priority
P0
priority
P1
priority
P2
priority
P3
priority
P4
role/ai
role/creator
role/design
role/director
role/engineer
role/exec
role/human
role/ops
role/qa
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
coilyco-gaming/sirens-echo!817
Loading…
Reference in a new issue
No description provided.
Delete branch "aos/claude/xh55-reasoning-roundtrip"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes #717.
ward exec gatePASS on all six steps.The blocking question is no longer blocking
#717 was parked on one live request: does DeepSeek accept
"reasoning_content": ""? Two agents named that gap and stopped, and it has sat for two days.It does not need answering first, because the change cannot be worse than what it replaces. The failing case currently sends no key, and no key is precisely the shape that earns the 400. Sending the empty string the model actually returned either fixes those turns or reproduces the error they already get. There is no third outcome. The next
eval-deeprun answers it as a side effect, and the answer belongs here.The proposed fix had a trap in it
#717 said: drop
omitemptyand set the field unconditionally at both build sites, "mechanical, no judgement."It is neither.
chatMessageis one struct for every role:Dropping
omitemptystamps"reasoning_content": ""onto system, user, and tool messages, on every request, on every route. #678 established the defect lives on one evaluation lane and thatsirens-echo/deepseekandsirens-echo/defaulthad zero instances in 7 days. Rewriting every request on the healthy lanes to repair a broken one is a blast radius, not a repair.What it does instead
Both fields become
*string, so presence survives the round trip:""— echoed, so thinking mode gets what it demands.An explicit
nullreads as absent, which is what the old encoding did with it too, so no lane changes shape on that account.This is @Quail's own diagnosis taken at its word: "Under omitempty an empty string and an absent key are the same bytes." The fix is to stop erasing the difference, not to pick one side of it. Same shape as
docs/sirens-echo-indistinguishable-values.md, living in an encoding rather than a log line.Tests
The characterization test is inverted rather than deleted, as it asked, and renamed to match what it now asserts. Two joined it:
TestAnUnnamedReasoningFieldIsNotInvented— the other half of the round trip, and the guard on the healthy lanes.TestOnlyAssistantMessagesCarryReasoning— the trap above, pinned so a futureomitemptydrop fails here.Checked by mutation, not by passing. Reinstating the omitempty erasure fails exactly
TestAnEmptyReasoningContentStillSendsTheKeyand no other test in the package.What I did not establish
Whether the provider accepts the empty string. If it is refused, what remains is putting something in a field the model did not fill, which is a decision rather than a repair — and that part stays exactly where #717 left it.
DeepSeek in thinking mode rejects an assistant message with no reasoning_content: "The `reasoning_content` in the thinking mode must be passed back to the API." Both assistant build sites copied the field, so the harness looked faithful. The encoding was not. ReasoningContent string `json:"reasoning_content,omitempty"` Under omitempty a model that returned an empty reasoning string and one that never mentioned reasoning produce the same bytes: no key. The harness sent the second shape for both and the provider refused the array. #678 measured 24 litellm 400s in 24h on that signature. NOT A BARE DROP OF OMITEMPTY That was the fix #717 proposed, calling it mechanical and judgement-free. It is neither. chatMessage is one struct for every role, so dropping omitempty stamps "reasoning_content": "" onto system, user, and tool messages, on every request, on every route. The defect lives on one evaluation lane; sirens-echo/deepseek and sirens-echo/default had no instance of it in 7 days. Rewriting every request on the healthy lanes to repair a broken one is a blast radius, not a repair. Both fields are *string instead, so presence survives the round trip. A provider that named the field, even as "", gets it echoed. A provider that never named it gets a byte-identical request to before. An explicit null reads as absent, which is what the old encoding did with it too. This is the docs/sirens-echo-indistinguishable-values.md shape, living in an encoding rather than a log line. ON THE QUESTION THIS WAS BLOCKED ON Whether the provider accepts "reasoning_content": "" is still unestablished, and it no longer blocks. The change cannot be worse than what it replaces: the failing case now sends the field where it sent nothing, and nothing is already the shape that earns the 400. It either fixes those turns or reproduces the error they already get. The next evaluation run answers it, and the answer belongs on #717. If the empty string is refused, what remains is putting something in a field the model did not fill, which is a decision rather than a repair. The characterization test is inverted rather than deleted, as it asked, and joined by one asserting the other half of the round trip and one asserting no non-assistant role ever carries the key. Checked by mutation: reinstating the omitempty erasure fails exactly the empty-string test and no other. Closes #717 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-authored-by: Angie <coilysiren@gmail.com> Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Kai Siren <coilysiren@gmail.com> Co-authored-by: Claude <noreply@anthropic.com>