From b28b36afa178885331ede3a3b42402ba58028891 Mon Sep 17 00:00:00 2001 From: Zeke Sikelianos Date: Sun, 5 Apr 2026 08:38:00 -0700 Subject: [PATCH] fix(read): reject unsupported image formats before sending to provider The Read tool accepted any image/* MIME type (except SVG and fastbidsheet) and sent it to the LLM provider. Providers like Anthropic only support JPEG, PNG, GIF, and WebP, so formats like BMP, TIFF, AVIF, and HEIC caused API validation errors. Because the unsupported attachment was persisted in the conversation history, every subsequent message replayed it, making the session unrecoverable. Closes #17772 Closes #15264 --- packages/opencode/src/tool/read.ts | 14 +++++++++- packages/opencode/test/tool/read.test.ts | 33 ++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/packages/opencode/src/tool/read.ts b/packages/opencode/src/tool/read.ts index 0b44c7ad5a79..7e14fa59a2a5 100644 --- a/packages/opencode/src/tool/read.ts +++ b/packages/opencode/src/tool/read.ts @@ -150,8 +150,20 @@ export const ReadTool = Tool.defineEffect( const loaded = yield* instruction.resolve(ctx.messages, filepath, ctx.messageID) const mime = AppFileSystem.mimeType(filepath) - const isImage = mime.startsWith("image/") && mime !== "image/svg+xml" && mime !== "image/vnd.fastbidsheet" + const supportedImageMimes = new Set(["image/jpeg", "image/png", "image/gif", "image/webp"]) + const isImage = supportedImageMimes.has(mime) + const isUnsupportedImage = + !isImage && mime.startsWith("image/") && mime !== "image/svg+xml" && mime !== "image/vnd.fastbidsheet" const isPdf = mime === "application/pdf" + + if (isUnsupportedImage) { + return yield* Effect.fail( + new Error( + `Cannot read image: ${mime} is not a supported format. Supported image formats: JPEG, PNG, GIF, WebP.`, + ), + ) + } + if (isImage || isPdf) { const msg = `${isImage ? "Image" : "PDF"} read successfully` return { diff --git a/packages/opencode/test/tool/read.test.ts b/packages/opencode/test/tool/read.test.ts index 12345266b318..f4ba81125845 100644 --- a/packages/opencode/test/tool/read.test.ts +++ b/packages/opencode/test/tool/read.test.ts @@ -425,6 +425,39 @@ root_type Monster;` expect(result.output).toContain("table Monster") }), ) + + it.live("rejects unsupported image formats like BMP", () => + Effect.gen(function* () { + const dir = yield* tmpdirScoped() + // minimal BMP file header + const bmp = Buffer.from("BM", "ascii") + yield* put(path.join(dir, "image.bmp"), bmp) + + const err = yield* fail(dir, { filePath: path.join(dir, "image.bmp") }) + expect(err.message).toContain("image/bmp is not a supported format") + expect(err.message).toContain("JPEG, PNG, GIF, WebP") + }), + ) + + it.live("rejects unsupported image formats like TIFF", () => + Effect.gen(function* () { + const dir = yield* tmpdirScoped() + yield* put(path.join(dir, "photo.tiff"), Buffer.from("II", "ascii")) + + const err = yield* fail(dir, { filePath: path.join(dir, "photo.tiff") }) + expect(err.message).toContain("is not a supported format") + }), + ) + + it.live("rejects unsupported image formats like AVIF", () => + Effect.gen(function* () { + const dir = yield* tmpdirScoped() + yield* put(path.join(dir, "photo.avif"), Buffer.from([0x00, 0x00, 0x00, 0x1c])) + + const err = yield* fail(dir, { filePath: path.join(dir, "photo.avif") }) + expect(err.message).toContain("is not a supported format") + }), + ) }) describe("tool.read loaded instructions", () => {