From 3963453f8dbd16f6c9ada77dc653c9a07682e734 Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Mon, 6 Jul 2026 16:45:24 -0500 Subject: [PATCH 1/6] fix(core): exclude media bytes from compaction estimate --- packages/core/src/session/compaction.ts | 12 +++- packages/core/test/session-compaction.test.ts | 56 ++++++++++++++++++- 2 files changed, 66 insertions(+), 2 deletions(-) diff --git a/packages/core/src/session/compaction.ts b/packages/core/src/session/compaction.ts index 375d6e5266cd..bbce833c0321 100644 --- a/packages/core/src/session/compaction.ts +++ b/packages/core/src/session/compaction.ts @@ -80,7 +80,17 @@ export interface Interface { export class Service extends Context.Service()("@opencode/v2/SessionCompaction") {} -const estimate = (value: unknown) => Token.estimate(JSON.stringify(value)) +const estimate = (value: unknown) => + Token.estimate( + JSON.stringify(value, (_key, item: unknown) => { + if (typeof item !== "object" || item === null || !("type" in item)) return item + // Providers account for native media separately; its base64 encoding is not prompt text. + if (item.type === "media" && "data" in item) return { ...item, data: "[media bytes]" } + if (item.type === "file" && "uri" in item && typeof item.uri === "string" && item.uri.startsWith("data:")) + return { ...item, uri: item.uri.slice(0, item.uri.indexOf(",") + 1) + "[media bytes]" } + return item + }), + ) const truncate = (value: string) => value.length <= TOOL_OUTPUT_MAX_CHARS ? value : `${value.slice(0, TOOL_OUTPUT_MAX_CHARS)}\n[truncated]` diff --git a/packages/core/test/session-compaction.test.ts b/packages/core/test/session-compaction.test.ts index dd5e9fb7496f..a89f50b70c3c 100644 --- a/packages/core/test/session-compaction.test.ts +++ b/packages/core/test/session-compaction.test.ts @@ -1,6 +1,7 @@ import { expect, test } from "bun:test" -import { LLMClient, LLMEvent, Model, type LLMRequest } from "@opencode-ai/llm" +import { LLM, LLMClient, LLMEvent, Message, Model, type LLMRequest } from "@opencode-ai/llm" import { OpenAIChat } from "@opencode-ai/llm/protocols" +import { Base64, FileAttachment } from "@opencode-ai/schema/prompt" import { Config } from "@opencode-ai/core/config" import { Database } from "@opencode-ai/core/database/database" import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" @@ -68,6 +69,59 @@ test("compaction describes tool media without embedding base64", () => { expect(serialized).not.toContain(base64) }) +it.effect("does not count media base64 as text context", () => + Effect.gen(function* () { + requests = [] + const compaction = yield* SessionCompaction.Service + const text = "context ".repeat(4_000) + const data = Base64.make(Buffer.alloc(64 * 1024).toString("base64")) + const file = FileAttachment.make({ + data, + mime: "image/png", + source: { type: "inline" }, + name: "screenshot.png", + }) + const inputModel = Model.make({ + id: "media-model", + provider: "test", + route: OpenAIChat.route.with({ limits: { context: 30_000, output: 1_000 } }), + }) + const messages = [ + SessionMessage.User.make({ + id: SessionMessage.ID.create(), + type: "user", + text, + time: { created: DateTime.makeUnsafe(0) }, + }), + SessionMessage.User.make({ + id: SessionMessage.ID.create(), + type: "user", + text: "Inspect this image", + files: [file], + time: { created: DateTime.makeUnsafe(1) }, + }), + ] + + expect( + yield* compaction.compactIfNeeded({ + sessionID: SessionV2.ID.make("ses_media_compaction"), + messages, + request: LLM.request({ + model: inputModel, + messages: [ + Message.user(text), + Message.user([ + { type: "text", text: "Inspect this image" }, + { type: "media", mediaType: "image/png", data, filename: "screenshot.png" }, + ]), + ], + }), + }), + ).toBe(false) + expect(requests).toHaveLength(0) + }), +) + it.effect("manual compaction summarizes short context instead of no-op", () => Effect.gen(function* () { requests = [] From 7b022ed71f4d8a1c6cc2c64b3a365814e54f8cb7 Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Mon, 6 Jul 2026 17:12:43 -0500 Subject: [PATCH 2/6] fix(core): filter attachments from context estimate --- packages/core/src/session/compaction.ts | 39 ++++++++------- packages/core/test/session-compaction.test.ts | 47 +++++++++++++------ 2 files changed, 56 insertions(+), 30 deletions(-) diff --git a/packages/core/src/session/compaction.ts b/packages/core/src/session/compaction.ts index bbce833c0321..0d3cd10bc94a 100644 --- a/packages/core/src/session/compaction.ts +++ b/packages/core/src/session/compaction.ts @@ -80,17 +80,28 @@ export interface Interface { export class Service extends Context.Service()("@opencode/v2/SessionCompaction") {} -const estimate = (value: unknown) => - Token.estimate( - JSON.stringify(value, (_key, item: unknown) => { - if (typeof item !== "object" || item === null || !("type" in item)) return item - // Providers account for native media separately; its base64 encoding is not prompt text. - if (item.type === "media" && "data" in item) return { ...item, data: "[media bytes]" } - if (item.type === "file" && "uri" in item && typeof item.uri === "string" && item.uri.startsWith("data:")) - return { ...item, uri: item.uri.slice(0, item.uri.indexOf(",") + 1) + "[media bytes]" } - return item - }), - ) +const estimate = (value: unknown) => Token.estimate(JSON.stringify(value)) + +const textContext = (request: LLMRequest) => ({ + system: request.system, + // TODO: Replace blanket attachment exclusion with model-aware media and file token accounting. + messages: request.messages + .filter((message) => message.metadata?.attachment === undefined) + .map((message) => ({ + id: message.id, + role: message.role, + content: message.content.flatMap((part) => { + if (part.type === "media") return [] + if (part.type !== "tool-result" || part.result.type !== "content") return [part] + return [ + { ...part, result: { ...part.result, value: part.result.value.filter((item) => item.type === "text") } }, + ] + }), + metadata: message.metadata, + native: message.native, + })), + tools: request.tools, +}) const truncate = (value: string) => value.length <= TOOL_OUTPUT_MAX_CHARS ? value : `${value.slice(0, TOOL_OUTPUT_MAX_CHARS)}\n[truncated]` @@ -289,11 +300,7 @@ const make = (dependencies: Dependencies) => { const context = input.request.model.route.defaults.limits?.context if (context === undefined || context <= 0) return false const output = input.request.generation?.maxTokens ?? input.request.model.route.defaults.limits?.output ?? 0 - if ( - estimate({ system: input.request.system, messages: input.request.messages, tools: input.request.tools }) <= - context - Math.max(output, config.buffer) - ) - return false + if (estimate(textContext(input.request)) <= context - Math.max(output, config.buffer)) return false return yield* compactAfterOverflow(input) }) return { diff --git a/packages/core/test/session-compaction.test.ts b/packages/core/test/session-compaction.test.ts index a89f50b70c3c..7af86834b042 100644 --- a/packages/core/test/session-compaction.test.ts +++ b/packages/core/test/session-compaction.test.ts @@ -1,5 +1,5 @@ import { expect, test } from "bun:test" -import { LLM, LLMClient, LLMEvent, Message, Model, type LLMRequest } from "@opencode-ai/llm" +import { LLM, LLMClient, LLMEvent, Model, type LLMRequest } from "@opencode-ai/llm" import { OpenAIChat } from "@opencode-ai/llm/protocols" import { Base64, FileAttachment } from "@opencode-ai/schema/prompt" import { Config } from "@opencode-ai/core/config" @@ -13,12 +13,15 @@ import { SessionCompaction } from "@opencode-ai/core/session/compaction" import { SessionEvent } from "@opencode-ai/core/session/event" import { SessionMessage } from "@opencode-ai/core/session/message" import { SessionProjector } from "@opencode-ai/core/session/projector" +import { toLLMMessages } from "@opencode-ai/core/session/runner/to-llm-message" import { SessionRunnerModel } from "@opencode-ai/core/session/runner/model" import { SessionTable } from "@opencode-ai/core/session/sql" import { SessionStore } from "@opencode-ai/core/session/store" import { SessionV2 } from "@opencode-ai/core/session" import { Project } from "@opencode-ai/core/project" import { ProjectTable } from "@opencode-ai/core/project/sql" +import { ModelV2 } from "@opencode-ai/core/model" +import { ProviderV2 } from "@opencode-ai/core/provider" import { AbsolutePath } from "@opencode-ai/core/schema" import { DateTime, Effect, Layer, Stream } from "effect" import { asc, eq } from "drizzle-orm" @@ -69,23 +72,33 @@ test("compaction describes tool media without embedding base64", () => { expect(serialized).not.toContain(base64) }) -it.effect("does not count media base64 as text context", () => +it.effect("does not count file attachments as text context", () => Effect.gen(function* () { requests = [] const compaction = yield* SessionCompaction.Service const text = "context ".repeat(4_000) const data = Base64.make(Buffer.alloc(64 * 1024).toString("base64")) - const file = FileAttachment.make({ + const image = FileAttachment.make({ data, mime: "image/png", source: { type: "inline" }, name: "screenshot.png", }) + const document = FileAttachment.make({ + data: Base64.make(Buffer.alloc(64 * 1024, "a").toString("base64")), + mime: "text/plain", + source: { type: "inline" }, + name: "notes.txt", + }) const inputModel = Model.make({ id: "media-model", provider: "test", route: OpenAIChat.route.with({ limits: { context: 30_000, output: 1_000 } }), }) + const inputModelRef = ModelV2.Ref.make({ + id: ModelV2.ID.make(inputModel.id), + providerID: ProviderV2.ID.make(inputModel.provider), + }) const messages = [ SessionMessage.User.make({ id: SessionMessage.ID.create(), @@ -97,25 +110,31 @@ it.effect("does not count media base64 as text context", () => id: SessionMessage.ID.create(), type: "user", text: "Inspect this image", - files: [file], + files: [document, image], time: { created: DateTime.makeUnsafe(1) }, }), ] + const request = LLM.request({ + model: inputModel, + messages: toLLMMessages(messages, inputModelRef), + }) + + expect(request.messages.flatMap((message) => message.content)).toContainEqual({ + type: "media", + mediaType: "image/png", + data, + filename: "screenshot.png", + }) + expect(request.messages[1]?.content[0]).toMatchObject({ + type: "text", + text: expect.stringContaining("Attached file: notes.txt"), + }) expect( yield* compaction.compactIfNeeded({ sessionID: SessionV2.ID.make("ses_media_compaction"), messages, - request: LLM.request({ - model: inputModel, - messages: [ - Message.user(text), - Message.user([ - { type: "text", text: "Inspect this image" }, - { type: "media", mediaType: "image/png", data, filename: "screenshot.png" }, - ]), - ], - }), + request, }), ).toBe(false) expect(requests).toHaveLength(0) From f953a8f7e39847348e2f119ca43d0557ecbd5b51 Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Mon, 6 Jul 2026 17:22:08 -0500 Subject: [PATCH 3/6] docs(core): explain attachment estimate filtering --- packages/core/src/session/compaction.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/core/src/session/compaction.ts b/packages/core/src/session/compaction.ts index 57cdbb2c2b5a..da7b71ef40c7 100644 --- a/packages/core/src/session/compaction.ts +++ b/packages/core/src/session/compaction.ts @@ -86,6 +86,7 @@ const textContext = (request: LLMRequest) => ({ system: request.system, // TODO: Replace blanket attachment exclusion with model-aware media and file token accounting. messages: request.messages + // Text attachments are lowered into dedicated marked messages; the original prompt is emitted separately. .filter((message) => message.metadata?.attachment === undefined) .map((message) => ({ id: message.id, From ad550c544e48d4e3dc3550efcc1ad7a8a30a1af6 Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Mon, 6 Jul 2026 17:58:17 -0500 Subject: [PATCH 4/6] fix(core): preserve text attachments in context estimate --- packages/core/src/session/compaction.ts | 29 ++++++++----------- packages/core/test/session-compaction.test.ts | 14 ++------- 2 files changed, 14 insertions(+), 29 deletions(-) diff --git a/packages/core/src/session/compaction.ts b/packages/core/src/session/compaction.ts index da7b71ef40c7..f1103d0c3af0 100644 --- a/packages/core/src/session/compaction.ts +++ b/packages/core/src/session/compaction.ts @@ -84,23 +84,18 @@ const estimate = (value: unknown) => Token.estimate(JSON.stringify(value)) const textContext = (request: LLMRequest) => ({ system: request.system, - // TODO: Replace blanket attachment exclusion with model-aware media and file token accounting. - messages: request.messages - // Text attachments are lowered into dedicated marked messages; the original prompt is emitted separately. - .filter((message) => message.metadata?.attachment === undefined) - .map((message) => ({ - id: message.id, - role: message.role, - content: message.content.flatMap((part) => { - if (part.type === "media") return [] - if (part.type !== "tool-result" || part.result.type !== "content") return [part] - return [ - { ...part, result: { ...part.result, value: part.result.value.filter((item) => item.type === "text") } }, - ] - }), - metadata: message.metadata, - native: message.native, - })), + // TODO: Replace media exclusion with model-aware attachment token accounting. + messages: request.messages.map((message) => ({ + id: message.id, + role: message.role, + content: message.content.flatMap((part) => { + if (part.type === "media") return [] + if (part.type !== "tool-result" || part.result.type !== "content") return [part] + return [{ ...part, result: { ...part.result, value: part.result.value.filter((item) => item.type === "text") } }] + }), + metadata: message.metadata, + native: message.native, + })), tools: request.tools, }) diff --git a/packages/core/test/session-compaction.test.ts b/packages/core/test/session-compaction.test.ts index cf24bcf87bab..0f9f8e12ec90 100644 --- a/packages/core/test/session-compaction.test.ts +++ b/packages/core/test/session-compaction.test.ts @@ -72,7 +72,7 @@ test("compaction describes tool media without embedding base64", () => { expect(serialized).not.toContain(base64) }) -it.effect("does not count file attachments as text context", () => +it.effect("does not count image attachments as text context", () => Effect.gen(function* () { requests = [] const compaction = yield* SessionCompaction.Service @@ -84,12 +84,6 @@ it.effect("does not count file attachments as text context", () => source: { type: "inline" }, name: "screenshot.png", }) - const document = FileAttachment.make({ - data: Base64.make(Buffer.alloc(64 * 1024, "a").toString("base64")), - mime: "text/plain", - source: { type: "inline" }, - name: "notes.txt", - }) const inputModel = Model.make({ id: "media-model", provider: "test", @@ -110,7 +104,7 @@ it.effect("does not count file attachments as text context", () => id: SessionMessage.ID.create(), type: "user", text: "Inspect this image", - files: [document, image], + files: [image], time: { created: DateTime.makeUnsafe(1) }, }), ] @@ -125,10 +119,6 @@ it.effect("does not count file attachments as text context", () => data, filename: "screenshot.png", }) - expect(request.messages[1]?.content[0]).toMatchObject({ - type: "text", - text: expect.stringContaining("Attached file: notes.txt"), - }) expect( yield* compaction.compactIfNeeded({ From a48d38b6cdb9a00b466f107431f6f5e432eff563 Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Mon, 6 Jul 2026 18:15:18 -0500 Subject: [PATCH 5/6] fix(core): estimate semantic request context --- packages/core/src/session/compaction.ts | 37 +++++------ packages/core/test/session-compaction.test.ts | 64 ++++++++++++++++++- 2 files changed, 82 insertions(+), 19 deletions(-) diff --git a/packages/core/src/session/compaction.ts b/packages/core/src/session/compaction.ts index f1103d0c3af0..ce4e84e91fb1 100644 --- a/packages/core/src/session/compaction.ts +++ b/packages/core/src/session/compaction.ts @@ -80,24 +80,25 @@ export interface Interface { export class Service extends Context.Service()("@opencode/v2/SessionCompaction") {} -const estimate = (value: unknown) => Token.estimate(JSON.stringify(value)) +const stringify = (value: unknown) => (typeof value === "string" ? value : (JSON.stringify(value) ?? String(value))) -const textContext = (request: LLMRequest) => ({ - system: request.system, - // TODO: Replace media exclusion with model-aware attachment token accounting. - messages: request.messages.map((message) => ({ - id: message.id, - role: message.role, - content: message.content.flatMap((part) => { - if (part.type === "media") return [] - if (part.type !== "tool-result" || part.result.type !== "content") return [part] - return [{ ...part, result: { ...part.result, value: part.result.value.filter((item) => item.type === "text") } }] - }), - metadata: message.metadata, - native: message.native, - })), - tools: request.tools, -}) +const serializeContent = (part: LLMRequest["messages"][number]["content"][number]) => { + if (part.type === "text" || part.type === "reasoning") return part.text + if (part.type === "media") return "" + if (part.type === "tool-call") return `${part.name}\n${stringify(part.input)}` + if (part.result.type === "content") + return [part.name, ...part.result.value.flatMap((item) => (item.type === "text" ? [item.text] : []))].join("\n") + return `${part.name}\n${stringify(part.result.value)}` +} + +const estimate = (request: LLMRequest) => + Token.estimate( + [ + ...request.system.map((part) => part.text), + JSON.stringify(request.tools), + ...request.messages.flatMap((message) => message.content.map(serializeContent).filter(Boolean)), + ].join("\n"), + ) const truncate = (value: string) => value.length <= TOOL_OUTPUT_MAX_CHARS ? value : `${value.slice(0, TOOL_OUTPUT_MAX_CHARS)}\n[truncated]` @@ -302,7 +303,7 @@ const make = (dependencies: Dependencies) => { const context = input.request.model.route.defaults.limits?.context if (context === undefined || context <= 0) return false const output = input.request.generation?.maxTokens ?? input.request.model.route.defaults.limits?.output ?? 0 - if (estimate(textContext(input.request)) <= context - Math.max(output, config.buffer)) return false + if (estimate(input.request) <= context - Math.max(output, config.buffer)) return false return yield* compactAfterOverflow(input) }) return { diff --git a/packages/core/test/session-compaction.test.ts b/packages/core/test/session-compaction.test.ts index 0f9f8e12ec90..ae6c1c70a845 100644 --- a/packages/core/test/session-compaction.test.ts +++ b/packages/core/test/session-compaction.test.ts @@ -1,5 +1,5 @@ import { expect, test } from "bun:test" -import { LLM, LLMClient, LLMEvent, Model, type LLMRequest } from "@opencode-ai/llm" +import { LLM, LLMClient, LLMEvent, Message, Model, ToolCallPart, type LLMRequest } from "@opencode-ai/llm" import { OpenAIChat } from "@opencode-ai/llm/protocols" import { Base64, FileAttachment } from "@opencode-ai/schema/prompt" import { Config } from "@opencode-ai/core/config" @@ -131,6 +131,68 @@ it.effect("does not count image attachments as text context", () => }), ) +it.effect("counts tool-call inputs as text context", () => + Effect.gen(function* () { + requests = [] + const db = (yield* Database.Service).db + const compaction = yield* SessionCompaction.Service + const text = "context ".repeat(4_500) + const sessionID = SessionV2.ID.make("ses_tool_input_compaction") + const inputModel = Model.make({ + id: "tool-input-model", + provider: "test", + route: OpenAIChat.route.with({ limits: { context: 30_000, output: 1_000 } }), + }) + const messages = [ + SessionMessage.User.make({ + id: SessionMessage.ID.create(), + type: "user", + text, + time: { created: DateTime.makeUnsafe(0) }, + }), + SessionMessage.User.make({ + id: SessionMessage.ID.create(), + type: "user", + text: "Continue", + time: { created: DateTime.makeUnsafe(1) }, + }), + ] + yield* db + .insert(ProjectTable) + .values({ id: Project.ID.global, worktree: AbsolutePath.make("/project"), sandboxes: [] }) + .onConflictDoNothing() + .run() + .pipe(Effect.orDie) + yield* db + .insert(SessionTable) + .values({ + id: sessionID, + project_id: Project.ID.global, + slug: "tool-input-compaction", + directory: "/project", + title: "Tool input compaction", + version: "test", + }) + .run() + .pipe(Effect.orDie) + + expect( + yield* compaction.compactIfNeeded({ + sessionID, + messages, + request: LLM.request({ + model: inputModel, + messages: [ + Message.user(text), + Message.assistant(ToolCallPart.make({ id: "call_read", name: "read", input: { path: "x".repeat(8_000) } })), + ], + }), + }), + ).toBe(true) + expect(requests).toHaveLength(1) + }), +) + test("compaction prompt requires the checkpoint headings in order", () => { const prompt = SessionCompaction.buildPrompt({ context: ["Conversation history"] }) expect(prompt.match(/^#{2,3} .+$/gm)).toEqual([ From 2acf08cca055f6bac41da762fefa08dc053a3a1a Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Mon, 6 Jul 2026 18:41:04 -0500 Subject: [PATCH 6/6] fix(core): harden semantic context estimation --- packages/core/src/session/compaction.ts | 43 +++++++++++++++++-- packages/core/test/session-compaction.test.ts | 28 ++++++++++-- 2 files changed, 63 insertions(+), 8 deletions(-) diff --git a/packages/core/src/session/compaction.ts b/packages/core/src/session/compaction.ts index ce4e84e91fb1..d3bb0a5cc64b 100644 --- a/packages/core/src/session/compaction.ts +++ b/packages/core/src/session/compaction.ts @@ -80,22 +80,57 @@ export interface Interface { export class Service extends Context.Service()("@opencode/v2/SessionCompaction") {} -const stringify = (value: unknown) => (typeof value === "string" ? value : (JSON.stringify(value) ?? String(value))) +const serializeString = (value: unknown) => { + try { + return String(value) + } catch { + return "[unserializable]" + } +} + +const serializeJson = (value: unknown) => { + try { + return JSON.stringify(value) ?? serializeString(value) + } catch { + return serializeString(value) + } +} + +const serializeError = (value: unknown) => { + try { + const prototype = + typeof value === "object" && value !== null && !Array.isArray(value) && Object.getPrototypeOf(value) + const structured = Array.isArray(value) || prototype === Object.prototype || prototype === null + return structured ? serializeJson(value) : serializeString(value) + } catch { + return serializeString(value) + } +} const serializeContent = (part: LLMRequest["messages"][number]["content"][number]) => { if (part.type === "text" || part.type === "reasoning") return part.text if (part.type === "media") return "" - if (part.type === "tool-call") return `${part.name}\n${stringify(part.input)}` + if (part.type === "tool-call") return `${part.name}\n${serializeJson(part.input)}` + // OpenAI replays hosted image generations by item reference; the opaque JSON result contains the image bytes. + if ( + part.providerExecuted && + part.name === "image_generation" && + part.result.type === "json" && + typeof part.providerMetadata?.openai?.itemId === "string" + ) + return part.name if (part.result.type === "content") return [part.name, ...part.result.value.flatMap((item) => (item.type === "text" ? [item.text] : []))].join("\n") - return `${part.name}\n${stringify(part.result.value)}` + if (part.result.type === "text") return `${part.name}\n${serializeString(part.result.value)}` + if (part.result.type === "error") return `${part.name}\n${serializeError(part.result.value)}` + return `${part.name}\n${serializeJson(part.result.value)}` } const estimate = (request: LLMRequest) => Token.estimate( [ ...request.system.map((part) => part.text), - JSON.stringify(request.tools), + serializeJson(request.tools), ...request.messages.flatMap((message) => message.content.map(serializeContent).filter(Boolean)), ].join("\n"), ) diff --git a/packages/core/test/session-compaction.test.ts b/packages/core/test/session-compaction.test.ts index ae6c1c70a845..47f5608cbf1d 100644 --- a/packages/core/test/session-compaction.test.ts +++ b/packages/core/test/session-compaction.test.ts @@ -1,6 +1,15 @@ import { expect, test } from "bun:test" -import { LLM, LLMClient, LLMEvent, Message, Model, ToolCallPart, type LLMRequest } from "@opencode-ai/llm" -import { OpenAIChat } from "@opencode-ai/llm/protocols" +import { + LLM, + LLMClient, + LLMEvent, + Message, + Model, + ToolCallPart, + ToolResultPart, + type LLMRequest, +} from "@opencode-ai/llm" +import { OpenAIChat, OpenAIResponses } from "@opencode-ai/llm/protocols" import { Base64, FileAttachment } from "@opencode-ai/schema/prompt" import { Config } from "@opencode-ai/core/config" import { Database } from "@opencode-ai/core/database/database" @@ -87,7 +96,7 @@ it.effect("does not count image attachments as text context", () => const inputModel = Model.make({ id: "media-model", provider: "test", - route: OpenAIChat.route.with({ limits: { context: 30_000, output: 1_000 } }), + route: OpenAIResponses.route.with({ limits: { context: 30_000, output: 1_000 } }), }) const inputModelRef = ModelV2.Ref.make({ id: ModelV2.ID.make(inputModel.id), @@ -110,7 +119,18 @@ it.effect("does not count image attachments as text context", () => ] const request = LLM.request({ model: inputModel, - messages: toLLMMessages(messages, inputModelRef), + messages: [ + ...toLLMMessages(messages, inputModelRef), + Message.assistant( + ToolResultPart.make({ + id: "image_generation_1", + name: "image_generation", + result: { type: "image_generation_call", output: Buffer.alloc(64 * 1024).toString("base64") }, + providerExecuted: true, + providerMetadata: { openai: { itemId: "image_generation_1" } }, + }), + ), + ], }) expect(request.messages.flatMap((message) => message.content)).toContainEqual({