fix(codex): await image URL fetches before sending to upstream (closes #575)

Remote HTTP(S) image URLs are fetched and inlined as base64 data URIs
in a new prefetchImages() step run before super.execute(), so the body
sent to Codex contains resolved image bytes instead of URLs the backend
cannot access.

Scope is limited to the Codex executor — base executor and other
providers are untouched.

Co-authored-by: anuragg-saxenaa <anuragg.saxenaa@gmail.com>
Made-with: Cursor
This commit is contained in:
anuragg-saxenaa
2026-04-17 12:15:10 +07:00
committed by decolua
parent 6e8aaab299
commit d0ace2a3cf
3 changed files with 210 additions and 16 deletions
+31 -16
View File
@@ -3,6 +3,7 @@ import { BaseExecutor } from "./base.js";
import { CODEX_DEFAULT_INSTRUCTIONS } from "../config/codexInstructions.js";
import { PROVIDERS } from "../config/providers.js";
import { normalizeResponsesInput } from "../translator/helpers/responsesApiHelper.js";
import { fetchImageAsBase64 } from "../translator/helpers/imageHelper.js";
import { getConsistentMachineId } from "../../src/shared/utils/machineId.js";
// In-memory map: hash(machineId + first assistant content) → { sessionId, lastUsed }
@@ -93,7 +94,36 @@ export class CodexExecutor extends BaseExecutor {
}
/**
* Transform request before sending - inject default instructions if missing
* Prefetch remote image URLs and inline them as base64 data URIs.
* Runs before execute() because Codex backend cannot fetch remote images.
* Mutates body.input in place.
*/
async prefetchImages(body) {
if (!Array.isArray(body?.input)) return;
for (const item of body.input) {
if (!Array.isArray(item.content)) continue;
const pending = item.content.map(async (c) => {
if (c.type !== "image_url") return c;
const url = typeof c.image_url === "string" ? c.image_url : c.image_url?.url;
const detail = c.image_url?.detail || "auto";
if (!url) return c;
if (url.startsWith("data:")) return { type: "input_image", image_url: url, detail };
const fetched = await fetchImageAsBase64(url, { timeoutMs: 15000 });
return { type: "input_image", image_url: fetched?.url || url, detail };
});
item.content = await Promise.all(pending);
}
}
async execute(args) {
// Fetch remote images before the synchronous transform/execute pipeline
await this.prefetchImages(args.body);
return super.execute(args);
}
/**
* Transform request before sending - inject default instructions if missing.
* Image fetching is handled separately in prefetchImages() so this stays sync.
*/
transformRequest(model, body, stream, credentials) {
this._isCompact = !!body._compact;
@@ -109,21 +139,6 @@ export class CodexExecutor extends BaseExecutor {
body.input = [{ type: "message", role: "user", content: [{ type: "input_text", text: "..." }] }];
}
// Normalize image content: image_url → input_image (Responses API format)
if (Array.isArray(body.input)) {
for (const item of body.input) {
if (Array.isArray(item.content)) {
item.content = item.content.map(c => {
if (c.type === "image_url") {
const url = typeof c.image_url === "string" ? c.image_url : c.image_url?.url;
return { type: "input_image", image_url: url, detail: c.image_url?.detail || "auto" };
}
return c;
});
}
}
}
// Ensure streaming is enabled (Codex API requires it)
body.stream = true;