From 39880484c737a54fa54adf90d7adb89fc0b7fbb4 Mon Sep 17 00:00:00 2001 From: Diego Rodrigues de Sa e Souza Date: Thu, 9 Jul 2026 04:51:37 -0300 Subject: [PATCH] fix(providers): drop image_generation for Codex Spark models regardless of plan (#6651) --- CHANGELOG.md | 1 + open-sse/executors/codex.ts | 6 +- .../unit/codex-spark-image-generation.test.ts | 61 +++++++++++++++++++ 3 files changed, 67 insertions(+), 1 deletion(-) create mode 100644 tests/unit/codex-spark-image-generation.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 2bba532ec2..a2e9d9de02 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ _Living section — bullets land here as PRs merge into `release/v3.8.47` (paral ### 🐛 Bug Fixes +- **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`. - **fix(cli):** per-agent AgentBridge DNS toggle was broken for 8 of the 9 supported agents, and a failed MITM startup step could orphan the spawned proxy child — `addDNSEntry`/`removeDNSEntry` (`src/mitm/dns/dnsConfig.ts`) always resolved the legacy Antigravity default hosts regardless of which agent's toggle was flipped, so enabling DNS for Cursor/Codex/Claude Code/etc. silently added only `daily-cloudcode-pa.googleapis.com` while the DB recorded `dns_enabled=true` for the selected agent. Both functions now accept an optional `agentId` and resolve hosts via `ALL_TARGETS`; `POST /api/tools/agent-bridge/agents/[id]/dns` passes the route's `id` through and now returns 404 for an id that doesn't match a known target instead of silently falling back. Separately, `startMitmInternal()` (`src/mitm/manager.ts`) now wraps `generateCert()` (log + rethrow), the `provisionDnsEntries()` call, and the PID-file write in try/catch so a mid-startup failure can't orphan the already-spawned MITM child process. On Windows, `addDNSEntries`/`removeDNSEntries` also batch every missing/present entry into a single elevated PowerShell invocation instead of one UAC prompt per host line. Regression guard: `tests/unit/dns-config-generic.test.ts` (agent-specific resolution + batching), `tests/unit/agent-bridge-dns-route-validation.test.ts` (404 for unknown agent id). ([#6338](https://github.com/diegosouzapw/OmniRoute/pull/6338) — thanks @hamsa0x7) - **fix(guardrails):** Vision Bridge's individual-model auto-reroute (route an image-bearing request straight to a vision-capable model instead of describe-then-forward) could bypass a policy-restricted API key's model allowlist/budget ([#6640](https://github.com/diegosouzapw/OmniRoute/pull/6640)) — `VisionBridgeGuardrail.preCall()` (`src/lib/guardrails/visionBridge.ts`) swaps `body.model` to the best available vision-capable model, but that swap happens in the guardrail pipeline AFTER `chat.ts` already called `enforceApiKeyPolicy()` against the ORIGINAL model, so a key scoped to a narrow `allowedModels` list could still execute against an unvetted (and possibly costlier) vision model the reroute picked. `chat.ts` now re-validates any guardrail-driven model change against the same per-key allowlist (`isModelAllowedForKey`) before honoring it, falling back to the original already-approved model when the reroute target is not allowed. The reroute path also now honors an explicit `settings.visionBridgeModel` operator override (previously ignored, unlike the combo/describe path a few lines below it, which already respects it via `getVisionBridgeConfig`). Regression guard: `tests/unit/guardrails/visionBridge.test.ts` (22 tests). (thanks @herjarsa) - **fix(auth):** an API key restricted via `allowedModels`/`allowedCombos` could bypass that restriction entirely over the Codex Responses-over-WebSocket bridge ([#6564](https://github.com/diegosouzapw/OmniRoute/issues/6564)) — `prepare()` in `src/app/api/internal/codex-responses-ws/route.ts` authenticated the WS bridge's API key (`authenticate()`/`authorizeWebSocketHandshake()`) and honored `allowedConnections`, but never called `enforceApiKeyPolicy()`, the same model/combo policy gate the HTTP `/v1/responses` path enforces via `handleChat()` — so a key scoped to e.g. `combo/model-1.0` could still reach a direct Codex model like `gpt-5.5` through this transport, as long as an eligible Codex OAuth connection existed. The bridge's WS auth token arrives via query params (`api_key`/`token`/`access_token`), not a normal `Authorization` header, so a new `enforceCodexWsApiKeyPolicy()` builds an equivalent `Request` carrying an explicit `Authorization: Bearer ` header and calls `enforceApiKeyPolicy()` against the CLIENT-requested model, before any Codex-specific model remapping or credential selection. Regression guard: `tests/unit/codex-ws-policy-enforcement-6564.test.ts` (a model-restricted key is rejected 403 before reaching credential selection; a combo-restricted key is rejected 403 requesting a disallowed combo; a key that DOES allow the requested model still proceeds past policy). diff --git a/open-sse/executors/codex.ts b/open-sse/executors/codex.ts index 6bca470eb5..8ad0a2fbb5 100644 --- a/open-sse/executors/codex.ts +++ b/open-sse/executors/codex.ts @@ -1129,7 +1129,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" + ); +});