diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts index b659799c1b6..9a4aa965956 100644 --- a/packages/opencode/src/provider/transform.ts +++ b/packages/opencode/src/provider/transform.ts @@ -135,36 +135,30 @@ export namespace ProviderTransform { if (typeof model.capabilities.interleaved === "object" && model.capabilities.interleaved.field) { const field = model.capabilities.interleaved.field + const key = + model.api.npm === "@ai-sdk/anthropic" || model.api.npm === "@ai-sdk/google-vertex/anthropic" + ? "anthropic" + : "openaiCompatible" 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, - }, - }, - } - } - - return { - ...msg, - content: filteredContent, - } + return { + ...msg, + content: msg.content.filter((part: any) => part.type !== "reasoning"), + providerOptions: { + ...msg.providerOptions, + [key]: { + ...(msg.providerOptions as any)?.[key], + [field]: reasoningText, + }, + }, } - - return msg }) } diff --git a/packages/opencode/test/provider/transform.test.ts b/packages/opencode/test/provider/transform.test.ts index 189bdfd32b4..4c13027c1ac 100644 --- a/packages/opencode/test/provider/transform.test.ts +++ b/packages/opencode/test/provider/transform.test.ts @@ -744,6 +744,122 @@ describe("ProviderTransform.message - DeepSeek reasoning content", () => { }) }) +describe("ProviderTransform.message - kimi-k2.5 anthropic interleaved reasoning", () => { + const kimiModel = { + id: "anthropic/kimi-k2.5", + providerID: "anthropic", + api: { + id: "kimi-k2.5", + url: "https://api.anthropic.com", + npm: "@ai-sdk/anthropic", + }, + name: "Kimi K2.5", + capabilities: { + temperature: true, + reasoning: true, + attachment: false, + toolcall: true, + input: { text: true, audio: false, image: false, video: false, pdf: false }, + output: { text: true, audio: false, image: false, video: false, pdf: false }, + interleaved: { field: "reasoning_content" }, + }, + cost: { input: 0.001, output: 0.002, cache: { read: 0.0001, write: 0.0002 } }, + limit: { context: 128000, output: 8192 }, + status: "active", + options: {}, + headers: {}, + release_date: "2025-01-01", + } as any + + test("injects reasoning_content into providerOptions.anthropic", () => { + const msgs = [ + { + role: "assistant", + content: [ + { type: "reasoning", text: "Let me think..." }, + { type: "text", text: "The answer is 42" }, + ], + }, + ] as any[] + + const result = ProviderTransform.message(msgs, kimiModel, {}) + + expect(result).toHaveLength(1) + expect(result[0].providerOptions?.anthropic?.reasoning_content).toBe("Let me think...") + expect(result[0].content).toEqual([{ type: "text", text: "The answer is 42" }]) + }) + + test("does not inject when no reasoning parts exist", () => { + const msgs = [ + { + role: "assistant", + content: [{ type: "text", text: "No reasoning here" }], + }, + ] as any[] + + const result = ProviderTransform.message(msgs, kimiModel, {}) + + expect(result[0].providerOptions?.anthropic?.reasoning_content).toBeUndefined() + expect(result[0].content).toEqual([{ type: "text", text: "No reasoning here" }]) + }) + + test("concatenates multiple reasoning parts", () => { + const msgs = [ + { + role: "assistant", + content: [ + { type: "reasoning", text: "First thought. " }, + { type: "reasoning", text: "Second thought." }, + { type: "text", text: "Result" }, + ], + }, + ] as any[] + + const result = ProviderTransform.message(msgs, kimiModel, {}) + + expect(result[0].providerOptions?.anthropic?.reasoning_content).toBe("First thought. Second thought.") + expect(result[0].content).toEqual([{ type: "text", text: "Result" }]) + }) + + test("works with google-vertex/anthropic SDK", () => { + const vertexModel = { + ...kimiModel, + api: { ...kimiModel.api, npm: "@ai-sdk/google-vertex/anthropic" }, + } + const msgs = [ + { + role: "assistant", + content: [ + { type: "reasoning", text: "Thinking via vertex..." }, + { type: "text", text: "Done" }, + ], + }, + ] as any[] + + const result = ProviderTransform.message(msgs, vertexModel, {}) + + expect(result[0].providerOptions?.anthropic?.reasoning_content).toBe("Thinking via vertex...") + }) + + test("leaves non-assistant messages unchanged", () => { + const msgs = [ + { role: "user", content: "Hello" }, + { + role: "assistant", + content: [ + { type: "reasoning", text: "Thinking..." }, + { type: "text", text: "Hi" }, + ], + }, + ] as any[] + + const result = ProviderTransform.message(msgs, kimiModel, {}) + + expect(result[0].content).toBe("Hello") + expect(result[1].providerOptions?.anthropic?.reasoning_content).toBe("Thinking...") + }) +}) + describe("ProviderTransform.message - empty image handling", () => { const mockModel = { id: "anthropic/claude-3-5-sonnet",