You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The Anthropic adapter parses thinking blocks into the normalized ResponseMessage.ReasoningContent, but assistant history for the next tool-result request is rebuilt from only visible text and tool_use blocks. Two things go wrong for extended-thinking models:
The signed thinking block (and any redacted_thinking block) is not replayed. Anthropic requires thinking blocks around tool use to be passed back unmodified and in their original order; a strict endpoint can reject the continuation, and a tolerant one returns HTTP 200 while silently losing reasoning continuity.
It is worse than a plain drop: a thinking + tool_use turn has no visible text, so ChatResponse.Content() falls back to the reasoning text, and the private thinking is replayed as an ordinary visible text block. Hidden reasoning leaks into visible assistant content, and its signature disappears entirely.
Source trace
All references are main @ 71d2981:
mapAnthropicResponse flattens thinking blocks into a joined ReasoningContent string — the block signature is already gone at this point.
buildAnthropicParams emits only text and tool_use blocks for assistant history; no code path emits a thinking block param.
Deterministic reproduction
No live endpoint or API key needed — this exercises the adapter's own response→history→request chain:
funcTestAnthropicToolUseReplayPreservesSignedThinking(t*testing.T) {
raw:=`{ "id": "msg_01", "type": "message", "role": "assistant", "model": "claude-sonnet-4-5", "content": [ {"type": "thinking", "thinking": "private plan the API requires on replay", "signature": "sig_abc123"}, {"type": "tool_use", "id": "toolu_01", "name": "file_read", "input": {"path": "main.go"}} ], "stop_reason": "tool_use" }`varsdkMsg anthropic.Messageiferr:=json.Unmarshal([]byte(raw), &sdkMsg); err!=nil {
t.Fatal(err)
}
client:=NewAnthropicClient(ClientConfig{URL: "https://api.anthropic.com"})
resp:=client.mapAnthropicResponse(&sdkMsg)
// Same response-to-history conversion used by llmloop.Runner.history:=NewToolCallMessage(resp.Content(), resp.ToolCalls())
params, err:=client.buildAnthropicParams("claude-sonnet-4-5", ChatRequest{
Messages: []Message{
{Role: "user", Content: "review this file"},
history,
NewToolResultMessage("toolu_01", "package main"),
},
})
iferr!=nil {
t.Fatal(err)
}
payload, err:=json.Marshal(params.Messages)
iferr!=nil {
t.Fatal(err)
}
if!bytes.Contains(payload, []byte(`"type":"thinking"`)) ||!bytes.Contains(payload, []byte("sig_abc123")) {
t.Fatalf("assistant replay dropped the signed thinking block: %s", payload)
}
}
Run:
go test ./internal/llm -run '^TestAnthropicToolUseReplayPreservesSignedThinking$' -count=1
Actual result on main @ 71d2981:
--- FAIL: TestAnthropicToolUseReplayPreservesSignedThinking (0.00s)
assistant replay dropped the signed thinking block: [{"content":[{"text":"review this file","type":"text"}],"role":"user"},{"content":[{"text":"private plan the API requires on replay","type":"text"},{"id":"toolu_01","input":{"path":"main.go"},"name":"file_read","type":"tool_use"}],"role":"assistant"},{"content":[{"tool_use_id":"toolu_01","is_error":false,"content":[{"text":"package main","type":"text"}],"type":"tool_result"}],"role":"user"}]
Note the replayed assistant turn: the signed thinking block came back as {"text":"private plan the API requires on replay","type":"text"} — private reasoning converted to visible text, sig_abc123 nowhere in the request.
Expected behavior
The request following a thinking + tool_use turn should carry the original ordered assistant content blocks — signed thinking / redacted_thinking preserved unmodified before tool_use — and hidden thinking should never be converted into visible text.
An adapter-owned anthropicReplay envelope behind the same private replayEnvelope seam discussed in #805/#806, rather than reusing the OpenAI Chat envelope (different protocol, different invariants):
retain the complete ordered assistant content blocks for the next Messages request — thinking with its signature, redacted_thinking, text, and tool_use — without flattening or reordering;
keep signatures and hidden/redacted thinking out of normalized JSON, persisted logs, and the generic ChatResponse fallback;
participate in message cloning, token accounting, and compression through the existing seam;
define an explicit model-transition policy, since signed thinking state may not be valid after switching models;
if Anthropic streaming is enabled, assemble thinking_delta/signature_delta/block-stop events before a turn becomes replayable.
Key regression coverage for any fix: a strict two-request fake Anthropic endpoint verifying that every block is replayed exactly and in order before the matched tool_result blocks; signed and redacted thinking each covered; multiple tool calls keep original order and IDs; persistence/log tests prove signatures and hidden thinking are not exposed.
OpenCodeReview version
main:71d29810be1e7f7b183c021fe9b06a2e850f1037protocol: anthropic)Bug description
The Anthropic adapter parses
thinkingblocks into the normalizedResponseMessage.ReasoningContent, but assistant history for the next tool-result request is rebuilt from only visible text andtool_useblocks. Two things go wrong for extended-thinking models:thinkingblock (and anyredacted_thinkingblock) is not replayed. Anthropic requires thinking blocks around tool use to be passed back unmodified and in their original order; a strict endpoint can reject the continuation, and a tolerant one returns HTTP 200 while silently losing reasoning continuity.ChatResponse.Content()falls back to the reasoning text, and the private thinking is replayed as an ordinary visibletextblock. Hidden reasoning leaks into visible assistant content, and itssignaturedisappears entirely.Source trace
All references are
main@71d2981:mapAnthropicResponseflattensthinkingblocks into a joinedReasoningContentstring — the blocksignatureis already gone at this point.RunPerFilerebuilds history viaresp.Content()+resp.ToolCalls()(the lossy projection described in Thinking state is parsed but dropped when assistant tool-call turns are replayed #805), andContent()substitutes reasoning text when visible content is empty.buildAnthropicParamsemits onlytextandtool_useblocks for assistant history; no code path emits athinkingblock param.Deterministic reproduction
No live endpoint or API key needed — this exercises the adapter's own response→history→request chain:
Run:
Actual result on
main@71d2981:Note the replayed assistant turn: the signed thinking block came back as
{"text":"private plan the API requires on replay","type":"text"}— private reasoning converted to visible text,sig_abc123nowhere in the request.Expected behavior
The request following a thinking + tool_use turn should carry the original ordered assistant content blocks — signed
thinking/redacted_thinkingpreserved unmodified beforetool_use— and hidden thinking should never be converted into visible text.Anthropic reference: https://platform.claude.com/docs/en/about-claude/models/extended-thinking-models#thinking-encryption
Proposed direction
An adapter-owned
anthropicReplayenvelope behind the same privatereplayEnvelopeseam discussed in #805/#806, rather than reusing the OpenAI Chat envelope (different protocol, different invariants):thinkingwith its signature,redacted_thinking, text, andtool_use— without flattening or reordering;ChatResponsefallback;thinking_delta/signature_delta/block-stop events before a turn becomes replayable.Key regression coverage for any fix: a strict two-request fake Anthropic endpoint verifying that every block is replayed exactly and in order before the matched
tool_resultblocks; signed and redacted thinking each covered; multiple tool calls keep original order and IDs; persistence/log tests prove signatures and hidden thinking are not exposed.