From 64b7f9b8bfecc50fabe57da4559ac58ca39aa1a8 Mon Sep 17 00:00:00 2001 From: Willow Lopez <100782273+Oxygen56@users.noreply.github.com> Date: Wed, 24 Jun 2026 19:13:11 +0800 Subject: [PATCH 1/2] fix: add final empty-content guard in message() pipeline Adds a provider-agnostic safety net at the end of the message() function that filters out messages with empty string content or empty array content before they reach any provider API. Previously only Anthropic and Bedrock had local guards; providers like DeepSeek would receive messages with empty content and fail with "messages.N: all messages must have non-empty content". Fixes #23260 --- packages/opencode/src/provider/transform.ts | 23 +++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts index e459380faae4..51a2316d133b 100644 --- a/packages/opencode/src/provider/transform.ts +++ b/packages/opencode/src/provider/transform.ts @@ -473,6 +473,29 @@ export function message(msgs: ModelMessage[], model: Provider.Model, options: Re }) } + // Final safety net: filter out messages with empty content from any provider. + // Individual transforms above each have their own (or no) guard; this single + // pass catches every current and future case that could otherwise cause + // provider API errors — including DeepSeek (messages.N: all messages must + // have non-empty content), Bedrock (ValidationException), and others. + 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) => { + if (part.type === "text") return part.text !== "" + if (part.type === "reasoning") return part.text.trim().length > 0 + return true + }) + if (filtered.length === 0) return undefined + return { ...msg, content: filtered } + }) + .filter((msg): msg is ModelMessage => msg !== undefined) + + return msgs } From 54fab33ea77b899297dbe4cc82d52333abdbf4e9 Mon Sep 17 00:00:00 2001 From: Willow Lopez <100782273+Oxygen56@users.noreply.github.com> Date: Wed, 24 Jun 2026 19:14:09 +0800 Subject: [PATCH 2/2] test: add empty-content guard tests for message() pipeline Covers empty string content, empty array content, all-empty text parts, mixed valid/invalid messages, and provider-agnostic behavior. Ref #23260 --- .../opencode/test/provider/transform.test.ts | 129 ++++++++++++++++++ 1 file changed, 129 insertions(+) diff --git a/packages/opencode/test/provider/transform.test.ts b/packages/opencode/test/provider/transform.test.ts index 5fa530155d6e..bb623ceb2ed8 100644 --- a/packages/opencode/test/provider/transform.test.ts +++ b/packages/opencode/test/provider/transform.test.ts @@ -4502,3 +4502,132 @@ describe("ProviderTransform.providerOptions - ai-gateway-provider", () => { expect(result).toEqual({ openaiCompatible: { reasoningEffort: "high" } }) }) }) + + +describe("ProviderTransform.message - empty content filtering", () => { + const deepseekModel = { + id: ModelV2.ID.make("deepseek/deepseek-chat"), + providerID: ProviderV2.ID.make("deepseek"), + api: { + id: "deepseek-chat", + url: "https://api.deepseek.com", + npm: "@ai-sdk/openai-compatible", + }, + name: "DeepSeek Chat", + 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: "2023-04-01", + } + + test("filters out messages with empty string content", () => { + const msgs = [ + { role: "user", content: "" }, + { role: "user", content: "valid message" }, + { role: "assistant", content: "" }, + ] as any[] + const result = ProviderTransform.message(msgs, deepseekModel, {}) + expect(result).toHaveLength(1) + expect(result[0].content).toBe("valid message") + }) + + test("filters out messages with empty array content", () => { + const msgs = [ + { role: "assistant", content: [] }, + { role: "user", content: "valid" }, + ] as any[] + const result = ProviderTransform.message(msgs, deepseekModel, {}) + expect(result).toHaveLength(1) + expect(result[0].content).toBe("valid") + }) + + test("filters out messages where all text parts are empty", () => { + const msgs = [ + { + role: "assistant", + content: [ + { type: "text", text: "" }, + { type: "reasoning", text: "" }, + ], + }, + { role: "user", content: "valid" }, + ] as any[] + const result = ProviderTransform.message(msgs, deepseekModel, {}) + expect(result).toHaveLength(1) + expect(result[0].content).toBe("valid") + }) + + test("preserves non-empty messages in mixed context", () => { + const msgs = [ + { role: "system", content: "system prompt" }, + { role: "user", content: "hello" }, + { role: "assistant", content: "" }, + { role: "user", content: "follow up" }, + { + role: "assistant", + content: [{ type: "text", text: "reply" }], + }, + { + role: "assistant", + content: [ + { type: "text", text: "" }, + { type: "reasoning", text: " " }, + ], + }, + ] as any[] + const result = ProviderTransform.message(msgs, deepseekModel, {}) as any[] + expect(result).toHaveLength(4) + expect(result[0].content).toBe("system prompt") + expect(result[1].content).toBe("hello") + expect(result[2].content).toBe("follow up") + expect(result[3].content[0].text).toBe("reply") + }) + + test("applies empty-content guard regardless of provider", () => { + // The guard is provider-agnostic — verify it works for a non-DeepSeek model too + const openaiModel = { + id: ModelV2.ID.make("openai/gpt-4"), + providerID: ProviderV2.ID.make("openai"), + api: { id: "gpt-4", url: "https://api.openai.com", npm: "@ai-sdk/openai" }, + name: "GPT-4", + 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: 1, output: 1, cache: { read: 0, write: 0 } }, + limit: { context: 128000, output: 4096 }, + status: "active", + options: {}, + headers: {}, + release_date: "2023-03-14", + } + + const msgs = [ + { role: "user", content: "" }, + { role: "assistant", content: [] }, + { role: "user", content: "only valid message" }, + ] as any[] + const result = ProviderTransform.message(msgs, openaiModel, {}) + expect(result).toHaveLength(1) + expect(result[0].content).toBe("only valid message") + }) +}) +