mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-01 12:52:11 +03:00
fix(codex): fan out image n requests in parallel (#2499)
Integrated into release/v3.8.2
This commit is contained in:
@@ -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 } : {}),
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
Reference in New Issue
Block a user