fix(codex): fan out image n requests in parallel (#2499)

Integrated into release/v3.8.2
This commit is contained in:
NMI
2026-05-21 23:52:58 +05:00
committed by GitHub
parent 01c5d5c7b7
commit c361b3ca02
2 changed files with 109 additions and 29 deletions

View File

@@ -2193,7 +2193,7 @@ async function handleCodexImageGeneration({
if (log && requestedCount > 1) {
log.warn(
"IMAGE",
`Codex hosted image_generation returns one image per call; requested n=${requestedCount} will run sequentially`
`Codex hosted image_generation returns one image per call; requested n=${requestedCount} will fan out in parallel`
);
}
@@ -2262,8 +2262,7 @@ async function handleCodexImageGeneration({
);
}
const collected: Array<{ b64_json: string; revised_prompt?: string }> = [];
for (let i = 0; i < requestedCount; i++) {
const fetchOneImage = async () => {
let response: Response;
try {
response = await fetch(providerConfig.baseUrl, {
@@ -2273,44 +2272,64 @@ async function handleCodexImageGeneration({
});
} catch (err) {
if (log) log.error("IMAGE", `${provider} fetch error: ${(err as Error).message}`);
return saveImageErrorResult({
provider,
model,
status: 502,
startTime,
error: `Image provider error: ${(err as Error).message}`,
requestBody: upstreamBody,
});
return {
ok: false as const,
error: {
provider,
model,
status: 502,
startTime,
error: `Image provider error: ${(err as Error).message}`,
requestBody: upstreamBody,
},
};
}
if (!response.ok) {
const errorText = await response.text();
if (log)
log.error("IMAGE", `${provider} error ${response.status}: ${errorText.slice(0, 200)}`);
return saveImageErrorResult({
provider,
model,
status: response.status,
startTime,
error: errorText,
requestBody: upstreamBody,
});
return {
ok: false as const,
error: {
provider,
model,
status: response.status,
startTime,
error: errorText,
requestBody: upstreamBody,
},
};
}
const rawSSE = await response.text();
const items = extractImageGenerationCalls(rawSSE);
if (items.length === 0) {
return saveImageErrorResult({
provider,
model,
status: 502,
startTime,
error:
"Codex completed without producing an image_generation_call — the model may have declined the tool",
requestBody: upstreamBody,
});
return {
ok: false as const,
error: {
provider,
model,
status: 502,
startTime,
error:
"Codex completed without producing an image_generation_call — the model may have declined the tool",
requestBody: upstreamBody,
},
};
}
for (const item of items) {
return { ok: true as const, items };
};
const imageResults = await Promise.all(
Array.from({ length: requestedCount }, () => fetchOneImage())
);
const collected: Array<{ b64_json: string; revised_prompt?: string }> = [];
for (const imageResult of imageResults) {
if (!imageResult.ok) return saveImageErrorResult(imageResult.error);
for (const item of imageResult.items) {
collected.push({
b64_json: item.b64,
...(item.revisedPrompt ? { revised_prompt: item.revisedPrompt } : {}),

View File

@@ -1842,6 +1842,67 @@ test("handleImageGeneration (codex) returns a data URL when response_format is n
}
});
test("handleImageGeneration (codex) fans out n>1 requests in parallel", async () => {
const originalFetch = globalThis.fetch;
const calls = [];
let releaseFirst;
let pending;
globalThis.fetch = async (_url, options = {}) => {
const index = calls.length;
calls.push(JSON.parse(String(options.body || "{}")));
const sse = buildCodexSSE([
{
type: "image_generation_call",
id: `ig_${index + 1}`,
status: "completed",
result: index === 0 ? "Zmlyc3Q=" : "c2Vjb25k",
},
]);
if (index === 0) {
return new Promise((resolve) => {
releaseFirst = () => resolve(new Response(sse, { status: 200 }));
});
}
return new Response(sse, { status: 200 });
};
try {
pending = handleImageGeneration({
body: {
model: "codex/gpt-5.4",
prompt: "kitten",
n: 2,
response_format: "b64_json",
},
credentials: { accessToken: "codex-token" },
log: null,
});
await Promise.resolve();
assert.equal(calls.length, 2);
assert.deepEqual(
calls.map((call) => call.input[0].content[0].text),
["kitten", "kitten"]
);
releaseFirst();
const result = await pending;
assert.equal(result.success, true);
assert.deepEqual(
result.data.data.map((item) => item.b64_json),
["Zmlyc3Q=", "c2Vjb25k"]
);
} finally {
if (releaseFirst) releaseFirst();
if (pending) await pending.catch(() => {});
globalThis.fetch = originalFetch;
}
});
test("handleImageGeneration (codex) surfaces an error when no image_generation_call is emitted", async () => {
const originalFetch = globalThis.fetch;
globalThis.fetch = async () => {