Repository navigation
fix(client): capture and replay Anthropic redacted thinking blocks - #7019
Conversation
Anthropic sometimes replaces a whole thinking block with an encrypted `redacted_thinking` content block. Main had no representation for it: the SSE `content_block_start` failed to decode and the block was dropped, the non-stream response decode failed wholesale, and nothing was ever replayed. A tool loop that replays the bare `tool_use` without its thinking block then fails with "Expected `thinking` or `redacted_thinking`, but found `tool_use`" - an unretryable 400 on every retry of that turn. Fix, ported from fork PR codewhale-hq#91 (redacted-thinking slice only): - models: add a tolerant `ContentBlockStart::RedactedThinking` stream variant (missing-key and explicit-null `data` decode to an empty payload instead of failing the event) and a serde-default `redacted_data` field on the neutral `ContentBlock::Thinking`, so persisted sessions round-trip and older sessions load unchanged - stream capture: park redacted blocks byte-exact the moment they arrive (and finished thinking blocks in wire order), persist them, keep them in the mid-stream retry prefix, and treat a stream that captured one as received content instead of silently re-issuing the request - non-stream capture: rewrite `redacted_thinking` blocks into the neutral shape before decoding so the response (and its tool calls) survives - replay: send the payload back byte-exact on native `*.anthropic.com` routes; withhold it from every non-native Messages host, dropping redacted-only assistant messages instead of emitting `content: []` - other wires (Chat Completions, Responses) pick explicit fields only, so the payload never leaves the Anthropic dialect, and the transcript renders redacted blocks like any textless thinking block - header only, payload bytes never displayed - engine: redacted starts count as actionable content and their payload counts toward the stream content cap Out of scope, still withheld from the fork commit: multi-turn thinking strip rules, Responses pairing integrity, Gemini signature-gate scoping, encrypted-reasoning capture widening, OpenRouter reasoning details, and the compaction-estimator byte accounting. Co-authored-by: zhuowp <9426526+zhuowp@users.noreply.github.com> Signed-off-by: asto <asto18089@126.com>
#7016, #7018 and #7019 from @asto18089 carry a Co-authored-by trailer for @zhuowp (the work was ported from a fork they contributed to). scripts/check-contributor-credit.py, run as the per-PR "Version drift" job, fails those PRs because @zhuowp is absent from web/lib/release-credits.ts. docs/CONTRIBUTORS.md and CHANGELOG.md already credit them. This adds the handle to UNRELEASED_CONTRIBUTORS, which the release cut moves into the release block. Refs #7016 Refs #7018 Refs #7019 Verified: python3 scripts/check-contributor-credit.py ends "Every contributor in the window is credited on all three surfaces"; web check-facts OK; npx tsc --noEmit OK. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NBKbhx19BRbZtvWK8rVKjz
Review follow-up on #7019 (@asto18089). The PR made any thinking block flushed into completed_thinking count as received content in stream_died_with_nothing. flush_thinking_block keeps blocks whose text is whitespace or that carry only opaque state. On Responses/Codex routes a reasoning item with encrypted content but an empty summary is common, so such a block blocked the silent NoContentStreamDeath retry where main retried. A whitespace-only chat reasoning chunk did the same. The guard now counts a completed block only when it is redacted or has non-blank thinking: the rule current_thinking already follows. Redacted blocks still count, which is what the PR needed. The PR's comment above the guard is updated to say so. Refs #7019 Verified on the merged tree (merge b6373c2 plus this change): scripts/dev-test.sh tui redacted thinking anthropic stream_: 442 run, 442 passed. scripts/dev-test.sh protocol redacted: 3 passed. cargo test -p codewhale-models --lib redacted: 2 passed. cargo fmt --check clean. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NBKbhx19BRbZtvWK8rVKjz
|
🤖 This reply is from Claude Opus 5.5, an AI agent working on behalf of the Codewhale maintainers. It is posted through @Hmbown's account, but Hunter did not write it. Merged in b6373c2. Thank you @asto18089. Before this, a I merged it locally because it conflicted with #7014 in one hunk of One follow-up on Not in a release yet. — Written by Claude Opus 5.5 (AI agent) on behalf of the maintainers. |
Adds [Unreleased] entries for #7014, #7015, #7017, #7018, #7019 and #7020, crediting @asto18089 (and @zhuowp for the fork ports). Each merged without a changelog line. The entry for #7020 names its behavior change: the app-server's legacy [providers.custom] table now refuses a non-Chat wire with provider_wire_format_unsupported instead of forwarding a Chat body. Refs #7014 Refs #7015 Refs #7017 Refs #7018 Refs #7019 Refs #7020 Refs #6997 Verified: sync-changelog --check up to date; web check-facts OK; check-contributor-credit "Every contributor in the window is credited on all three surfaces". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NBKbhx19BRbZtvWK8rVKjz
What
Anthropic sometimes returns a
redacted_thinkingcontent block instead of a decodable thinking block (safety/policy on extended-thinking models). The stream decoder had no variant for it: the block was dropped with a warning, the turn survived — but the assistant tool_use then replayed without its thinking block, and Anthropic rejects that with an unretryable 400 on every retry of the turn. The non-stream decode path dropped it wholesale, and the block was never persisted or replayed.This PR captures
redacted_thinkingbyte-exact on both paths, persists it, replays it byte-exact on native Anthropic request building, and withholds the payload from non-native-Anthropic routes (a conservative call — a forwarding proxy might handle the block, but withholding is fail-safe). Redacted payloads never reach the visible transcript; display treats them like any elided thinking.Alongside the capture plumbing, finished thinking blocks now persist in wire order (an ordered buffer instead of last-block-wins) — strictly necessary for correct redacted placement in multi-block turns, and a no-op for histories that only ever carried one block.
Scope
Deliberately excluded from the same-titled fork work: Responses pairing integrity, final-tool-turn strip rules, Gemini signature-gate scoping, encrypted-reasoning capture widening (upstream already captures on all Responses routes that send
include), signature-only-block persistence, and token-estimator accounting for redacted bytes (disclosed here as a known approximation).Tests
"data": nullgateway shapes), non-stream decode shim, native byte-exact replay, non-native strip, redacted-only assistant message drop, serde round-trip with field-absent session compat, and an engine-level pin that the block persists into the follow-up request in wire order.cargo test -p codewhale-models --locked44 passed;-p codewhale-tui --lib redacted34 passed; targetedanthropic/thinking/flushfilters green; clippy-D warningsclean (models, protocol, tui lib);check --workspacegreen.Ported from the Pinvou/CodeWhale fork (#91), redacted-thinking slice only.
No-Issue: Anthropic redacted_thinking blocks were dropped and never replayed, so tool loops hit an unretryable 400