From 8e2760e83d50c7bdd2b234728090a0777e65b31b Mon Sep 17 00:00:00 2001 From: Robbie Hammond Date: Fri, 18 Apr 2025 22:55:24 -0700 Subject: [PATCH] Add fallback text for missing images (#397) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # What? * When a prompt references an image path that doesn’t exist, replace it with ```[missing image: ]``` instead of throwing an ENOENT. * Adds a few unit tests for input-utils as there weren't any beforehand. # Why? Right now if you enter an invalid image path (e.g. it doesn't exist), codex immediately crashes with a ENOENT error like so: ``` Error: ENOENT: no such file or directory, open 'test.png' ... { errno: -2, code: 'ENOENT', syscall: 'open', path: 'test.png' } ``` This aborts the entire session. A soft fallback lets the rest of the input continue. # How? Wraps the image encoding + inputItem content pushing in a try-catch. This is a minimal patch to avoid completely crashing — future work could surface a warning to the user when this happens, or something to that effect. --------- Co-authored-by: Thibault Sottiaux --- codex-cli/src/utils/input-utils.ts | 30 ++++++++++++-------- codex-cli/tests/input-utils.test.ts | 43 +++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+), 11 deletions(-) create mode 100644 codex-cli/tests/input-utils.test.ts diff --git a/codex-cli/src/utils/input-utils.ts b/codex-cli/src/utils/input-utils.ts index 80a238ba..449f27cb 100644 --- a/codex-cli/src/utils/input-utils.ts +++ b/codex-cli/src/utils/input-utils.ts @@ -2,6 +2,7 @@ import type { ResponseInputItem } from "openai/resources/responses/responses"; import { fileTypeFromBuffer } from "file-type"; import fs from "fs/promises"; +import path from "path"; export async function createInputItem( text: string, @@ -14,17 +15,24 @@ export async function createInputItem( }; for (const filePath of images) { - /* eslint-disable no-await-in-loop */ - const binary = await fs.readFile(filePath); - const kind = await fileTypeFromBuffer(binary); - /* eslint-enable no-await-in-loop */ - const encoded = binary.toString("base64"); - const mime = kind?.mime ?? "application/octet-stream"; - inputItem.content.push({ - type: "input_image", - detail: "auto", - image_url: `data:${mime};base64,${encoded}`, - }); + try { + /* eslint-disable no-await-in-loop */ + const binary = await fs.readFile(filePath); + const kind = await fileTypeFromBuffer(binary); + /* eslint-enable no-await-in-loop */ + const encoded = binary.toString("base64"); + const mime = kind?.mime ?? "application/octet-stream"; + inputItem.content.push({ + type: "input_image", + detail: "auto", + image_url: `data:${mime};base64,${encoded}`, + }); + } catch (err) { + inputItem.content.push({ + type: "input_text", + text: `[missing image: ${path.basename(filePath)}]`, + }); + } } return inputItem; diff --git a/codex-cli/tests/input-utils.test.ts b/codex-cli/tests/input-utils.test.ts new file mode 100644 index 00000000..5290e554 --- /dev/null +++ b/codex-cli/tests/input-utils.test.ts @@ -0,0 +1,43 @@ +import { describe, it, expect, vi } from "vitest"; +import fs from "fs/promises"; +import { createInputItem } from "../src/utils/input-utils.js"; + +describe("createInputItem", () => { + it("returns only text when no images provided", async () => { + const result = await createInputItem("hello", []); + expect(result).toEqual({ + role: "user", + type: "message", + content: [{ type: "input_text", text: "hello" }], + }); + }); + + it("includes image content for existing file", async () => { + const fakeBuffer = Buffer.from("fake image content"); + const readSpy = vi.spyOn(fs, "readFile").mockResolvedValue(fakeBuffer as any); + const result = await createInputItem("hello", ["dummy-path"]); + const expectedUrl = `data:application/octet-stream;base64,${fakeBuffer.toString("base64")}`; + expect(result.role).toBe("user"); + expect(result.type).toBe("message"); + expect(result.content.length).toBe(2); + const [textItem, imageItem] = result.content; + expect(textItem).toEqual({ type: "input_text", text: "hello" }); + expect(imageItem).toEqual({ + type: "input_image", + detail: "auto", + image_url: expectedUrl, + }); + readSpy.mockRestore(); + }); + + it("falls back to missing image text for non-existent file", async () => { + const filePath = "tests/__fixtures__/does-not-exist.png"; + const result = await createInputItem("hello", [filePath]); + expect(result.content.length).toBe(2); + const fallbackItem = result.content[1]; + expect(fallbackItem).toEqual({ + type: "input_text", + text: "[missing image: does-not-exist.png]", + }); + }); +}); \ No newline at end of file