diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index 9c6949ebcbf..cbebd8605b3 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -742,7 +742,8 @@ export const layer = Layer.effect( } else if ("blob" in c && c.blob) { const mime = ("mimeType" in c ? c.mimeType : undefined) ?? part.mime const url = `data:${mime};base64,${c.blob}` - if (mime.split(";")[0]?.trim().toLowerCase() === "text/plain") { + const mediaType = mime.split(";")[0]?.trim().toLowerCase() + if (mediaType === "text/plain") { pieces.push({ messageID: info.id, sessionID: input.sessionID, @@ -751,6 +752,20 @@ export const layer = Layer.effect( text: decodeDataUrl(url), }) } + const supported = + mediaType?.startsWith("image/") || + mediaType?.startsWith("audio/") || + mediaType?.startsWith("video/") || + mediaType === "application/pdf" + if (mediaType !== "text/plain" && !supported) { + pieces.push({ + messageID: info.id, + sessionID: input.sessionID, + type: "text", + synthetic: true, + text: `[Binary content: ${mime}]`, + }) + } pieces.push({ messageID: info.id, sessionID: input.sessionID, @@ -758,6 +773,7 @@ export const layer = Layer.effect( mime, filename: part.filename, url, + source: supported || mediaType === "text/plain" ? undefined : part.source, }) } } diff --git a/packages/opencode/test/session/message-v2.test.ts b/packages/opencode/test/session/message-v2.test.ts index a1d3a7e31f7..29d4e596ec9 100644 --- a/packages/opencode/test/session/message-v2.test.ts +++ b/packages/opencode/test/session/message-v2.test.ts @@ -351,6 +351,19 @@ describe("session.message-v2.toModelMessage", () => { filename: "guide.txt", url: "data:text/plain; charset=utf-8;base64,IyBSZXNvdXJjZSBjb250ZW50cw==", }, + { + ...basePart(messageID, "p4"), + type: "file", + mime: "application/octet-stream", + filename: "resource.bin", + url: "data:application/octet-stream;base64,AAEC", + source: { + type: "resource", + clientName: "resource-only-fixture", + uri: "opencode-fixture://binary", + text: { value: "@fixture-binary", start: 15, end: 30 }, + }, + }, ] as SessionV1.Part[], }, ] diff --git a/packages/opencode/test/session/prompt.test.ts b/packages/opencode/test/session/prompt.test.ts index 00d58d923c1..c674061322c 100644 --- a/packages/opencode/test/session/prompt.test.ts +++ b/packages/opencode/test/session/prompt.test.ts @@ -266,6 +266,14 @@ const resourceNoLLMServer = testEffect( blob: Buffer.from("MCP text blob fixture").toString("base64"), }, ] + : uri === "opencode-fixture://binary" + ? [ + { + uri, + mimeType: "application/octet-stream", + blob: "AAEC", + }, + ] : [ { uri, @@ -2095,6 +2103,18 @@ resourceNoLLMServer.instance( text: { value: "@fixture-text-blob", start: 15, end: 33 }, }, }, + { + type: "file", + mime: "application/octet-stream", + url: "opencode-fixture://binary", + filename: "fixture-binary", + source: { + type: "resource", + clientName: "resource-only-fixture", + uri: "opencode-fixture://binary", + text: { value: "@fixture-binary", start: 34, end: 49 }, + }, + }, { type: "file", mime: "image/png", @@ -2112,6 +2132,11 @@ resourceNoLLMServer.instance( expect(message.parts.some((part) => part.type === "text" && part.text === "# MCP resource fixture")).toBe(true) expect(message.parts.some((part) => part.type === "text" && part.text === "MCP text blob fixture")).toBe(true) + expect( + message.parts.some( + (part) => part.type === "text" && part.text === "[Binary content: application/octet-stream]", + ), + ).toBe(true) expect( message.parts.some( (part) => @@ -2121,6 +2146,15 @@ resourceNoLLMServer.instance( part.url.startsWith("data:text/plain; charset=utf-8;base64,"), ), ).toBe(true) + expect( + message.parts.some( + (part) => + part.type === "file" && + part.source?.type === "resource" && + part.mime === "application/octet-stream" && + part.url.startsWith("data:application/octet-stream;base64,"), + ), + ).toBe(true) expect( message.parts.some( (part) => part.type === "file" && part.source?.type === "resource" && part.url === "opencode-fixture://pixel",