diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index 022ec3167956..eeb99dd8c325 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -1085,6 +1085,29 @@ export namespace Provider { opts.signal = combined } + // For anthropic protocol non-Claude models with interleaved reasoning, + // convert thinking blocks to reasoning_content on assistant messages + if ( + typeof model.capabilities.interleaved === "object" && + model.capabilities.interleaved.field && + (model.api.npm === "@ai-sdk/anthropic" || model.api.npm === "@ai-sdk/google-vertex/anthropic") && + opts.body && + opts.method === "POST" + ) { + const body = JSON.parse(opts.body as string) + if (Array.isArray(body.messages)) { + const field = model.capabilities.interleaved.field + for (const msg of body.messages) { + if (msg.role !== "assistant" || !Array.isArray(msg.content)) continue + const thinking = msg.content.filter((b: any) => b.type === "thinking") + const rest = msg.content.filter((b: any) => b.type !== "thinking") + msg[field] = thinking.map((b: any) => b.thinking).join("") + msg.content = rest + } + opts.body = JSON.stringify(body) + } + } + // Strip openai itemId metadata following what codex does // Codex uses #[serde(skip_serializing)] on id fields for all item types: // Message, Reasoning, FunctionCall, LocalShellCall, CustomToolCall, WebSearchCall diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts index b659799c1b6f..bd60250df159 100644 --- a/packages/opencode/src/provider/transform.ts +++ b/packages/opencode/src/provider/transform.ts @@ -135,36 +135,45 @@ export namespace ProviderTransform { if (typeof model.capabilities.interleaved === "object" && model.capabilities.interleaved.field) { const field = model.capabilities.interleaved.field + const isAnthropic = model.api.npm === "@ai-sdk/anthropic" || model.api.npm === "@ai-sdk/google-vertex/anthropic" return msgs.map((msg) => { - if (msg.role === "assistant" && Array.isArray(msg.content)) { - const reasoningParts = msg.content.filter((part: any) => part.type === "reasoning") - const reasoningText = reasoningParts.map((part: any) => part.text).join("") + if (msg.role !== "assistant" || !Array.isArray(msg.content)) return msg - // Filter out reasoning parts from content - const filteredContent = msg.content.filter((part: any) => part.type !== "reasoning") + const reasoningText = msg.content + .filter((part: any) => part.type === "reasoning") + .map((part: any) => part.text) + .join("") + if (!reasoningText) return msg - // Include reasoning_content | reasoning_details directly on the message for all assistant messages - if (reasoningText) { - return { - ...msg, - content: filteredContent, - providerOptions: { - ...msg.providerOptions, - openaiCompatible: { - ...(msg.providerOptions as any)?.openaiCompatible, - [field]: reasoningText, - }, - }, - } - } + const filtered = msg.content.filter((part: any) => part.type !== "reasoning") + // For anthropic protocol non-Claude models, inject a fake signature so the SDK + // emits thinking blocks. The fetch wrapper converts them to reasoning_content. + if (isAnthropic) { return { ...msg, - content: filteredContent, + content: [ + { + type: "reasoning" as const, + text: reasoningText, + providerOptions: { anthropic: { signature: "interleaved-placeholder" } }, + }, + ...filtered, + ], } } - return msg + return { + ...msg, + content: filtered, + providerOptions: { + ...msg.providerOptions, + openaiCompatible: { + ...(msg.providerOptions as any)?.openaiCompatible, + [field]: reasoningText, + }, + }, + } }) }