From 2a0d2da6118fcaf8a52920e90d691915554303c0 Mon Sep 17 00:00:00 2001 From: Will Gordon Date: Tue, 4 Aug 2026 11:03:28 -0400 Subject: [PATCH 1/2] fix(sse): restores #5887 openai precedence for bare gpt-5.5 routing #9275 added gpt-5.5 (+ effort variants) to CODEX_NATIVE_UNPREFIXED_MODELS, an unconditional early-return check at the top of resolveModelByProviderInference(). This silently made unreachable the codex-vs-openai precedence logic a few lines below it, still present and still correctly commented, just dead code for gpt-5.5, that issue #5887 built specifically for this model: codex-only installs route to codex, but when openai is also active, the historical openai default wins (tests/unit/codex-gpt55-routing-5887.test.ts). Verified this was a genuine regression, not a deliberate override: - Ran the #5887(b) test against the commit immediately before #9275 merged, and it passed. #9275's own CI run shows it failing, and #9275 merged anyway. - agentrouter's static registry does not catalog gpt-5.5 at all (only gpt-5.6-sol), so the inference-race bug #9275 exists to prevent cannot occur for gpt-5.5. There was no technical reason to add it to the same unconditional set as the gpt-5.6-sol tier. - tests/unit/codex-synced-bare-model-routing.test.ts independently encodes the same openai-wins-when-both-active contract for gpt-5.5 in two more tests, both broken by the same commit. Fix: remove only gpt-5.5 (+ variants) from CODEX_NATIVE_UNPREFIXED_MODELS, restoring its resolution through the existing, unmodified, already-tested precedence logic. gpt-5.6-sol and the other models #9275 actually needed to fix are untouched. Also fixes two pre-existing, unrelated vscode-token-routes.test.ts failures (a #9275 error-message format change, trailing period, that predates this branch and would fail regardless) and rebaselines two inherited, pre-existing file-size drifts (base.ts, chat.ts) from an unrelated already-merged commit (7163081f5), so check:file-size passes cleanly on this branch. --- config/quality/file-size-baseline.json | 7 ++-- open-sse/services/model.ts | 34 +++++++++++------- tests/unit/fix-bare-model-precedence.test.ts | 36 ++++++++++++++------ tests/unit/fix-bare-routing-fallback.test.ts | 18 +++++++--- tests/unit/vscode-token-routes.test.ts | 12 ++++--- 5 files changed, 70 insertions(+), 37 deletions(-) diff --git a/config/quality/file-size-baseline.json b/config/quality/file-size-baseline.json index 925ce4fb744..33f1ac442f1 100644 --- a/config/quality/file-size-baseline.json +++ b/config/quality/file-size-baseline.json @@ -1,4 +1,5 @@ { + "_rebaseline_2026_08_04_5887_gpt55_openai_precedence": "PR fix/codex-gpt55-openai-precedence-5887 own growth: tests/unit/vscode-token-routes.test.ts 1256->1260 (+4, two explanatory comments on the #9275 trailing-period error-message assertions this PR also fixes — see the fix commit itself for that unrelated pre-existing failure). Two further inherited drifts, neither this PR's own growth (this PR's own commits touch neither file for these lines): open-sse/executors/base.ts 1578->1623 (+45, commit 7163081f5 fix(agentrouter): retry on 400 content-blocked + burst guard (#9323), merged directly to release/v3.8.50, grew base.ts without rebaselining) and src/sse/handlers/chat.ts 1845->1846 (+1, same fast-gates-does-not-run-check:file-size root cause documented repeatedly elsewhere in this file). No offending branch left to fix in either inherited case.", "_rebaseline_2026_07_24_8470_hyperagent_sticky_thread": "PR #8470 (artickc, fix/hyperagent-tool-loop-thread-sticky) own growth: open-sse/executors/hyperagent.ts 936->1025 (wc -l; check-file-size.mjs counts via split(\"\\n\").length so the gate sees 937->1026, +89, crosses the 1000 cap). Fixes a real bug where a reverse-conversion proxy (text-Intent/JSON to Claude Code native tool_calls) rewrites assistant messages between agentic tool-loop turns, breaking HyperAgent's conversation-prefix fingerprint and cold-starting the thread mid tool-loop. Adds Anthropic tool_use/tool_result flattening to extractMessageText() plus a new rootUserFingerprint()/root-key lookup tier in resolveHyperAgentThreadBinding()/storeHyperAgentThreadAfterTurn() so the thread stays sticky across the tool loop. Cohesive additions inside the existing single-file executor; not extractable without splitting the executor mid-request-flow. Covered by tests/unit/executor-hyperagent.test.ts (19/19, +5 new cases for tool_result/tool_use flattening + root-key stickiness). Pre-merge review flagged a cross-conversation root-key collision risk (tracked in the PR's own mandatory pre-merge checklist, not yet addressed) — unrelated to this file-size ratchet, tracked separately by /fix-prs.", "_rebaseline_2026_07_25_8494_capability_filter_fail_closed": "PR #8494 (fix/capability-filters-fail-closed, #8488) own growth: open-sse/services/combo.ts 3640->3693 (+53) adds a fail-closed guard after filterTargetsByRequestCompatibility() — when every eligible target is excluded by request-capability filtering (vision/tools/etc) instead of quota/health, the combo now returns an explicit `capability_mismatch` 400 (describeCapabilityFilterExhaustion, imported from combo/comboStructure.ts) rather than silently falling through to a generic no-targets error, plus a `compatFilterFailOpen` escape hatch (combo config OR settings) mirrored at both the main/auto and round-robin call sites for symmetry. combo/comboStructure.ts (previously under cap, un-frozen) grows 794->918 (+124) — new home for describeCapabilityFilterExhaustion + providerSupportsEmulatedToolCalling (#5240 emulated tool-calling exemption so fail-closed does not regress prompt-emulation-only combos like all-chatgpt-web). Irreducible orchestration wiring at the existing filter chokepoint (same precedent as #7301's universal-cooldown-retry generalization). Companion test tests/unit/combo-routing-engine.test.ts 3409->3449 (+40, fail-closed/fail-open coverage across both call sites) also rebaselined. Covered by tests/unit/8488-capability-filter-fail-closed.test.ts (new) + 95/95 passing across both files. Structural shrink of combo.ts tracked in #3501.", "_rebaseline_2026_07_25_8499_ts7_result_union_predicates": "PR #8499 (backryun, chore/ts7-types-executor-scattered) own growth: muse-spark-web.ts 1396->1405 (+9, irreducible). Under this workspace's `strictNullChecks: false`, the boolean-literal discriminant on `GraphqlResult` (`{ ok: true } | { ok: false; error: string }`) narrows the positive `.ok===true` branch but leaves `!result.ok` at the full union under TS7, making `.error` unreachable to the checker at the two call sites (warmup, mode-switch). Fixed by adding a single `isGraphqlFailure()` type-predicate helper (doc comment + 3-line body) reused at both call sites instead of duplicating the predicate inline — not extractable to a shared module without splitting a single-file executor's local narrowing helper out of its own file. Covered by the existing muse-spark-web executor test suite (no behavior change, pure narrowing fix).", @@ -203,7 +204,7 @@ "tests/unit/translator-openai-to-kiro.test.ts": 1250, "tests/unit/translator-resp-gemini-to-openai.test.ts": 1234, "tests/unit/usage-service-hardening.test.ts": 1483, - "tests/unit/vscode-token-routes.test.ts": 1256, + "tests/unit/vscode-token-routes.test.ts": 1260, "tests/unit/executor-antigravity.test.ts": 1098 }, "_rebaseline_2026_06_09": "Re-baseline consciente pre-release v3.8.19: 9 arquivos cresceram durante o ciclo (features mergeadas: RequestLoggerV2 +281 request-logger rework, stream +101, combo +73, chatCore +45, catalog +32 fable-5/catalog-flag, callLogs +4, accountFallback +2, usageHistory novo 840) + core.ts +7 (fix resetAllDbModuleState, PR 3536). A catraca segue valendo destes valores — proximo crescimento falha. Decisao: encolher (esp. RequestLoggerV2/chatCore) e a issue #3501 ficam para o ciclo seguinte.", @@ -342,7 +343,7 @@ "_rebaseline_pr1043_minimax_tts": "Upstream port decolua/9router#1043 (toanalien) own growth: audioSpeech.ts 965->1061 (+96). Adds MiniMax T2A v2 TTS dispatch (handleMinimaxSpeech + hexToBytes helper) — provider entry was already in audioRegistry (format: minimax-tts) but no handler existed, falling through to the OpenAI-compatible default that fails (T2A has custom shape + hex-encoded audio + base_resp envelope). New branch sits next to the other inline provider branches (xiaomi-mimo, coqui, tortoise, aws-polly) — extracting would just create indirection. Covered by tests/unit/minimax-tts-1043.test.ts (3 tests, GREEN: success, base_resp error, invalid-hex).", "_rebaseline_pr4592_exclude_exhausted_auto": "Reconcile #4592 already-merged growth: combo.ts 2991->3036 (+45, terminal-status quota-cutoff exclusion in buildAutoCandidates + opt-in gate). Fast-gate PR->release does not run check:file-size.", "open-sse/executors/antigravity.ts": 1528, - "open-sse/executors/base.ts": 1578, + "open-sse/executors/base.ts": 1623, "open-sse/executors/chatgpt-web.ts": 3241, "open-sse/executors/codex.ts": 1534, "open-sse/executors/cursor.ts": 1560, @@ -400,7 +401,7 @@ "src/shared/components/RequestLoggerV2.tsx": 1629, "src/shared/components/analytics/charts.tsx": 1035, "src/shared/services/cliRuntime.ts": 1122, - "src/sse/handlers/chat.ts": 1845, + "src/sse/handlers/chat.ts": 1846, "src/sse/services/auth.ts": 2508, "tests/unit/account-fallback-service.test.ts": 1572, "tests/unit/provider-validation-specialty.test.ts": 2980, diff --git a/open-sse/services/model.ts b/open-sse/services/model.ts index 59ba39d2530..df9cf00f278 100644 --- a/open-sse/services/model.ts +++ b/open-sse/services/model.ts @@ -122,13 +122,24 @@ for (const [aliasOrId, models] of Object.entries(PROVIDER_MODELS)) { const KNOWN_MODEL_IDS = new Set(MODEL_TO_PROVIDERS.keys()); // Bare Codex CLI defaults must always route to the `codex` provider (chatgpt.com // OAuth) even when other providers that also catalog the model id (e.g. -// `agentrouter`, `openai`) are active. The Codex cookie quota on the user's -// account is the source of truth for capacity, and bare-id requests from -// `codex` (CLI)/`Codex` (web) would otherwise silently fan out to whichever -// provider won the inference race — leaving the user wondering why the -// canonical ChatGPT subscription stopped working. Override per-request by -// prefixing the model id (e.g. `agentrouter/gpt-5.6-sol`, -// `openai/gpt-5.6-sol`) — the prefix path always wins. +// `agentrouter`) are active. The Codex cookie quota on the user's account is +// the source of truth for capacity, and bare-id requests from `codex` +// (CLI)/`Codex` (web) would otherwise silently fan out to whichever provider +// won the inference race — leaving the user wondering why the canonical +// ChatGPT subscription stopped working. Override per-request by prefixing the +// model id (e.g. `agentrouter/gpt-5.6-sol`, `openai/gpt-5.6-sol`) — the prefix +// path always wins. +// +// gpt-5.5 (+ effort variants) is deliberately NOT in this set (#9323 added it, +// #5887-openai-precedence-regression removed it). It has no `agentrouter` +// static-catalog entry, so it was never exposed to the inference-race bug +// this set exists to prevent — but it IS cataloged by `openai`, and +// resolveModelByProviderInference() below already has a dedicated, tested +// codex-vs-openai precedence rule for it (issue #5887: codex-only installs +// route to codex; when openai is ALSO active, the historical openai default +// wins). Putting gpt-5.5 in this unconditional set short-circuits that rule +// before it ever runs, silently reverting #5887 for any user who has both +// providers configured (tests/unit/codex-gpt55-routing-5887.test.ts). export const CODEX_NATIVE_UNPREFIXED_MODELS = new Set([ "codex-auto-review", "gpt-5.6-sol", @@ -151,11 +162,6 @@ export const CODEX_NATIVE_UNPREFIXED_MODELS = new Set([ "gpt-5.6-luna-high", "gpt-5.6-luna-medium", "gpt-5.6-luna-low", - "gpt-5.5", - "gpt-5.5-xhigh", - "gpt-5.5-high", - "gpt-5.5-medium", - "gpt-5.5-low", "gpt-5.3-codex-spark", ]); @@ -649,7 +655,9 @@ async function resolveModelByProviderInference(modelId: string, extendedContext: // Canonicalize candidates (deduplicate alias providers pointing to the same provider ID) const canonicalCandidates = Array.from( - new Set(candidatesToUse.map((p) => resolveProviderAlias(p)).filter((p): p is string => p !== null)) + new Set( + candidatesToUse.map((p) => resolveProviderAlias(p)).filter((p): p is string => p !== null) + ) ); // Filter candidates by active connections configured in the database diff --git a/tests/unit/fix-bare-model-precedence.test.ts b/tests/unit/fix-bare-model-precedence.test.ts index 6a963ddf977..bfe1614e98a 100644 --- a/tests/unit/fix-bare-model-precedence.test.ts +++ b/tests/unit/fix-bare-model-precedence.test.ts @@ -1,10 +1,7 @@ import test from "node:test"; import assert from "node:assert/strict"; -import { - CODEX_NATIVE_UNPREFIXED_MODELS, - getModelInfoCore, -} from "../../open-sse/services/model.ts"; +import { CODEX_NATIVE_UNPREFIXED_MODELS, getModelInfoCore } from "../../open-sse/services/model.ts"; // #FIX: bare Codex-default model ids must always route to the `codex` // provider (chatgpt.com OAuth) when no provider prefix is supplied, even @@ -24,10 +21,6 @@ test("CODEX_NATIVE_UNPREFIXED_MODELS includes gpt-5.6-sol tier set", () => { "gpt-5.6-terra-xhigh", "gpt-5.6-luna", "gpt-5.6-luna-xhigh", - "gpt-5.5", - "gpt-5.5-xhigh", - "gpt-5.5-medium", - "gpt-5.5-low", "gpt-5.3-codex-spark", "codex-auto-review", ]) { @@ -39,15 +32,36 @@ test("CODEX_NATIVE_UNPREFIXED_MODELS includes gpt-5.6-sol tier set", () => { } }); +// #5887-openai-precedence-regression: gpt-5.5 (+ effort variants) must NOT be +// in this unconditional set. Unlike gpt-5.6-sol, it has no `agentrouter` +// static-catalog entry, so it was never exposed to the inference-race bug +// this set exists to prevent — but resolveModelByProviderInference() has a +// dedicated, tested codex-vs-openai precedence rule for it (issue #5887) that +// this set would silently short-circuit. See +// tests/unit/codex-gpt55-routing-5887.test.ts for the precedence behavior. +test("CODEX_NATIVE_UNPREFIXED_MODELS excludes gpt-5.5 (preserves #5887 openai precedence)", () => { + for (const id of ["gpt-5.5", "gpt-5.5-xhigh", "gpt-5.5-high", "gpt-5.5-medium", "gpt-5.5-low"]) { + assert.equal( + CODEX_NATIVE_UNPREFIXED_MODELS.has(id), + false, + `expected CODEX_NATIVE_UNPREFIXED_MODELS to exclude ${id}` + ); + } +}); + test("bare gpt-5.6-sol resolves to codex (provider native prefix wins)", async () => { const info = await getModelInfoCore("gpt-5.6-sol", null); assert.equal(info.provider, "codex", "bare gpt-5.6-sol must route to codex"); assert.equal(info.model, "gpt-5.6-sol"); }); -test("bare gpt-5.5 resolves to codex", async () => { +test("bare gpt-5.5 resolves to openai when no provider connections are active", async () => { + // No connections seeded in this file: falls through to the historical + // openai-static-catalog default (see #5887(c) for the analogous, DB-backed + // "openai active, gpt-4o" case). Codex-only and both-active scenarios are + // covered by tests/unit/codex-gpt55-routing-5887.test.ts. const info = await getModelInfoCore("gpt-5.5", null); - assert.equal(info.provider, "codex"); + assert.equal(info.provider, "openai"); assert.equal(info.model, "gpt-5.5"); }); @@ -75,4 +89,4 @@ test("codex-auto-review remains in the precedence set (regression guard)", async assert.equal(CODEX_NATIVE_UNPREFIXED_MODELS.has("codex-auto-review"), true); const info = await getModelInfoCore("codex-auto-review", null); assert.equal(info.provider, "codex"); -}); \ No newline at end of file +}); diff --git a/tests/unit/fix-bare-routing-fallback.test.ts b/tests/unit/fix-bare-routing-fallback.test.ts index 865ac2feb16..141fdb313ba 100644 --- a/tests/unit/fix-bare-routing-fallback.test.ts +++ b/tests/unit/fix-bare-routing-fallback.test.ts @@ -5,8 +5,12 @@ import { getModelInfoCore } from "../../open-sse/services/model.ts"; // #FIX: end-to-end precedence checks for bare model routing. These guard // the contract that: -// - Bare Codex-default model ids (gpt-5.6-sol, gpt-5.5, etc.) ALWAYS route -// to `codex`, regardless of which other providers are also active. +// - Bare Codex-default model ids (gpt-5.6-sol, etc. — see +// CODEX_NATIVE_UNPREFIXED_MODELS) ALWAYS route to `codex`, regardless of +// which other providers are also active. gpt-5.5 is the one exception: +// #5887-openai-precedence-regression removed it from that set, since it +// has its own dedicated, tested codex-vs-openai precedence rule (issue +// #5887) instead — see tests/unit/codex-gpt55-routing-5887.test.ts. // - Bare model ids shared between providers (e.g. claude-opus-5 across // anthropic/claude/github/agentrouter/etc.) never silently route to a // provider whose static registry does NOT actually catalog them (the @@ -22,9 +26,13 @@ test("bare gpt-5.6-sol routes to codex (precedence via CODEX_NATIVE_UNPREFIXED_M ); }); -test("bare gpt-5.5 routes to codex", async () => { +test("bare gpt-5.5 routes to openai when no provider connections are active", async () => { + // gpt-5.5 is deliberately excluded from CODEX_NATIVE_UNPREFIXED_MODELS + // (#5887-openai-precedence-regression) — see + // tests/unit/codex-gpt55-routing-5887.test.ts for the codex-vs-openai + // precedence contract this preserves. const info = await getModelInfoCore("gpt-5.5", null); - assert.equal(info.provider, "codex"); + assert.equal(info.provider, "openai"); }); test("bare gpt-5.6-sol-xhigh (a tier id) routes to codex", async () => { @@ -59,4 +67,4 @@ test("bare claude-opus-5 never resolves to kiro (synced-catalog validation)", as test("bare claude-opus-4-8 also never resolves to kiro (same fix must apply to all shared models)", async () => { const info = await getModelInfoCore("claude-opus-4-8", null); assert.notEqual(info.provider, "kiro"); -}); \ No newline at end of file +}); diff --git a/tests/unit/vscode-token-routes.test.ts b/tests/unit/vscode-token-routes.test.ts index 2c05c97076e..5973b34b350 100644 --- a/tests/unit/vscode-token-routes.test.ts +++ b/tests/unit/vscode-token-routes.test.ts @@ -767,9 +767,7 @@ test("vscode tokenized tags route only exposes usable canonical chat models", as ); assert.ok( !catalogModel.api_format || - ["chat-completions", "responses", "openai-responses"].includes( - catalogModel.api_format - ), + ["chat-completions", "responses", "openai-responses"].includes(catalogModel.api_format), `tag ${tagModel.name} should use a text-generation API format` ); assert.ok( @@ -1161,7 +1159,9 @@ test("vscode tokenized /chat/completions route applies the path token and codex // error code mapping is "model_not_found" (open-sse/config/errorConfig.ts:29). assert.equal(response.status, 404); assert.equal(body.error?.code, "model_not_found"); - assert.equal(body.error?.message, "No active credentials for provider: codex"); + // #9275: handleNoCredentials now always appends a trailing "." (plus an + // optional candidate-alias hint, empty here) after the provider name. + assert.equal(body.error?.message, "No active credentials for provider: codex."); }); test("vscode tokenized /responses route applies the path token and codex tier rewrite", async () => { @@ -1192,7 +1192,9 @@ test("vscode tokenized /responses route applies the path token and codex tier re // Upstream port decolua/9router#336: see chat/completions sibling test above. assert.equal(response.status, 404); assert.equal(body.error?.code, "model_not_found"); - assert.equal(body.error?.message, "No active credentials for provider: codex"); + // #9275: handleNoCredentials now always appends a trailing "." (plus an + // optional candidate-alias hint, empty here) after the provider name. + assert.equal(body.error?.message, "No active credentials for provider: codex."); }); test("vscode tokenized api/show route preserves the selected reasoning effort for codex variants", async () => { From 46da0aabc7fb2869b05130869d08b51beb15c0b2 Mon Sep 17 00:00:00 2001 From: Will Gordon Date: Tue, 4 Aug 2026 12:26:25 -0400 Subject: [PATCH 2/2] refactor(sse): makes the gpt-5.5 openai-precedence exception explicit Previous commit fixed the regression by simply omitting gpt-5.5 from CODEX_NATIVE_UNPREFIXED_MODELS, relying on fallthrough to the pre-existing precedence logic below. That's structurally the same failure mode that caused the original bug: a check "just happens" not to apply to a given model id, with correctness depending on nobody re-adding it later without reading a comment 20 lines away from the call site (exactly what happened once already, in #9275). Adds a second, explicitly-named set, CODEX_NATIVE_MODELS_WITH_OPENAI_PRECEDENCE, and checks it directly at the call site: `CODEX_NATIVE_UNPREFIXED_MODELS.has(modelId) && !CODEX_NATIVE_MODELS_WITH_OPENAI_PRECEDENCE.has(modelId)`. A future engineer reading this exact line sees the carve-out and why it exists, instead of needing to notice gpt-5.5's absence from a list defined well above it. Test updated to assert membership in the new set directly, rather than only asserting absence from the old one. --- open-sse/services/model.ts | 41 ++++++++++++++------ tests/unit/fix-bare-model-precedence.test.ts | 29 +++++++++----- 2 files changed, 49 insertions(+), 21 deletions(-) diff --git a/open-sse/services/model.ts b/open-sse/services/model.ts index df9cf00f278..13afeb161bd 100644 --- a/open-sse/services/model.ts +++ b/open-sse/services/model.ts @@ -129,17 +129,6 @@ const KNOWN_MODEL_IDS = new Set(MODEL_TO_PROVIDERS.keys()); // ChatGPT subscription stopped working. Override per-request by prefixing the // model id (e.g. `agentrouter/gpt-5.6-sol`, `openai/gpt-5.6-sol`) — the prefix // path always wins. -// -// gpt-5.5 (+ effort variants) is deliberately NOT in this set (#9323 added it, -// #5887-openai-precedence-regression removed it). It has no `agentrouter` -// static-catalog entry, so it was never exposed to the inference-race bug -// this set exists to prevent — but it IS cataloged by `openai`, and -// resolveModelByProviderInference() below already has a dedicated, tested -// codex-vs-openai precedence rule for it (issue #5887: codex-only installs -// route to codex; when openai is ALSO active, the historical openai default -// wins). Putting gpt-5.5 in this unconditional set short-circuits that rule -// before it ever runs, silently reverting #5887 for any user who has both -// providers configured (tests/unit/codex-gpt55-routing-5887.test.ts). export const CODEX_NATIVE_UNPREFIXED_MODELS = new Set([ "codex-auto-review", "gpt-5.6-sol", @@ -165,6 +154,28 @@ export const CODEX_NATIVE_UNPREFIXED_MODELS = new Set([ "gpt-5.3-codex-spark", ]); +// EXCEPTION to CODEX_NATIVE_UNPREFIXED_MODELS, enforced explicitly at its call +// site below rather than by omission — a future edit that "helpfully" adds +// gpt-5.5 back to that set (as #9323/#9275 did) must touch this comment and +// this check, not just silently forget an entry exists. gpt-5.5 (+ effort +// variants) has no `agentrouter` static-catalog entry (verified in +// open-sse/config/providers/registry/agentrouter/index.ts), so the +// inference-race bug CODEX_NATIVE_UNPREFIXED_MODELS exists to prevent cannot +// occur for it — but it IS cataloged by `openai`, and +// resolveModelByProviderInference() below has its own dedicated, tested +// codex-vs-openai precedence rule for exactly this model (issue #5887: +// codex-only installs route to codex; when openai is ALSO active, the +// historical openai default wins). Unconditionally forcing codex here would +// skip that rule before it ever runs, reverting #5887 for any user who has +// both providers configured (tests/unit/codex-gpt55-routing-5887.test.ts). +export const CODEX_NATIVE_MODELS_WITH_OPENAI_PRECEDENCE = new Set([ + "gpt-5.5", + "gpt-5.5-xhigh", + "gpt-5.5-high", + "gpt-5.5-medium", + "gpt-5.5-low", +]); + interface ProviderConnectionLike { provider?: unknown; isActive?: unknown; @@ -563,7 +574,13 @@ function parseAliasTarget(target: string): ResolvedModelTarget | null { } async function resolveModelByProviderInference(modelId: string, extendedContext: boolean) { - if (CODEX_NATIVE_UNPREFIXED_MODELS.has(modelId)) { + // See CODEX_NATIVE_MODELS_WITH_OPENAI_PRECEDENCE above: gpt-5.5 stays out of + // this unconditional codex override on purpose, so its own + // codex-vs-openai precedence rule (issue #5887, below) still runs. + if ( + CODEX_NATIVE_UNPREFIXED_MODELS.has(modelId) && + !CODEX_NATIVE_MODELS_WITH_OPENAI_PRECEDENCE.has(modelId) + ) { return { provider: "codex", model: modelId, diff --git a/tests/unit/fix-bare-model-precedence.test.ts b/tests/unit/fix-bare-model-precedence.test.ts index bfe1614e98a..bff90674caa 100644 --- a/tests/unit/fix-bare-model-precedence.test.ts +++ b/tests/unit/fix-bare-model-precedence.test.ts @@ -1,7 +1,11 @@ import test from "node:test"; import assert from "node:assert/strict"; -import { CODEX_NATIVE_UNPREFIXED_MODELS, getModelInfoCore } from "../../open-sse/services/model.ts"; +import { + CODEX_NATIVE_UNPREFIXED_MODELS, + CODEX_NATIVE_MODELS_WITH_OPENAI_PRECEDENCE, + getModelInfoCore, +} from "../../open-sse/services/model.ts"; // #FIX: bare Codex-default model ids must always route to the `codex` // provider (chatgpt.com OAuth) when no provider prefix is supplied, even @@ -32,15 +36,22 @@ test("CODEX_NATIVE_UNPREFIXED_MODELS includes gpt-5.6-sol tier set", () => { } }); -// #5887-openai-precedence-regression: gpt-5.5 (+ effort variants) must NOT be -// in this unconditional set. Unlike gpt-5.6-sol, it has no `agentrouter` -// static-catalog entry, so it was never exposed to the inference-race bug -// this set exists to prevent — but resolveModelByProviderInference() has a -// dedicated, tested codex-vs-openai precedence rule for it (issue #5887) that -// this set would silently short-circuit. See -// tests/unit/codex-gpt55-routing-5887.test.ts for the precedence behavior. -test("CODEX_NATIVE_UNPREFIXED_MODELS excludes gpt-5.5 (preserves #5887 openai precedence)", () => { +// #5887-openai-precedence-regression: gpt-5.5 (+ effort variants) must be in +// CODEX_NATIVE_MODELS_WITH_OPENAI_PRECEDENCE, and NOT unconditionally forced +// to codex by CODEX_NATIVE_UNPREFIXED_MODELS. Unlike gpt-5.6-sol, it has no +// `agentrouter` static-catalog entry, so it was never exposed to the +// inference-race bug the unconditional set exists to prevent — but +// resolveModelByProviderInference() has a dedicated, tested codex-vs-openai +// precedence rule for it (issue #5887) that the unconditional set would +// silently short-circuit. See tests/unit/codex-gpt55-routing-5887.test.ts for +// the precedence behavior. +test("gpt-5.5 is in the explicit openai-precedence exception, not the unconditional codex set", () => { for (const id of ["gpt-5.5", "gpt-5.5-xhigh", "gpt-5.5-high", "gpt-5.5-medium", "gpt-5.5-low"]) { + assert.equal( + CODEX_NATIVE_MODELS_WITH_OPENAI_PRECEDENCE.has(id), + true, + `expected CODEX_NATIVE_MODELS_WITH_OPENAI_PRECEDENCE to include ${id}` + ); assert.equal( CODEX_NATIVE_UNPREFIXED_MODELS.has(id), false,