fix: preserve Anthropic thinking block signatures when switching Claude models#25003
fix: preserve Anthropic thinking block signatures when switching Claude models#25003kunalshaw79 wants to merge 1 commit intoanomalyco:devfrom
Conversation
…de models
Bug: Switching from one Claude model to another (e.g. Sonnet → Opus, or
any two distinct claude-* models) caused the session to drop the
providerMetadata from reasoning/thinking parts. The Anthropic API requires
the 'signature' field on every thinking block echoed back in subsequent turns;
without it the request is rejected with:
messages.N.content.0.thinking.signature: Field required
Root cause (message-v2.ts line 840):
const differentModel =
`${model.providerID}/${model.id}` !== `${msg.info.providerID}/${msg.info.modelID}`
This is true whenever the model ID changes. Line 963 then drops
providerMetadata for reasoning blocks when differentModel is true:
...(differentModel ? {} : { providerMetadata: part.metadata })
The signature is a cryptographic token that Anthropic issues and expects
back — it is valid across all Claude models and must be preserved even
when switching between them.
Fix 1 (message-v2.ts):
Introduce keepReasoningMetadata alongside differentModel. It is true
when both the previous and current model are Anthropic-family (providerID
is 'anthropic', 'amazon-bedrock', or modelID contains 'claude'). Use
keepReasoningMetadata for reasoning parts instead of !differentModel so
the signature survives intra-Anthropic model switches.
Fix 2 (transform.ts — belt and suspenders):
In normalizeMessages for @ai-sdk/anthropic and @ai-sdk/amazon-bedrock,
filter out any reasoning part whose providerMetadata.anthropic.signature
is missing or empty. This handles sessions created before the signature
was stored (pre-fix history) and any edge case where a reasoning block
arrives without a valid signature. A block without a signature cannot be
echoed back safely; dropping it is better than crashing the session.
Reproduction:
1. Start a session with a Claude model that has reasoning (e.g. Opus).
2. Switch the session to a different Claude model.
3. Send a new message.
Before fix: 'thinking.signature: Field required'.
After fix: conversation continues normally.
|
Thanks for your contribution! This PR doesn't have a linked issue. All PRs must reference an existing issue. Please:
See CONTRIBUTING.md for details. |
|
The following comment was made by an LLM, it may be inaccurate: Based on my search, I found one highly related PR that appears to address a similar issue: Related PR:
This PR is directly related as it also addresses preserving reasoning/thinking block metadata when switching between models. It may be worth reviewing to ensure there's no overlap or to understand the previous approach to this issue. There are also several other related PRs that deal with thinking block preservation and reasoning metadata handling (e.g., #14393, #12131), but they appear to focus on different aspects of the problem (like compaction headroom or signature validation) rather than the model-switching scenario. |
|
Thanks for updating your PR! It now meets our contributing guidelines. 👍 |
|
We are going to do a more durable and different fix, thanks for trying tho. |
Issue for this PR
Closes #25005
Also related to #23104 which has a similar fix — see note below.
Type of change
What does this PR do?
message-v2.tscomputesdifferentModel = truewhenever the current model ID differs from the one that produced a historical message. For reasoning/thinking parts this is wrong: it stripsproviderMetadata, which is where the Anthropic SDK stores the cryptographicsignaturefield required on every subsequent turn.Without the signature, Anthropic rejects the request:
Changes:
message-v2.ts: IntroducekeepReasoningMetadata— true when both old and new model are Anthropic-family (providerID isanthropic,amazon-bedrock, or modelID containsclaude). Use this for reasoning parts instead of!differentModel, so signatures survive intra-Anthropic model switches. Non-Anthropic providers still have metadata dropped as before.transform.ts: Add a safety filter innormalizeMessagesfor Anthropic-family providers that strips reasoning blocks with a missing/empty signature before the API call. This handles old sessions created before signature capture and any edge case where metadata is unavailable. Dropping such a block is safer than crashing the entire conversation.Note on #23104: That PR uses a simpler one-line fix (unconditionally pass
providerMetadatafor all reasoning parts regardless of model). This PR takes a slightly more conservative approach (preserve metadata only for Anthropic-family switches) and adds the transform.ts safety filter. Both fix the same root cause. Happy to close this in favour of #23104 if maintainers prefer the simpler approach.How did you verify your code works?
bun test test/session/message-v2.test.ts— 27/27 passbun typecheck— no errorsChecklist