diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts index a2eda9d4bbd..9ee25cd9570 100644 --- a/packages/opencode/src/provider/transform.ts +++ b/packages/opencode/src/provider/transform.ts @@ -171,6 +171,20 @@ export namespace ProviderTransform { const filtered = msg.content.map((part) => { if (part.type !== "file" && part.type !== "image") return part + // Check for empty base64 image data + if (part.type === "image") { + const imageStr = part.image.toString() + if (imageStr.startsWith("data:")) { + const match = imageStr.match(/^data:([^;]+);base64,(.*)$/) + if (match && (!match[2] || match[2].length === 0)) { + return { + type: "text" as const, + text: "ERROR: Image file is empty or corrupted. Please provide a valid image.", + } + } + } + } + const mime = part.type === "image" ? part.image.toString().split(";")[0].replace("data:", "") : part.mediaType const filename = part.type === "file" ? part.filename : undefined const modality = mimeToModality(mime) diff --git a/packages/opencode/test/provider/transform.test.ts b/packages/opencode/test/provider/transform.test.ts index 4e202a63cb4..2a9d7c36f3c 100644 --- a/packages/opencode/test/provider/transform.test.ts +++ b/packages/opencode/test/provider/transform.test.ts @@ -259,3 +259,106 @@ describe("ProviderTransform.message - DeepSeek reasoning content", () => { expect(result[0].providerOptions?.openaiCompatible?.reasoning_content).toBeUndefined() }) }) + +describe("ProviderTransform.message - empty image handling", () => { + const mockModel = { + 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("should replace empty base64 image with error text", () => { + const msgs = [ + { + role: "user", + content: [ + { type: "text", text: "What is in this image?" }, + { type: "image", image: "data:image/png;base64," }, + ], + }, + ] as any[] + + const result = ProviderTransform.message(msgs, mockModel) + + expect(result).toHaveLength(1) + expect(result[0].content).toHaveLength(2) + expect(result[0].content[0]).toEqual({ type: "text", text: "What is in this image?" }) + expect(result[0].content[1]).toEqual({ + type: "text", + text: "ERROR: Image file is empty or corrupted. Please provide a valid image.", + }) + }) + + test("should keep valid base64 images unchanged", () => { + const validBase64 = + "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==" + const msgs = [ + { + role: "user", + content: [ + { type: "text", text: "What is in this image?" }, + { type: "image", image: `data:image/png;base64,${validBase64}` }, + ], + }, + ] as any[] + + const result = ProviderTransform.message(msgs, mockModel) + + expect(result).toHaveLength(1) + expect(result[0].content).toHaveLength(2) + expect(result[0].content[0]).toEqual({ type: "text", text: "What is in this image?" }) + expect(result[0].content[1]).toEqual({ type: "image", image: `data:image/png;base64,${validBase64}` }) + }) + + test("should handle mixed valid and empty images", () => { + const validBase64 = + "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==" + const msgs = [ + { + role: "user", + content: [ + { type: "text", text: "Compare these images" }, + { type: "image", image: `data:image/png;base64,${validBase64}` }, + { type: "image", image: "data:image/jpeg;base64," }, + ], + }, + ] as any[] + + const result = ProviderTransform.message(msgs, mockModel) + + expect(result).toHaveLength(1) + expect(result[0].content).toHaveLength(3) + expect(result[0].content[0]).toEqual({ type: "text", text: "Compare these images" }) + expect(result[0].content[1]).toEqual({ type: "image", image: `data:image/png;base64,${validBase64}` }) + expect(result[0].content[2]).toEqual({ + type: "text", + text: "ERROR: Image file is empty or corrupted. Please provide a valid image.", + }) + }) +})