Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 18 additions & 2 deletions packages/opencode/src/provider/transform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,9 @@ export namespace ProviderTransform {
options: Record<string, unknown>,
): 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) => {
Expand All @@ -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
Expand All @@ -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")) {
Expand Down
4 changes: 4 additions & 0 deletions packages/opencode/src/session/message-v2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
5 changes: 4 additions & 1 deletion packages/opencode/src/session/processor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Loading