summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAiden Cline <[email protected]>2026-01-05 01:18:46 -0600
committerAiden Cline <[email protected]>2026-01-05 01:40:15 -0600
commitc285304acf50ab9465a2e23cbf6efc1e5b3a3cf4 (patch)
treeb0aa4673b74ef89f883523568b47e932c16e1644
parent4d187af9d20b04e4fbd77ad04eaaea241b8e25bf (diff)
downloadopencode-c285304acf50ab9465a2e23cbf6efc1e5b3a3cf4.tar.gz
opencode-c285304acf50ab9465a2e23cbf6efc1e5b3a3cf4.zip
fix: for anthropic compat ensure empty msgs and empty reasoning is filtered out
-rw-r--r--packages/opencode/src/provider/transform.ts19
-rw-r--r--packages/opencode/test/provider/transform.test.ts175
2 files changed, 194 insertions, 0 deletions
diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts
index 48011c81a..ae236285c 100644
--- a/packages/opencode/src/provider/transform.ts
+++ b/packages/opencode/src/provider/transform.ts
@@ -17,6 +17,25 @@ function mimeToModality(mime: string): Modality | undefined {
export namespace ProviderTransform {
function normalizeMessages(msgs: ModelMessage[], model: Provider.Model): ModelMessage[] {
+ // Anthropic rejects messages with empty content - filter out empty string messages
+ // and remove empty text/reasoning parts from array content
+ if (model.api.npm === "@ai-sdk/anthropic") {
+ msgs = msgs
+ .map((msg) => {
+ if (typeof msg.content === "string") return msg
+ if (!Array.isArray(msg.content)) return msg
+ const filtered = msg.content.filter((part) => {
+ if (part.type === "text" || part.type === "reasoning") {
+ return part.text !== ""
+ }
+ return true
+ })
+ if (filtered.length === 0) return undefined
+ return { ...msg, content: filtered }
+ })
+ .filter((msg): msg is ModelMessage => msg !== undefined && msg.content !== "")
+ }
+
if (model.api.id.includes("claude")) {
return msgs.map((msg) => {
if ((msg.role === "assistant" || msg.role === "tool") && Array.isArray(msg.content)) {
diff --git a/packages/opencode/test/provider/transform.test.ts b/packages/opencode/test/provider/transform.test.ts
index 287015c4d..32b1ecb24 100644
--- a/packages/opencode/test/provider/transform.test.ts
+++ b/packages/opencode/test/provider/transform.test.ts
@@ -410,6 +410,181 @@ describe("ProviderTransform.message - empty image handling", () => {
})
})
+describe("ProviderTransform.message - anthropic empty content filtering", () => {
+ const anthropicModel = {
+ id: "anthropic/claude-3-5-sonnet",
+ providerID: "anthropic",
+ api: {
+ id: "claude-3-5-sonnet-20241022",
+ url: "https://api.anthropic.com",
+ npm: "@ai-sdk/anthropic",
+ },
+ name: "Claude 3.5 Sonnet",
+ capabilities: {
+ temperature: true,
+ reasoning: false,
+ attachment: true,
+ toolcall: true,
+ input: { text: true, audio: false, image: true, video: false, pdf: true },
+ output: { text: true, audio: false, image: false, video: false, pdf: false },
+ interleaved: false,
+ },
+ cost: {
+ input: 0.003,
+ output: 0.015,
+ cache: { read: 0.0003, write: 0.00375 },
+ },
+ limit: {
+ context: 200000,
+ output: 8192,
+ },
+ status: "active",
+ options: {},
+ headers: {},
+ } as any
+
+ test("filters out messages with empty string content", () => {
+ const msgs = [
+ { role: "user", content: "Hello" },
+ { role: "assistant", content: "" },
+ { role: "user", content: "World" },
+ ] as any[]
+
+ const result = ProviderTransform.message(msgs, anthropicModel)
+
+ expect(result).toHaveLength(2)
+ expect(result[0].content).toBe("Hello")
+ expect(result[1].content).toBe("World")
+ })
+
+ test("filters out empty text parts from array content", () => {
+ const msgs = [
+ {
+ role: "assistant",
+ content: [
+ { type: "text", text: "" },
+ { type: "text", text: "Hello" },
+ { type: "text", text: "" },
+ ],
+ },
+ ] as any[]
+
+ const result = ProviderTransform.message(msgs, anthropicModel)
+
+ expect(result).toHaveLength(1)
+ expect(result[0].content).toHaveLength(1)
+ expect(result[0].content[0]).toEqual({ type: "text", text: "Hello" })
+ })
+
+ test("filters out empty reasoning parts from array content", () => {
+ const msgs = [
+ {
+ role: "assistant",
+ content: [
+ { type: "reasoning", text: "" },
+ { type: "text", text: "Answer" },
+ { type: "reasoning", text: "" },
+ ],
+ },
+ ] as any[]
+
+ const result = ProviderTransform.message(msgs, anthropicModel)
+
+ expect(result).toHaveLength(1)
+ expect(result[0].content).toHaveLength(1)
+ expect(result[0].content[0]).toEqual({ type: "text", text: "Answer" })
+ })
+
+ test("removes entire message when all parts are empty", () => {
+ const msgs = [
+ { role: "user", content: "Hello" },
+ {
+ role: "assistant",
+ content: [
+ { type: "text", text: "" },
+ { type: "reasoning", text: "" },
+ ],
+ },
+ { role: "user", content: "World" },
+ ] as any[]
+
+ const result = ProviderTransform.message(msgs, anthropicModel)
+
+ expect(result).toHaveLength(2)
+ expect(result[0].content).toBe("Hello")
+ expect(result[1].content).toBe("World")
+ })
+
+ test("keeps non-text/reasoning parts even if text parts are empty", () => {
+ const msgs = [
+ {
+ role: "assistant",
+ content: [
+ { type: "text", text: "" },
+ { type: "tool-call", toolCallId: "123", toolName: "bash", input: { command: "ls" } },
+ ],
+ },
+ ] as any[]
+
+ const result = ProviderTransform.message(msgs, anthropicModel)
+
+ expect(result).toHaveLength(1)
+ expect(result[0].content).toHaveLength(1)
+ expect(result[0].content[0]).toEqual({
+ type: "tool-call",
+ toolCallId: "123",
+ toolName: "bash",
+ input: { command: "ls" },
+ })
+ })
+
+ test("keeps messages with valid text alongside empty parts", () => {
+ const msgs = [
+ {
+ role: "assistant",
+ content: [
+ { type: "reasoning", text: "Thinking..." },
+ { type: "text", text: "" },
+ { type: "text", text: "Result" },
+ ],
+ },
+ ] as any[]
+
+ const result = ProviderTransform.message(msgs, anthropicModel)
+
+ expect(result).toHaveLength(1)
+ expect(result[0].content).toHaveLength(2)
+ expect(result[0].content[0]).toEqual({ type: "reasoning", text: "Thinking..." })
+ expect(result[0].content[1]).toEqual({ type: "text", text: "Result" })
+ })
+
+ test("does not filter for non-anthropic providers", () => {
+ const openaiModel = {
+ ...anthropicModel,
+ providerID: "openai",
+ api: {
+ id: "gpt-4",
+ url: "https://api.openai.com",
+ npm: "@ai-sdk/openai",
+ },
+ }
+
+ const msgs = [
+ { role: "assistant", content: "" },
+ {
+ role: "assistant",
+ content: [{ type: "text", text: "" }],
+ },
+ ] as any[]
+
+ const result = ProviderTransform.message(msgs, openaiModel)
+
+ expect(result).toHaveLength(2)
+ expect(result[0].content).toBe("")
+ expect(result[1].content).toHaveLength(1)
+ })
+})
+
describe("ProviderTransform.variants", () => {
const createMockModel = (overrides: Partial<any> = {}): any => ({
id: "test/test-model",