diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts index e564c54a1e80..28a9a61e6c3a 100644 --- a/packages/opencode/src/provider/transform.ts +++ b/packages/opencode/src/provider/transform.ts @@ -47,7 +47,9 @@ export namespace ProviderTransform { options: Record, ): ModelMessage[] { // Anthropic rejects messages with empty content - filter out empty string messages - // and remove empty text/reasoning parts from array content + // and remove empty text/reasoning parts from array content. + // NOTE: Redacted thinking blocks (with providerMetadata) must be PRESERVED even if empty - + // the Anthropic API requires all thinking blocks to be replayed exactly as received. if (model.api.npm === "@ai-sdk/anthropic") { msgs = msgs .map((msg) => { @@ -57,7 +59,11 @@ export namespace ProviderTransform { } if (!Array.isArray(msg.content)) return msg const filtered = msg.content.filter((part) => { - if (part.type === "text" || part.type === "reasoning") { + if (part.type === "text") return part.text !== "" + if (part.type === "reasoning") { + // Preserve redacted_thinking blocks (they have providerMetadata but may have empty text) + if ((part as any).providerMetadata?.anthropic) return true + // Filter out regular empty reasoning blocks return part.text !== "" } return true @@ -66,6 +72,16 @@ export namespace ProviderTransform { return { ...msg, content: filtered } }) .filter((msg): msg is ModelMessage => msg !== undefined && msg.content !== "") + + // Reorder content blocks: reasoning blocks must come first for Anthropic + msgs = msgs.map((msg) => { + if (msg.role === "assistant" && Array.isArray(msg.content)) { + const reasoning = msg.content.filter((part) => part.type === "reasoning") + const other = msg.content.filter((part) => part.type !== "reasoning") + return { ...msg, content: [...reasoning, ...other] } + } + return msg + }) } if (model.api.id.includes("claude")) { diff --git a/packages/opencode/src/session/message-v2.ts b/packages/opencode/src/session/message-v2.ts index b6043b0325d2..830cfff93e96 100644 --- a/packages/opencode/src/session/message-v2.ts +++ b/packages/opencode/src/session/message-v2.ts @@ -617,6 +617,10 @@ export namespace MessageV2 { }) } if (part.type === "reasoning") { + // Preserve ALL reasoning parts including redacted_thinking blocks. + // The Anthropic API requires previous assistant messages to be replayed + // exactly as received, including redacted_thinking blocks in their + // original positions. Filtering them causes signature validation errors. assistantMessage.parts.push({ type: "reasoning", text: part.text, diff --git a/packages/opencode/src/session/processor.ts b/packages/opencode/src/session/processor.ts index b5289e903a16..beb7cd5386c3 100644 --- a/packages/opencode/src/session/processor.ts +++ b/packages/opencode/src/session/processor.ts @@ -88,7 +88,10 @@ export namespace SessionProcessor { case "reasoning-end": if (value.id in reasoningMap) { const part = reasoningMap[value.id] - part.text = part.text.trimEnd() + // Do NOT trimEnd() thinking text - the signature is computed on the + // exact text including trailing whitespace. Modifying it invalidates + // the signature, causing "Invalid data in redacted_thinking block" + // errors when the thinking is replayed in subsequent API calls. part.time = { ...part.time,