Skip to content

Commit 993c109

Browse files
committed
fix(provider): reorder reasoning blocks first in assistant messages
The Anthropic API requires thinking/redacted_thinking blocks to appear before other content blocks in assistant messages. Add reordering logic in normalizeMessages() and preserve all reasoning blocks including redacted_thinking in toModelMessages(). Ref #10970
1 parent e8d6d1c commit 993c109

2 files changed

Lines changed: 22 additions & 2 deletions

File tree

packages/opencode/src/provider/transform.ts

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,9 @@ export namespace ProviderTransform {
4747
options: Record<string, unknown>,
4848
): ModelMessage[] {
4949
// Anthropic rejects messages with empty content - filter out empty string messages
50-
// and remove empty text/reasoning parts from array content
50+
// and remove empty text/reasoning parts from array content.
51+
// NOTE: Redacted thinking blocks (with providerMetadata) must be PRESERVED even if empty -
52+
// the Anthropic API requires all thinking blocks to be replayed exactly as received.
5153
if (model.api.npm === "@ai-sdk/anthropic") {
5254
msgs = msgs
5355
.map((msg) => {
@@ -57,7 +59,11 @@ export namespace ProviderTransform {
5759
}
5860
if (!Array.isArray(msg.content)) return msg
5961
const filtered = msg.content.filter((part) => {
60-
if (part.type === "text" || part.type === "reasoning") {
62+
if (part.type === "text") return part.text !== ""
63+
if (part.type === "reasoning") {
64+
// Preserve redacted_thinking blocks (they have providerMetadata but may have empty text)
65+
if ((part as any).providerMetadata?.anthropic) return true
66+
// Filter out regular empty reasoning blocks
6167
return part.text !== ""
6268
}
6369
return true
@@ -66,6 +72,16 @@ export namespace ProviderTransform {
6672
return { ...msg, content: filtered }
6773
})
6874
.filter((msg): msg is ModelMessage => msg !== undefined && msg.content !== "")
75+
76+
// Reorder content blocks: reasoning blocks must come first for Anthropic
77+
msgs = msgs.map((msg) => {
78+
if (msg.role === "assistant" && Array.isArray(msg.content)) {
79+
const reasoning = msg.content.filter((part) => part.type === "reasoning")
80+
const other = msg.content.filter((part) => part.type !== "reasoning")
81+
return { ...msg, content: [...reasoning, ...other] }
82+
}
83+
return msg
84+
})
6985
}
7086

7187
if (model.api.id.includes("claude")) {

packages/opencode/src/session/message-v2.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -617,6 +617,10 @@ export namespace MessageV2 {
617617
})
618618
}
619619
if (part.type === "reasoning") {
620+
// Preserve ALL reasoning parts including redacted_thinking blocks.
621+
// The Anthropic API requires previous assistant messages to be replayed
622+
// exactly as received, including redacted_thinking blocks in their
623+
// original positions. Filtering them causes signature validation errors.
620624
assistantMessage.parts.push({
621625
type: "reasoning",
622626
text: part.text,

0 commit comments

Comments
 (0)