mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-20 22:22:57 +03:00
fix(images): retry Codex image generation on a sibling ChatGPT account
Some ChatGPT accounts can run Codex but lack entitlement for the specific requested image model — upstream returns a stable 400 for that exact case. Today that fails the whole request; #8307 (fenix007) identified this as a production incident and wrote the original repro/test for it. Its own implementation predates the general credential-fallback mechanism this codebase now has (executeImageWithCredentialFallback + saveImageErrorResult's retryable opt-in, #10494) and duplicated that retry loop inline in route.ts, which now collides with it. This reimplements the same fix through the existing mechanism instead: classify the exact upstream message and mark the failure retryable, letting the already-wired sibling-account fallback do the rest. Co-authored-by: fenix007 <4217955+fenix007@users.noreply.github.com> Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1 @@
|
||||
- **fix(images):** retry Codex image generation on a sibling ChatGPT account when the requested model isn't entitled on the current account, instead of failing the request outright ([#8307](https://github.com/diegosouzapw/OmniRoute/pull/8307)).
|
||||
@@ -184,6 +184,29 @@ function sanitizeImageProviderError(errorText: string): unknown {
|
||||
return sanitizeErrorMessage(errorText);
|
||||
}
|
||||
|
||||
// #8307 — some ChatGPT accounts can run Codex but lack entitlement for the specific
|
||||
// requested image model. Upstream signals this as a 400 with an exact, stable message
|
||||
// (not a generic "invalid request"). Classify it so the caller can mark the failure
|
||||
// `retryable: true`, which routes it through the same sibling-account fallback that
|
||||
// already handles 401s (executeImageWithCredentialFallback, src/sse/services/imageCredentialRetry.ts).
|
||||
function isCodexChatGptModelAccessError(status: number, errorText: string, model: string): boolean {
|
||||
if (status !== 400) return false;
|
||||
const parsed = parseJsonOrNull(errorText);
|
||||
let detail: string | null = null;
|
||||
if (typeof parsed === "string") {
|
||||
detail = parsed;
|
||||
} else if (parsed && typeof parsed === "object") {
|
||||
const obj = parsed as Record<string, unknown>;
|
||||
if (typeof obj.detail === "string") detail = obj.detail;
|
||||
else if (typeof obj.message === "string") detail = obj.message;
|
||||
else if (obj.error && typeof obj.error === "object") {
|
||||
const nested = (obj.error as Record<string, unknown>).message;
|
||||
if (typeof nested === "string") detail = nested;
|
||||
}
|
||||
}
|
||||
return detail === `The '${model}' model is not supported when using Codex with a ChatGPT account.`;
|
||||
}
|
||||
|
||||
const BFL_MODEL_ENDPOINTS = {
|
||||
"flux-2-max": "/v1/flux-2-max",
|
||||
"flux-2-pro": "/v1/flux-2-pro",
|
||||
@@ -2532,6 +2555,7 @@ async function handleCodexImageGeneration({
|
||||
const safeErrorLog =
|
||||
typeof safeError === "string" ? safeError : JSON.stringify(safeError ?? {});
|
||||
if (log) log.error("IMAGE", `${provider} error ${response.status}: ${safeErrorLog}`);
|
||||
const retryable = isCodexChatGptModelAccessError(response.status, errorText, model);
|
||||
return {
|
||||
ok: false as const,
|
||||
error: {
|
||||
@@ -2542,6 +2566,7 @@ async function handleCodexImageGeneration({
|
||||
error: safeError,
|
||||
requestBody: requestBodyForLog,
|
||||
path: logPath,
|
||||
...(retryable ? { retryable: true } : {}),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2026,3 +2026,57 @@ test("handleImageGeneration (codex) forwards size and maps GPT-Image quality to
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
|
||||
// #8307 — some ChatGPT accounts can run Codex but lack entitlement for the specific
|
||||
// requested image model, and the upstream 400 for that exact case is retryable on a
|
||||
// sibling account: executeImageWithCredentialFallback (route.ts) already retries on
|
||||
// this signal when the handler marks the failure `retryable: true` — mirroring the
|
||||
// existing 401 auto-rotate path, no new retry loop needed in the handler itself.
|
||||
test("handleImageGeneration (codex) marks the ChatGPT-account model-access 400 as retryable", async () => {
|
||||
const originalFetch = globalThis.fetch;
|
||||
globalThis.fetch = async () =>
|
||||
new Response(
|
||||
JSON.stringify({
|
||||
error: {
|
||||
message:
|
||||
"The 'gpt-5.6-sol' model is not supported when using Codex with a ChatGPT account.",
|
||||
},
|
||||
}),
|
||||
{ status: 400, headers: { "content-type": "application/json" } }
|
||||
);
|
||||
|
||||
try {
|
||||
const result = await handleImageGeneration({
|
||||
body: { model: "codex/gpt-5.6-sol", prompt: "kitten" },
|
||||
credentials: { accessToken: "codex-token" },
|
||||
log: null,
|
||||
});
|
||||
assert.equal(result.success, false);
|
||||
assert.equal(result.status, 400);
|
||||
assert.equal(result.retryable, true);
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
|
||||
test("handleImageGeneration (codex) does not mark an ordinary 400 as retryable", async () => {
|
||||
const originalFetch = globalThis.fetch;
|
||||
globalThis.fetch = async () =>
|
||||
new Response(JSON.stringify({ error: { message: "Invalid prompt" } }), {
|
||||
status: 400,
|
||||
headers: { "content-type": "application/json" },
|
||||
});
|
||||
|
||||
try {
|
||||
const result = await handleImageGeneration({
|
||||
body: { model: "codex/gpt-5.6-sol", prompt: "kitten" },
|
||||
credentials: { accessToken: "codex-token" },
|
||||
log: null,
|
||||
});
|
||||
assert.equal(result.success, false);
|
||||
assert.equal(result.status, 400);
|
||||
assert.equal(result.retryable, undefined);
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user