diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts index 4566fc1de2b..95b9c0ca3b0 100644 --- a/packages/opencode/src/provider/transform.ts +++ b/packages/opencode/src/provider/transform.ts @@ -48,27 +48,30 @@ export namespace ProviderTransform { }) } - // 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") { - if (msg.content === "") return undefined - return msg - } - if (!Array.isArray(msg.content)) return msg - const filtered = msg.content.filter((part) => { + // Provider APIs reject messages with empty or whitespace-only content + // Filter out empty/whitespace string messages and remove empty text/reasoning parts + msgs = msgs + .map((msg) => { + if (typeof msg.content === "string") { + const trimmed = msg.content.trim() + if (trimmed === "") return undefined + return { ...msg, content: trimmed } + } + if (!Array.isArray(msg.content)) return msg + const filtered = msg.content + .map((part) => { if (part.type === "text" || part.type === "reasoning") { - return part.text !== "" + const trimmed = part.text.trim() + if (trimmed === "") return undefined + return { ...part, text: trimmed } } - return true + return part }) - if (filtered.length === 0) return undefined - return { ...msg, content: filtered } - }) - .filter((msg): msg is ModelMessage => msg !== undefined && msg.content !== "") - } + .filter((part): part is NonNullable => part !== undefined) + 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) => { diff --git a/packages/opencode/test/provider/transform.test.ts b/packages/opencode/test/provider/transform.test.ts index 33047b5bcb4..d80eaae67cc 100644 --- a/packages/opencode/test/provider/transform.test.ts +++ b/packages/opencode/test/provider/transform.test.ts @@ -448,7 +448,7 @@ describe("ProviderTransform.message - empty image handling", () => { }) }) -describe("ProviderTransform.message - anthropic empty content filtering", () => { +describe("ProviderTransform.message - empty and whitespace content filtering", () => { const anthropicModel = { id: "anthropic/claude-3-5-sonnet", providerID: "anthropic", @@ -596,7 +596,7 @@ describe("ProviderTransform.message - anthropic empty content filtering", () => expect(result[0].content[1]).toEqual({ type: "text", text: "Result" }) }) - test("does not filter for non-anthropic providers", () => { + test("filters empty content for all providers including OpenAI", () => { const openaiModel = { ...anthropicModel, providerID: "openai", @@ -608,18 +608,70 @@ describe("ProviderTransform.message - anthropic empty content filtering", () => } const msgs = [ + { role: "user", content: "Hello" }, { role: "assistant", content: "" }, { role: "assistant", content: [{ type: "text", text: "" }], }, + { role: "user", content: "World" }, ] as any[] const result = ProviderTransform.message(msgs, openaiModel, {}) expect(result).toHaveLength(2) - expect(result[0].content).toBe("") - expect(result[1].content).toHaveLength(1) + expect(result[0].content).toBe("Hello") + expect(result[1].content).toBe("World") + }) + + test("filters whitespace-only string content", () => { + const msgs = [ + { role: "user", content: "Hello" }, + { role: "assistant", content: " " }, + { role: "user", content: "\t\n" }, + { 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 whitespace-only text parts from array content", () => { + const msgs = [ + { + role: "assistant", + content: [ + { type: "text", text: " " }, + { type: "text", text: "Hello" }, + { type: "text", text: "\t\n" }, + ], + }, + ] 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("trims whitespace from text content", () => { + const msgs = [ + { role: "user", content: " Hello World " }, + { + role: "assistant", + content: [{ type: "text", text: " Response " }], + }, + ] as any[] + + const result = ProviderTransform.message(msgs, anthropicModel, {}) + + expect(result).toHaveLength(2) + expect(result[0].content).toBe("Hello World") + expect(result[1].content[0]).toEqual({ type: "text", text: "Response" }) }) })