Skip to content

feat(lib): add strip_thinking_blocks utility for conversation history recovery#1605

Open
daveCode-dot wants to merge 1 commit into
anthropics:mainfrom
daveCode-dot:fix/thinking-strip-utility
Open

feat(lib): add strip_thinking_blocks utility for conversation history recovery#1605
daveCode-dot wants to merge 1 commit into
anthropics:mainfrom
daveCode-dot:fix/thinking-strip-utility

Conversation

@daveCode-dot
Copy link
Copy Markdown

Context

Multi-turn conversations with extended thinking accumulate ThinkingBlock entries 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:

400 Invalid `signature` in `thinking` block

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) — accepts Iterable[MessageParam], returns list[MessageParam] with every type: "thinking" and type: "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) and BaseModel instances returned by the SDK. Idempotent; does not mutate the input list.

src/anthropic/lib/__init__.py — exports strip_thinking_blocks alongside files_from_dir so callers can reach it via from anthropic.lib import strip_thinking_blocks.

Intended usage pattern:

try:
    response = client.messages.create(model=model, messages=history, ...)
except anthropic.BadRequestError as exc:
    if "Invalid `signature` in `thinking` block" in str(exc):
        history = strip_thinking_blocks(history)
        response = client.messages.create(model=model, messages=history, ...)
    else:
        raise

Why this approach

The fix lives in lib/ as a pure utility, symmetric with the existing files_from_dir helper: 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.py covering:

  • String-content messages pass through unchanged
  • thinking blocks removed from assistant messages
  • redacted_thinking blocks removed
  • Mixed content (thinking + text + tool_use): only thinking types removed
  • User messages without thinking: unchanged
  • Non-thinking blocks preserved verbatim
  • All-thinking content produces empty list (edge case)
  • Empty messages list
  • Multi-message conversations processed independently
  • BaseModel instances (_FakeThinkingBlock, _FakeRedactedBlock): stripped via getattr
  • Does not mutate the original input list
  • Role and other message keys preserved
  • Idempotent (apply twice → same result)

All 14 tests pass (uv run pytest tests/lib/test_thinking.py -v).

Note: tests/api_resources/beta/test_webhooks.py fails with ModuleNotFoundError: No module named 'standardwebhooks' on the upstream main branch before any changes in this PR. Unrelated to this fix.

Adjacent gaps

  • The BadRequestError message for expired thinking signatures does not suggest strip_thinking_blocks as 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_blocks is not yet re-exported from the top-level anthropic namespace (from anthropic import strip_thinking_blocks). If maintainers prefer it to be a first-class export, that would be a one-line addition to src/anthropic/__init__.py.

… 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.
@daveCode-dot daveCode-dot requested a review from a team as a code owner May 27, 2026 12:12
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