From f0a12b549c887dd6e140d85d5028caa2a4c62383 Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Wed, 15 Apr 2026 14:27:46 -0500 Subject: [PATCH] fix: msg tool issue --- packages/opencode/src/provider/transform.ts | 16 ++++- .../opencode/test/provider/transform.test.ts | 66 +++++++++++++++++++ packages/opencode/test/session/llm.test.ts | 6 +- 3 files changed, 84 insertions(+), 4 deletions(-) diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts index bab056dae7..3e2b61e039 100644 --- a/packages/opencode/src/provider/transform.ts +++ b/packages/opencode/src/provider/transform.ts @@ -75,7 +75,7 @@ export namespace ProviderTransform { if (model.api.id.includes("claude")) { const scrub = (id: string) => id.replace(/[^a-zA-Z0-9_-]/g, "_") - return msgs.map((msg) => { + msgs = msgs.map((msg) => { if (msg.role === "assistant" && Array.isArray(msg.content)) { return { ...msg, @@ -101,6 +101,20 @@ export namespace ProviderTransform { return msg }) } + if (model.api.npm === "@ai-sdk/anthropic") { + msgs = msgs.flatMap((msg) => { + if (msg.role !== "assistant" || !Array.isArray(msg.content)) return [msg] + + const parts = msg.content + const first = parts.findIndex((part) => part.type === "tool-call") + if (first === -1) return [msg] + if (!parts.slice(first).some((part) => part.type !== "tool-call")) return [msg] + return [ + { ...msg, content: parts.filter((part) => part.type !== "tool-call") }, + { ...msg, content: parts.filter((part) => part.type === "tool-call") }, + ] + }) + } if ( model.providerID === "mistral" || model.api.id.toLowerCase().includes("mistral") || diff --git a/packages/opencode/test/provider/transform.test.ts b/packages/opencode/test/provider/transform.test.ts index 1b750d1b93..1d99b06af3 100644 --- a/packages/opencode/test/provider/transform.test.ts +++ b/packages/opencode/test/provider/transform.test.ts @@ -1271,6 +1271,72 @@ describe("ProviderTransform.message - anthropic empty content filtering", () => expect(result[0].content).toBe("") expect(result[1].content).toHaveLength(1) }) + + test("splits anthropic assistant messages when text trails tool calls", () => { + const msgs = [ + { + role: "user", + content: [{ type: "text", text: "Check my home directory for PDFs" }], + }, + { + role: "assistant", + content: [ + { type: "tool-call", toolCallId: "toolu_1", toolName: "read", input: { filePath: "/root" } }, + { type: "tool-call", toolCallId: "toolu_2", toolName: "glob", input: { pattern: "**/*.pdf" } }, + { type: "text", text: "I checked your home directory and looked for PDF files." }, + ], + }, + { + role: "tool", + content: [ + { type: "tool-result", toolCallId: "toolu_1", toolName: "read", output: { type: "text", value: "ok" } }, + { + type: "tool-result", + toolCallId: "toolu_2", + toolName: "glob", + output: { type: "text", value: "No files found" }, + }, + ], + }, + ] as any[] + + const result = ProviderTransform.message(msgs, anthropicModel, {}) as any[] + + expect(result).toHaveLength(4) + expect(result[1]).toMatchObject({ + role: "assistant", + content: [{ type: "text", text: "I checked your home directory and looked for PDF files." }], + }) + expect(result[2]).toMatchObject({ + role: "assistant", + content: [ + { type: "tool-call", toolCallId: "toolu_1", toolName: "read", input: { filePath: "/root" } }, + { type: "tool-call", toolCallId: "toolu_2", toolName: "glob", input: { pattern: "**/*.pdf" } }, + ], + }) + }) + + test("leaves valid anthropic assistant tool ordering unchanged", () => { + const msgs = [ + { + role: "assistant", + content: [ + { type: "text", text: "I checked your home directory and looked for PDF files." }, + { type: "tool-call", toolCallId: "toolu_1", toolName: "read", input: { filePath: "/root" } }, + { type: "tool-call", toolCallId: "toolu_2", toolName: "glob", input: { pattern: "**/*.pdf" } }, + ], + }, + ] as any[] + + const result = ProviderTransform.message(msgs, anthropicModel, {}) as any[] + + expect(result).toHaveLength(1) + expect(result[0].content).toMatchObject([ + { type: "text", text: "I checked your home directory and looked for PDF files." }, + { type: "tool-call", toolCallId: "toolu_1", toolName: "read", input: { filePath: "/root" } }, + { type: "tool-call", toolCallId: "toolu_2", toolName: "glob", input: { pattern: "**/*.pdf" } }, + ]) + }) }) describe("ProviderTransform.message - strip openai metadata when store=false", () => { diff --git a/packages/opencode/test/session/llm.test.ts b/packages/opencode/test/session/llm.test.ts index 8719e348d7..a7fde90f01 100644 --- a/packages/opencode/test/session/llm.test.ts +++ b/packages/opencode/test/session/llm.test.ts @@ -1131,9 +1131,6 @@ describe("session.llm.stream", () => { { type: "text", text: "I checked your home directory and looked for PDF files.", - cache_control: { - type: "ephemeral", - }, }, { type: "tool_use", @@ -1146,6 +1143,9 @@ describe("session.llm.stream", () => { id: "toolu_01APxrADs7VozN8uWzw9WwHr", name: "glob", input: { pattern: "**/*.pdf", path: "/root" }, + cache_control: { + type: "ephemeral", + }, }, ], },