From db9e942398a210fb02d3ce9d1d8a2f8ae231cdd6 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Fri, 31 Jul 2026 13:24:05 -0400 Subject: [PATCH] fix(core): resize large image attachments (#39919) --- packages/core/src/session.ts | 6 ++-- packages/core/test/session-prompt.test.ts | 36 +++++++++++++++++++++-- 2 files changed, 37 insertions(+), 5 deletions(-) diff --git a/packages/core/src/session.ts b/packages/core/src/session.ts index 9f5f8e4aa5a..e438e67bb26 100644 --- a/packages/core/src/session.ts +++ b/packages/core/src/session.ts @@ -938,7 +938,7 @@ const materializeAttachment = Effect.fn("Session.materializeAttachment")(functio : resolved.bytes const normalized = yield* normalizeImageAttachment( input, - Base64.make(Buffer.from(content).toString("base64")), + Buffer.from(content).toString("base64"), mime, image, ) @@ -954,11 +954,11 @@ const materializeAttachment = Effect.fn("Session.materializeAttachment")(functio const normalizeImageAttachment = Effect.fn("Session.normalizeImageAttachment")(function* ( input: PromptInput.FileAttachment, - data: Base64, + data: string, mime: string, image: Effect.Effect, ) { - if (!mime.startsWith("image/")) return { data, mime } + if (!mime.startsWith("image/")) return { data: Base64.make(data), mime } const service = yield* image const label = input.name ?? (input.uri.startsWith("data:") ? "inline attachment" : input.uri) const content = { uri: label, content: data, encoding: "base64" as const, mime } diff --git a/packages/core/test/session-prompt.test.ts b/packages/core/test/session-prompt.test.ts index 9f30efe9c06..b292a11c434 100644 --- a/packages/core/test/session-prompt.test.ts +++ b/packages/core/test/session-prompt.test.ts @@ -26,8 +26,8 @@ import { SessionPendingTable, SessionMessageTable, SessionTable } from "@opencod import { SessionStore } from "@opencode-ai/core/session/store" import { LocationServiceMap } from "@opencode-ai/core/location-service-map" import type { LocationServices } from "@opencode-ai/core/location-services" +import { Image } from "@opencode-ai/core/image" import { testEffect } from "./lib/effect" -import { imagePassthrough } from "./lib/image" const executionCalls: Session.ID[] = [] const interruptCalls: Session.ID[] = [] @@ -58,7 +58,10 @@ const locations = Layer.effect( () => // Attachment admission only needs the location-scoped Image service. // oxlint-disable-next-line typescript-eslint/no-unsafe-type-assertion - imagePassthrough as unknown as Layer.Layer, + Layer.mock(Image.Service, { + normalize: (_resource, content) => + Effect.succeed(content.content.length > 5 * 1024 * 1024 ? { ...content, content: "AA==" } : content), + }) as unknown as Layer.Layer, ), ) const it = testEffect( @@ -386,6 +389,35 @@ describe("Session.prompt", () => { }), ) + it.effect("normalizes large image content before validating persisted Base64", () => + Effect.gen(function* () { + yield* setup + const session = yield* Session.Service + const pixel = Buffer.from( + "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=", + "base64", + ) + const bytes = Buffer.concat([pixel, Buffer.alloc(4_323_030 - pixel.length)]) + const data = bytes.toString("base64") + expect(data).toHaveLength(5_764_040) + + const message = yield* session.prompt({ + sessionID, + text: "Inspect this image", + files: [{ uri: `data:image/png;base64,${data}` }], + resume: false, + }) + + expect(message.data.files).toEqual([ + { + data: "AA==", + mime: "image/png", + source: { type: "inline" }, + }, + ]) + }), + ) + it.effect("sniffs data URL content instead of trusting its declared MIME", () => Effect.gen(function* () { yield* setup