authorgravatar for saphid@gmail.comAlex Southwell <saphid@gmail.com> 2026-09-18 10:25:51+10:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-17 17:25:51-07:00
log3fd21df62da3191f1abf12a0cd74df6de9ed30a2
tree8a79cd92eb2343fd8f8f2e9465b7ac2ce4e652d4
parent5f225395637cb347cfe416c649825a8c7c210014
signature Signed by PGP key B5690EEEBB952194

fix(server): pass Codex image attachments by path to avoid oversized requests (#11050)

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

4 files changed, 66 insertions(+), 28 deletions(-)

apps/server/src/provider/Layers/CodexAdapter.test.ts+48
......@@ -448,6 +448,54 @@ sessionErrorLayer("CodexAdapterLive session errors", (it) => {
448448 }),
449449 );
450450
451 it.effect("passes image attachments to Codex by path instead of base64", () =>
452 Effect.gen(function* () {
453 const adapter = yield* CodexAdapter;
454 const threadId = asThreadId("thread-image-attachment");
455 yield* adapter.startSession({
456 provider: ProviderDriverKind.make("codex"),
457 threadId,
458 runtimeMode: "full-access",
459 });
460 const runtime = sessionRuntimeFactory.lastRuntime;
461 NodeAssert.ok(runtime);
462 runtime.sendTurnImpl.mockClear();
463
464 const serverConfig = yield* ServerConfig;
465 const attachmentsDir = serverConfig.attachmentsDir;
466 const attachmentId = "attachment-local-image-1";
467 const attachmentPath = NodePath.join(attachmentsDir, `${attachmentId}.png`);
468 NodeFS.writeFileSync(attachmentPath, Buffer.alloc(4, 0x89));
469
470 try {
471 yield* adapter.sendTurn({
472 threadId,
473 input: "Use this image.",
474 attachments: [
475 {
476 type: "image",
477 id: attachmentId,
478 name: "generated.png",
479 mimeType: "image/png",
480 sizeBytes: NodeFS.statSync(attachmentPath).size,
481 },
482 ],
483 });
484
485 const input = runtime.sendTurnImpl.mock.calls[0]?.[0];
486 NodeAssert.ok(input);
487 NodeAssert.deepStrictEqual(input.attachments, [
488 {
489 type: "localImage",
490 path: attachmentPath,
491 },
492 ]);
493 } finally {
494 NodeFS.rmSync(attachmentPath, { force: true });
495 }
496 }),
497 );
498
451499 it.effect("passes configured launch args into the session runtime", () => {
452500 const runtimeFactory = makeRuntimeFactory();
453501 const layer = Layer.effect(
apps/server/src/provider/Layers/CodexAdapter.ts+8-18
......@@ -36,7 +36,6 @@ import * as NodeCrypto from "node:crypto";
3636import * as Crypto from "effect/Crypto";
3737import * as Exit from "effect/Exit";
3838import * as Fiber from "effect/Fiber";
39import * as FileSystem from "effect/FileSystem";
4039import * as Queue from "effect/Queue";
4140import * as Schema from "effect/Schema";
4241import * as Scope from "effect/Scope";
......@@ -67,6 +66,7 @@ import {
6766 makeCodexSessionRuntime,
6867 type CodexSessionRuntimeError,
6968 type CodexSessionRuntimeOptions,
69 type CodexSessionRuntimeSendTurnInput,
7070 type CodexSessionRuntimeShape,
7171} from "./CodexSessionRuntime.ts";
7272import { type EventNdjsonLogger, makeEventNdjsonLogger } from "./EventNdjsonLogger.ts";
......@@ -2218,7 +2218,6 @@ export const makeCodexAdapter = Effect.fn("makeCodexAdapter")(function* (
22182218 options?: CodexAdapterLiveOptions,
22192219) {
22202220 const boundInstanceId = options?.instanceId ?? ProviderInstanceId.make("codex");
2221 const fileSystem = yield* FileSystem.FileSystem;
22222221 const childProcessSpawner = yield* ChildProcessSpawner.ChildProcessSpawner;
22232222 const crypto = yield* Crypto.Crypto;
22242223 const serverConfig = yield* Effect.service(ServerConfig);
......@@ -2491,27 +2490,18 @@ export const makeCodexAdapter = Effect.fn("makeCodexAdapter")(function* (
24912490 detail: `Invalid attachment id '${attachment.id}'.`,
24922491 });
24932492 }
2494 const bytes = yield* fileSystem.readFile(attachmentPath).pipe(
2495 Effect.mapError(
2496 (cause) =>
2497 new ProviderAdapterRequestError({
2498 provider: PROVIDER,
2499 method: "turn/start",
2500 detail: `Failed to read attachment file: ${cause.message}.`,
2501 cause,
2502 }),
2503 ),
2504 );
25052493 return {
2506 type: "image" as const,
2507 url: `data:${attachment.mimeType};base64,${Buffer.from(bytes).toString("base64")}`,
2494 type: "localImage" as const,
2495 path: attachmentPath,
25082496 };
25092497 });
25102498
25112499 const sendTurn: CodexAdapterShape["sendTurn"] = Effect.fn("sendTurn")(function* (input) {
2512 // Codex ingests images only. Anything else would be base64-encoded as an
2513 // image and rejected or misread; generic files reach the agent through the
2514 // path line ProviderService puts in the prompt.
2500 // Codex ingests images only. Anything else would be inlined as an image
2501 // and rejected or misread; generic files reach the agent through the path
2502 // line ProviderService puts in the prompt. Images are passed by path
2503 // instead of base64 so the turn/start request does not scale with file
2504 // size; the CLI reads the file itself.
25152505 const codexAttachments = yield* Effect.forEach(
25162506 (input.attachments ?? []).filter((attachment) => attachment.type === "image"),
25172507 (attachment) => resolveAttachment(input, attachment),
apps/server/src/provider/Layers/CodexSessionRuntime.test.ts+6-6
......@@ -179,8 +179,8 @@ describe("buildTurnStartParams", () => {
179179 runtimeMode: "full-access",
180180 attachments: [
181181 {
182 type: "image",
183 url: { secret } as unknown as string,
182 type: "localImage",
183 path: { secret } as unknown as string,
184184 },
185185 ],
186186 }).pipe(Effect.flip),
......@@ -248,8 +248,8 @@ describe("buildTurnStartParams", () => {
248248 interactionMode: "default",
249249 attachments: [
250250 {
251 type: "image",
252 url: "data:image/png;base64,abc",
251 type: "localImage",
252 path: "/tmp/generated.png",
253253 },
254254 ],
255255 }),
......@@ -268,8 +268,8 @@ describe("buildTurnStartParams", () => {
268268 text: "Implement it",
269269 },
270270 {
271 type: "image",
272 url: "data:image/png;base64,abc",
271 type: "localImage",
272 path: "/tmp/generated.png",
273273 },
274274 ],
275275 model: "gpt-5.3-codex",
apps/server/src/provider/Layers/CodexSessionRuntime.ts+4-4
......@@ -186,8 +186,8 @@ export interface CodexSessionRuntimeOptions {
186186export interface CodexSessionRuntimeSendTurnInput {
187187 readonly input?: string;
188188 readonly attachments?: ReadonlyArray<{
189 readonly type: "image";
190 readonly url: string;
189 readonly type: "localImage";
190 readonly path: string;
191191 }>;
192192 readonly model?: string;
193193 readonly serviceTier?: CodexServiceTier | undefined;
......@@ -614,8 +614,8 @@ export function buildTurnStartParams(input: {
614614 readonly runtimeMode: RuntimeMode;
615615 readonly prompt?: string;
616616 readonly attachments?: ReadonlyArray<{
617 readonly type: "image";
618 readonly url: string;
617 readonly type: "localImage";
618 readonly path: string;
619619 }>;
620620 readonly model?: string;
621621 readonly serviceTier?: CodexServiceTier;