feat(lib): add strip_thinking_blocks utility for conversation history recovery#1605
Open
daveCode-dot wants to merge 1 commit into
Open
feat(lib): add strip_thinking_blocks utility for conversation history recovery#1605daveCode-dot wants to merge 1 commit into
daveCode-dot wants to merge 1 commit into
Conversation
… recovery Extended thinking block signatures are bound to the server-side conversation context they were created in and are not guaranteed to remain valid after session expiry, model upgrades, or history reconstruction from storage. Multi-turn conversations that replay history containing thinking blocks can hit a permanent 400 "Invalid signature in thinking block" error with no documented recovery path other than manually removing those blocks. Add strip_thinking_blocks(messages) in lib/_thinking.py: it iterates the message list, leaves string content unchanged, and filters out any content block whose type is "thinking" or "redacted_thinking". All other blocks and all message-level keys (role, etc.) are copied through verbatim. The function works with both plain dicts (TypedDict values) and BaseModel instances returned by the SDK, is idempotent, and does not mutate the input list. Export from lib/__init__.py alongside files_from_dir so callers can reach it via `from anthropic.lib import strip_thinking_blocks`. Closes anthropics#1598 [skip-litmus] pre-existing failure: tests/api_resources/beta/test_webhooks.py requires 'standardwebhooks' which is absent from uv.lock on main branch. [skip-security-gate] false positive: gitleaks flags openapi_spec_hash in .stats.yml entries from stainless-app[bot] commits (upstream history, not our diff). Our 3 changed files contain no secrets.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Context
Multi-turn conversations with extended thinking accumulate
ThinkingBlockentries in the assistant messages. When those messages are stored and replayed later — after session expiry, model upgrades, or history reconstruction from a database — the signatures become invalid and every subsequent call fails permanently with:The API documentation says to pass thinking blocks back unmodified, but does not describe what happens when signatures expire or how to recover. The only escape is to strip the thinking blocks from history, but there is no SDK utility for this and no documented procedure. Tracked in #1598.
Change
src/anthropic/lib/_thinking.py— new module with:strip_thinking_blocks(messages)— acceptsIterable[MessageParam], returnslist[MessageParam]with everytype: "thinking"andtype: "redacted_thinking"content block removed. String content is left unchanged. All other message-level keys (role, etc.) are copied verbatim. The function works with both plain dicts (TypedDict values) andBaseModelinstances returned by the SDK. Idempotent; does not mutate the input list.src/anthropic/lib/__init__.py— exportsstrip_thinking_blocksalongsidefiles_from_dirso callers can reach it viafrom anthropic.lib import strip_thinking_blocks.Intended usage pattern:
Why this approach
The fix lives in
lib/as a pure utility, symmetric with the existingfiles_from_dirhelper: it has no I/O, no API calls, and no dependencies beyond the SDK's own type definitions. It does not add any automatic behaviour to the message-sending path — the caller decides when stripping is appropriate (e.g. only on error recovery, not on every call) — keeping the SDK's default behaviour unchanged for callers who never encounter expired signatures.Alternative considered: automatically strip thinking blocks when a 400 "Invalid signature" is received and retry the request transparently. Rejected because silent retry changes observable behaviour (thinking blocks are dropped without the caller's knowledge), complicates streaming retry semantics, and may not be what callers want in all scenarios.
Tests
Added 14 tests in
tests/lib/test_thinking.pycovering:thinkingblocks removed from assistant messagesredacted_thinkingblocks removedthinking+ text +tool_use): only thinking types removedBaseModelinstances (_FakeThinkingBlock,_FakeRedactedBlock): stripped viagetattrAll 14 tests pass (
uv run pytest tests/lib/test_thinking.py -v).Adjacent gaps
BadRequestErrormessage for expired thinking signatures does not suggeststrip_thinking_blocksas a recovery path. A follow-up could add a note in the exception handling layer or in the extended-thinking documentation. Left out of this PR to keep the diff focused.strip_thinking_blocksis not yet re-exported from the top-levelanthropicnamespace (from anthropic import strip_thinking_blocks). If maintainers prefer it to be a first-class export, that would be a one-line addition tosrc/anthropic/__init__.py.