diff --git a/open-sse/handlers/imageGeneration.ts b/open-sse/handlers/imageGeneration.ts index 8ba6286a31..bc0f4cf30a 100644 --- a/open-sse/handlers/imageGeneration.ts +++ b/open-sse/handlers/imageGeneration.ts @@ -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 } : {}), diff --git a/tests/unit/image-generation-handler.test.ts b/tests/unit/image-generation-handler.test.ts index a60c4c3e6f..83128984ae 100644 --- a/tests/unit/image-generation-handler.test.ts +++ b/tests/unit/image-generation-handler.test.ts @@ -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 () => {