Add fallback text for missing images (#397)

# What?
* When a prompt references an image path that doesn’t exist, replace it
with
  ```[missing image: <path>]``` 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 <tibo@openai.com>
This commit is contained in:
Robbie Hammond
2025-04-18 22:55:24 -07:00
committed by GitHub
parent b46b596e5f
commit 8e2760e83d
2 changed files with 62 additions and 11 deletions

View File

@@ -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;

View File

@@ -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]",
});
});
});