diff --git a/packages/console/app/src/routes/zen/util/provider/anthropic.ts b/packages/console/app/src/routes/zen/util/provider/anthropic.ts index a5f92a29acf..04bfb94fa92 100644 --- a/packages/console/app/src/routes/zen/util/provider/anthropic.ts +++ b/packages/console/app/src/routes/zen/util/provider/anthropic.ts @@ -191,8 +191,8 @@ export function fromAnthropicRequest(body: any): CommonRequest { if (!s) continue if ((s as any).type !== "text") continue if (typeof (s as any).text !== "string") continue - if ((s as any).text.length === 0) continue - msgs.push({ role: "system", content: (s as any).text }) + if ((s as any).text.trim().length === 0) continue + msgs.push({ role: "system", content: (s as any).text.trim() }) } } @@ -221,8 +221,10 @@ export function fromAnthropicRequest(body: any): CommonRequest { const partsOut: any[] = [] for (const p of partsIn) { if (!p || !(p as any).type) continue - if ((p as any).type === "text" && typeof (p as any).text === "string") - partsOut.push({ type: "text", text: (p as any).text }) + if ((p as any).type === "text" && typeof (p as any).text === "string") { + const t = (p as any).text.trim() + if (t.length > 0) partsOut.push({ type: "text", text: t }) + } if ((p as any).type === "image") { const ip = toImg((p as any).source) if (ip) partsOut.push(ip) @@ -247,7 +249,10 @@ export function fromAnthropicRequest(body: any): CommonRequest { const tcs: any[] = [] for (const p of partsIn) { if (!p || !(p as any).type) continue - if ((p as any).type === "text" && typeof (p as any).text === "string") texts.push((p as any).text) + if ((p as any).type === "text" && typeof (p as any).text === "string") { + const t = (p as any).text.trim() + if (t.length > 0) texts.push(t) + } if ((p as any).type === "tool_use") { const name = (p as any).name const id = (p as any).id @@ -324,8 +329,8 @@ export function toAnthropicRequest(body: CommonRequest) { return ccCount <= 4 ? { cache_control: { type: "ephemeral" } } : {} } const system = sysIn - .filter((m: any) => typeof m.content === "string" && m.content.length > 0) - .map((m: any) => ({ type: "text", text: m.content, ...cc() })) + .filter((m: any) => typeof m.content === "string" && m.content.trim().length > 0) + .map((m: any) => ({ type: "text", text: m.content.trim(), ...cc() })) const msgsIn = Array.isArray(body.messages) ? body.messages : [] const msgsOut: any[] = [] @@ -348,16 +353,21 @@ export function toAnthropicRequest(body: CommonRequest) { if ((m as any).role === "user") { if (typeof (m as any).content === "string") { - msgsOut.push({ - role: "user", - content: [{ type: "text", text: (m as any).content, ...cc() }], - }) + const c = (m as any).content.trim() + if (c.length > 0) { + msgsOut.push({ + role: "user", + content: [{ type: "text", text: c, ...cc() }], + }) + } } else if (Array.isArray((m as any).content)) { const parts: any[] = [] for (const p of (m as any).content) { if (!p || !(p as any).type) continue - if ((p as any).type === "text" && typeof (p as any).text === "string") - parts.push({ type: "text", text: (p as any).text, ...cc() }) + if ((p as any).type === "text" && typeof (p as any).text === "string") { + const t = (p as any).text.trim() + if (t.length > 0) parts.push({ type: "text", text: t, ...cc() }) + } if ((p as any).type === "image_url") { const s = toSrc(p) if (s) parts.push({ type: "image", source: s, ...cc() }) @@ -370,8 +380,8 @@ export function toAnthropicRequest(body: CommonRequest) { if ((m as any).role === "assistant") { const out: any = { role: "assistant", content: [] as any[] } - if (typeof (m as any).content === "string" && (m as any).content.length > 0) { - ;(out.content as any[]).push({ type: "text", text: (m as any).content, ...cc() }) + if (typeof (m as any).content === "string" && (m as any).content.trim().length > 0) { + ;(out.content as any[]).push({ type: "text", text: (m as any).content.trim(), ...cc() }) } if (Array.isArray((m as any).tool_calls)) { for (const tc of (m as any).tool_calls) { @@ -554,8 +564,8 @@ export function toAnthropicResponse(resp: CommonResponse) { const content: any[] = [] - if (typeof message.content === "string" && message.content.length > 0) - content.push({ type: "text", text: message.content }) + if (typeof message.content === "string" && message.content.trim().length > 0) + content.push({ type: "text", text: message.content.trim() }) if (Array.isArray(message.tool_calls)) { for (const tc of message.tool_calls) { diff --git a/packages/console/app/src/routes/zen/util/provider/openai-compatible.ts b/packages/console/app/src/routes/zen/util/provider/openai-compatible.ts index 699243d085f..2a94d53e286 100644 --- a/packages/console/app/src/routes/zen/util/provider/openai-compatible.ts +++ b/packages/console/app/src/routes/zen/util/provider/openai-compatible.ts @@ -81,18 +81,22 @@ export function fromOaCompatibleRequest(body: any): CommonRequest { if (!m || !m.role) continue if (m.role === "system") { - if (typeof m.content === "string" && m.content.length > 0) msgsOut.push({ role: "system", content: m.content }) + if (typeof m.content === "string" && m.content.trim().length > 0) + msgsOut.push({ role: "system", content: m.content.trim() }) continue } if (m.role === "user") { - if (typeof m.content === "string") { - msgsOut.push({ role: "user", content: m.content }) + if (typeof m.content === "string" && m.content.trim().length > 0) { + msgsOut.push({ role: "user", content: m.content.trim() }) } else if (Array.isArray(m.content)) { const parts: any[] = [] for (const p of m.content) { if (!p || !p.type) continue - if (p.type === "text" && typeof p.text === "string") parts.push({ type: "text", text: p.text }) + if (p.type === "text" && typeof p.text === "string") { + const t = p.text.trim() + if (t.length > 0) parts.push({ type: "text", text: t }) + } if (p.type === "image_url") parts.push({ type: "image_url", image_url: p.image_url }) } if (parts.length === 1 && parts[0].type === "text") msgsOut.push({ role: "user", content: parts[0].text }) @@ -103,7 +107,7 @@ export function fromOaCompatibleRequest(body: any): CommonRequest { if (m.role === "assistant") { const out: any = { role: "assistant" } - if (typeof m.content === "string") out.content = m.content + if (typeof m.content === "string" && m.content.trim().length > 0) out.content = m.content.trim() if (Array.isArray(m.tool_calls)) out.tool_calls = m.tool_calls msgsOut.push(out) continue @@ -149,20 +153,24 @@ export function toOaCompatibleRequest(body: CommonRequest) { if (!m || !m.role) continue if (m.role === "system") { - if (typeof m.content === "string" && m.content.length > 0) msgsOut.push({ role: "system", content: m.content }) + if (typeof m.content === "string" && m.content.trim().length > 0) + msgsOut.push({ role: "system", content: m.content.trim() }) continue } if (m.role === "user") { - if (typeof m.content === "string") { - msgsOut.push({ role: "user", content: m.content }) + if (typeof m.content === "string" && m.content.trim().length > 0) { + msgsOut.push({ role: "user", content: m.content.trim() }) continue } if (Array.isArray(m.content)) { const parts: any[] = [] for (const p of m.content) { if (!p || !p.type) continue - if (p.type === "text" && typeof p.text === "string") parts.push({ type: "text", text: p.text }) + if (p.type === "text" && typeof p.text === "string") { + const t = p.text.trim() + if (t.length > 0) parts.push({ type: "text", text: t }) + } const ip = toImg(p) if (ip) parts.push(ip) } @@ -174,7 +182,7 @@ export function toOaCompatibleRequest(body: CommonRequest) { if (m.role === "assistant") { const out: any = { role: "assistant" } - if (typeof m.content === "string") out.content = m.content + if (typeof m.content === "string" && m.content.trim().length > 0) out.content = m.content.trim() if (Array.isArray(m.tool_calls)) out.tool_calls = m.tool_calls msgsOut.push(out) continue @@ -224,8 +232,8 @@ export function fromOaCompatibleResponse(resp: any): CommonResponse { const content: any[] = [] - if (typeof message.content === "string" && message.content.length > 0) { - content.push({ type: "text", text: message.content }) + if (typeof message.content === "string" && message.content.trim().length > 0) { + content.push({ type: "text", text: message.content.trim() }) } if (Array.isArray(message.tool_calls)) { diff --git a/packages/console/app/src/routes/zen/util/provider/openai.ts b/packages/console/app/src/routes/zen/util/provider/openai.ts index f4d7699e97c..9b29b729a3d 100644 --- a/packages/console/app/src/routes/zen/util/provider/openai.ts +++ b/packages/console/app/src/routes/zen/util/provider/openai.ts @@ -115,24 +115,25 @@ export function fromOpenaiRequest(body: any): CommonRequest { if ((m as any).role === "system" || (m as any).role === "developer") { const c = (m as any).content - if (typeof c === "string" && c.length > 0) msgs.push({ role: "system", content: c }) + if (typeof c === "string" && c.trim().length > 0) msgs.push({ role: "system", content: c.trim() }) if (Array.isArray(c)) { const t = c.find((p: any) => p && typeof p.text === "string") - if (t && typeof t.text === "string" && t.text.length > 0) msgs.push({ role: "system", content: t.text }) + if (t && typeof t.text === "string" && t.text.trim().length > 0) + msgs.push({ role: "system", content: t.text.trim() }) } continue } if ((m as any).role === "user") { const c = (m as any).content - if (typeof c === "string") { - msgs.push({ role: "user", content: c }) - } else if (Array.isArray(c)) { + if (typeof c === "string" && c.trim().length > 0) msgs.push({ role: "user", content: c.trim() }) + else if (Array.isArray(c)) { const parts: any[] = [] for (const p of c) { if (!p || !(p as any).type) continue - if (((p as any).type === "text" || (p as any).type === "input_text") && typeof (p as any).text === "string") - parts.push({ type: "text", text: (p as any).text }) + if (((p as any).type === "text" || (p as any).type === "input_text") && typeof (p as any).text === "string") { + if ((p as any).text.trim().length > 0) parts.push({ type: "text", text: (p as any).text.trim() }) + } const ip = toImg(p) if (ip) parts.push(ip) if ((p as any).type === "tool_result") { @@ -151,7 +152,7 @@ export function fromOpenaiRequest(body: any): CommonRequest { if ((m as any).role === "assistant") { const c = (m as any).content const out: any = { role: "assistant" } - if (typeof c === "string" && c.length > 0) out.content = c + if (typeof c === "string" && c.trim().length > 0) out.content = c.trim() if (Array.isArray((m as any).tool_calls)) out.tool_calls = (m as any).tool_calls msgs.push(out) continue @@ -206,8 +207,11 @@ export function toOpenaiRequest(body: CommonRequest) { const toPart = (p: any) => { if (!p || typeof p !== "object") return undefined - if ((p as any).type === "text" && typeof (p as any).text === "string") - return { type: "input_text", text: (p as any).text } + if ((p as any).type === "text" && typeof (p as any).text === "string") { + const t = (p as any).text.trim() + if (t.length === 0) return undefined + return { type: "input_text", text: t } + } if ((p as any).type === "image_url" && (p as any).image_url) return { type: "input_image", image_url: (p as any).image_url } const s = (p as any).source @@ -231,15 +235,17 @@ export function toOpenaiRequest(body: CommonRequest) { if ((m as any).role === "system") { const c = (m as any).content - if (typeof c === "string") input.push({ role: "system", content: c }) + if (typeof c === "string" && c.trim().length > 0) input.push({ role: "system", content: c.trim() }) continue } if ((m as any).role === "user") { const c = (m as any).content - if (typeof c === "string") { - input.push({ role: "user", content: [{ type: "input_text", text: c }] }) - } else if (Array.isArray(c)) { + if (typeof c === "string" && c.trim().length > 0) { + input.push({ role: "user", content: [{ type: "input_text", text: c.trim() }] }) + continue + } + if (Array.isArray(c)) { const parts: any[] = [] for (const p of c) { const op = toPart(p) @@ -252,8 +258,8 @@ export function toOpenaiRequest(body: CommonRequest) { if ((m as any).role === "assistant") { const c = (m as any).content - if (typeof c === "string" && c.length > 0) { - input.push({ role: "assistant", content: [{ type: "output_text", text: c }] }) + if (typeof c === "string" && c.trim().length > 0) { + input.push({ role: "assistant", content: [{ type: "output_text", text: c.trim() }] }) } if (Array.isArray((m as any).tool_calls)) { for (const tc of (m as any).tool_calls) { @@ -417,13 +423,13 @@ export function toOpenaiResponse(resp: CommonResponse) { const outputItems: any[] = [] - if (typeof msg.content === "string" && msg.content.length > 0) { + if (typeof msg.content === "string" && msg.content.trim().length > 0) { outputItems.push({ id: `msg_${Math.random().toString(36).slice(2)}`, type: "message", status: "completed", role: "assistant", - content: [{ type: "output_text", text: msg.content, annotations: [], logprobs: [] }], + content: [{ type: "output_text", text: msg.content.trim(), annotations: [], logprobs: [] }], }) } diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts index f6b7ec8cbcc..53ed69651ee 100644 --- a/packages/opencode/src/provider/transform.ts +++ b/packages/opencode/src/provider/transform.ts @@ -49,20 +49,33 @@ export namespace ProviderTransform { msgs = msgs .map((msg) => { if (typeof msg.content === "string") { - if (msg.content === "") return undefined - return msg + if (msg.content.trim().length === 0) return undefined + return { ...msg, content: msg.content.trim() } } 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 - }) + const filtered = msg.content + .filter((part) => { + if (part.type === "text" || part.type === "reasoning") { + if (typeof (part as any).text === "string") return (part as any).text.trim().length > 0 + return false + } + return true + }) + .map((part) => { + if ((part.type === "text" || part.type === "reasoning") && typeof (part as any).text === "string") { + return { ...part, text: (part as any).text.trim() } + } + return part + }) if (filtered.length === 0) return undefined return { ...msg, content: filtered } }) - .filter((msg): msg is ModelMessage => msg !== undefined && msg.content !== "") + .filter((msg): msg is ModelMessage => { + if (msg === undefined) return false + if (typeof msg.content === "string") return msg.content.length > 0 + if (Array.isArray(msg.content)) return msg.content.length > 0 + return true + }) } if (model.api.id.includes("claude")) { diff --git a/packages/opencode/test/provider/transform.test.ts b/packages/opencode/test/provider/transform.test.ts index 2b8f1872f56..e0c68ee01bc 100644 --- a/packages/opencode/test/provider/transform.test.ts +++ b/packages/opencode/test/provider/transform.test.ts @@ -596,6 +596,97 @@ describe("ProviderTransform.message - anthropic empty content filtering", () => expect(result[0].content[1]).toEqual({ type: "text", text: "Result" }) }) + test("filters out messages with whitespace-only string content", () => { + const msgs = [ + { role: "user", content: "Hello" }, + { role: "assistant", content: " " }, + { role: "assistant", 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 out whitespace-only text parts from array content", () => { + const msgs = [ + { + role: "assistant", + content: [ + { type: "text", text: " " }, + { type: "text", text: "Hello" }, + { type: "text", text: "\t\n" }, + { type: "reasoning", text: " " }, + { type: "text", text: "World" }, + ], + }, + ] 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: "text", text: "Hello" }) + expect(result[0].content[1]).toEqual({ type: "text", text: "World" }) + }) + + test("removes entire message when all parts are whitespace-only", () => { + const msgs = [ + { role: "user", content: "Hello" }, + { + role: "assistant", + content: [ + { type: "text", text: " " }, + { type: "reasoning", text: "\t" }, + ], + }, + { 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("trims leading and trailing whitespace from string content", () => { + const msgs = [ + { role: "user", content: " Hello " }, + { role: "assistant", content: " World\t\n" }, + ] 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("trims leading and trailing whitespace from array text content", () => { + const msgs = [ + { + role: "assistant", + content: [ + { type: "text", text: " Hello " }, + { type: "text", text: "\nWorld\t" }, + { type: "reasoning", text: " Thinking " }, + ], + }, + ] as any[] + + const result = ProviderTransform.message(msgs, anthropicModel, {}) + + expect(result).toHaveLength(1) + expect(result[0].content).toHaveLength(3) + expect(result[0].content[0]).toEqual({ type: "text", text: "Hello" }) + expect(result[0].content[1]).toEqual({ type: "text", text: "World" }) + expect(result[0].content[2]).toEqual({ type: "reasoning", text: "Thinking" }) + }) + test("does not filter for non-anthropic providers", () => { const openaiModel = { ...anthropicModel,