diff --git a/changelog.d/fixes/6721-codex-spark-image-drop.md b/changelog.d/fixes/6721-codex-spark-image-drop.md new file mode 100644 index 0000000000..06cc548828 --- /dev/null +++ b/changelog.d/fixes/6721-codex-spark-image-drop.md @@ -0,0 +1 @@ +- **fix(providers):** Codex Desktop requests to `gpt-5.3-codex-spark` failed with `[400]: Tool 'image_generation' is not supported with gpt-5.3-codex-spark`, even on paid-plan accounts ([#6651](https://github.com/diegosouzapw/OmniRoute/issues/6651)) — `CodexExecutor.transformRequest` (`open-sse/executors/codex.ts`) only dropped the Codex Desktop-injected `image_generation` hosted tool when `isCodexFreePlan()` matched the account's plan, with no awareness that Spark-scope Codex models reject `image_generation` upstream regardless of plan. `dropImageGeneration` now also drops it when `getCodexModelScope(model) === "spark"` (the existing Spark classifier from `open-sse/config/codexQuotaScopes.ts`), independent of account plan. Regression guard: `tests/unit/codex-spark-image-generation.test.ts`. diff --git a/open-sse/executors/codex.ts b/open-sse/executors/codex.ts index ceaa8bda69..2fd66b2343 100644 --- a/open-sse/executors/codex.ts +++ b/open-sse/executors/codex.ts @@ -1132,7 +1132,11 @@ export class CodexExecutor extends BaseExecutor { // Cursor may include custom tools (e.g. ApplyPatch) that work locally but are // invalid upstream, and translation bugs can leave orphaned/empty tool_choice names. normalizeCodexTools(body, { - dropImageGeneration: isCodexFreePlan(credentials?.providerSpecificData), + // gpt-5.3-codex-spark (and other Spark-scope models) reject image_generation + // upstream even on paid-plan accounts, so drop it independent of plan (#6651). + dropImageGeneration: + isCodexFreePlan(credentials?.providerSpecificData) || + getCodexModelScope(model) === "spark", preserveCustomTools: nativeCodexPassthrough, }); diff --git a/tests/unit/codex-spark-image-generation.test.ts b/tests/unit/codex-spark-image-generation.test.ts new file mode 100644 index 0000000000..7d2671b866 --- /dev/null +++ b/tests/unit/codex-spark-image-generation.test.ts @@ -0,0 +1,61 @@ +/** + * #6651 — Codex Desktop injects the `image_generation` hosted tool into every + * Responses API request. OmniRoute only dropped it for free-plan Codex + * accounts (isCodexFreePlan). It did NOT drop it for gpt-5.3-codex-spark (and + * other Spark-scope models), which reject `image_generation` upstream even on + * paid-plan accounts, producing: + * [400]: Tool 'image_generation' is not supported with gpt-5.3-codex-spark. + * + * Fix: CodexExecutor.transformRequest now also drops image_generation when + * the target model resolves to the Spark quota scope + * (getCodexModelScope(model) === "spark"), independent of plan. + */ +import test from "node:test"; +import assert from "node:assert/strict"; + +const { CodexExecutor } = await import("../../open-sse/executors/codex.ts"); + +function buildBody() { + return { + _nativeCodexPassthrough: true, + tools: [ + { type: "image_generation", output_format: "png" }, + { type: "function", name: "foo", parameters: { type: "object" } }, + ], + }; +} + +test("#6651: CodexExecutor.transformRequest drops image_generation for gpt-5.3-codex-spark even on a paid-plan account", () => { + const executor = new CodexExecutor(); + + // Paid-plan account (not free) — isCodexFreePlan() alone returns false, so + // the fix must rely on the model-scope check to still drop the tool. + const result = executor.transformRequest("gpt-5.3-codex-spark", buildBody(), false, { + providerSpecificData: { workspacePlanType: "team" }, + }) as { tools: Array<{ type?: string }> }; + + assert.equal( + result.tools.some((t) => t.type === "image_generation"), + false, + "image_generation must be dropped for gpt-5.3-codex-spark regardless of account plan (#6651)" + ); + assert.equal( + result.tools.some((t) => t.type === "function"), + true, + "the function tool must survive" + ); +}); + +test("#6651: CodexExecutor.transformRequest still preserves image_generation for non-Spark models on paid plans", () => { + const executor = new CodexExecutor(); + + const result = executor.transformRequest("gpt-5", buildBody(), false, { + providerSpecificData: { workspacePlanType: "team" }, + }) as { tools: Array<{ type?: string }> }; + + assert.equal( + result.tools.some((t) => t.type === "image_generation"), + true, + "image_generation must still be preserved for non-Spark models on paid plans" + ); +});