diff --git a/open-sse/executors/codex/tools.ts b/open-sse/executors/codex/tools.ts index 12cb840f2c..277af81eb9 100644 --- a/open-sse/executors/codex/tools.ts +++ b/open-sse/executors/codex/tools.ts @@ -26,7 +26,14 @@ export const CODEX_HOSTED_TOOL_TYPES: ReadonlySet = new Set([ // dropped for free-plan accounts (mirrors CLIProxyAPI's isCodexFreePlanAuth). export function isCodexFreePlan(providerSpecificData: unknown): boolean { if (!providerSpecificData || typeof providerSpecificData !== "object") return false; - const plan = (providerSpecificData as { workspacePlanType?: unknown }).workspacePlanType; + const data = providerSpecificData as { + workspacePlanType?: unknown; + chatgptPlanType?: unknown; + }; + const plan = + typeof data.workspacePlanType === "string" + ? data.workspacePlanType + : data.chatgptPlanType; return typeof plan === "string" && plan.trim().toLowerCase() === "free"; } diff --git a/open-sse/handlers/imageGeneration.ts b/open-sse/handlers/imageGeneration.ts index 25b7a3482f..2eb758fd41 100644 --- a/open-sse/handlers/imageGeneration.ts +++ b/open-sse/handlers/imageGeneration.ts @@ -14,6 +14,7 @@ import { getAntigravityEnvelopeUserAgent } from "../services/antigravityIdentity import { kieExecutor } from "../executors/kie.ts"; import { mapImageSize } from "../translator/image/sizeMapper.ts"; import { getCodexClientVersion, getCodexUserAgent } from "../config/codexClient.ts"; +import { isCodexFreePlan } from "../executors/codex/tools.ts"; import { saveCallLog } from "@/lib/usageDb"; import { sleep } from "../utils/sleep.ts"; import { @@ -2480,6 +2481,18 @@ async function handleCodexImageGeneration({ }); } + if (isCodexFreePlan(credentials?.providerSpecificData)) { + return saveImageErrorResult({ + provider, + model, + status: 403, + startTime, + error: "Codex image_generation is unavailable on free-plan accounts", + path: logPath, + retryable: true, + }); + } + const workspaceId = credentials?.providerSpecificData && typeof credentials.providerSpecificData === "object" && diff --git a/tests/unit/codex-free-plan-image-generation.test.ts b/tests/unit/codex-free-plan-image-generation.test.ts index 4215da6ac8..fea3afcf59 100644 --- a/tests/unit/codex-free-plan-image-generation.test.ts +++ b/tests/unit/codex-free-plan-image-generation.test.ts @@ -12,6 +12,9 @@ const { normalizeCodexTools, isCodexFreePlan } = await import("../../open-sse/ex test("isCodexFreePlan detects workspacePlanType === 'free' (case-insensitive)", () => { assert.equal(isCodexFreePlan({ workspacePlanType: "free" }), true); assert.equal(isCodexFreePlan({ workspacePlanType: "FREE" }), true); + assert.equal(isCodexFreePlan({ chatgptPlanType: "free" }), true); + assert.equal(isCodexFreePlan({ chatgptPlanType: "FREE" }), true); + assert.equal(isCodexFreePlan({ workspacePlanType: "team", chatgptPlanType: "free" }), false); assert.equal(isCodexFreePlan({ workspacePlanType: "team" }), false); assert.equal(isCodexFreePlan({ workspacePlanType: "" }), false); assert.equal(isCodexFreePlan({}), false); diff --git a/tests/unit/image-generation-handler.test.ts b/tests/unit/image-generation-handler.test.ts index 0caa469564..1d7a18a3e3 100644 --- a/tests/unit/image-generation-handler.test.ts +++ b/tests/unit/image-generation-handler.test.ts @@ -2028,6 +2028,32 @@ test("handleImageGeneration (codex) forwards size and maps GPT-Image quality to } }); +test("handleImageGeneration (codex) skips imported free-plan accounts and marks them retryable", async () => { + const originalFetch = globalThis.fetch; + let fetchCalls = 0; + globalThis.fetch = async () => { + fetchCalls += 1; + throw new Error("free-plan image request should not reach upstream"); + }; + + try { + const result = await handleImageGeneration({ + body: { model: "codex/gpt-5.6-terra", prompt: "kitten" }, + credentials: { + accessToken: "codex-token", + providerSpecificData: { chatgptPlanType: "free" }, + }, + log: null, + }); + assert.equal(result.success, false); + assert.equal(result.status, 403); + assert.equal(result.retryable, true); + assert.equal(fetchCalls, 0); + } finally { + 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