mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-05 14:52:09 +03:00
fix(routing): only let Codex-native bare ids preempt a provider when codex is active (#9447)
* fix(routing): only let Codex-native bare ids preempt a provider when codex is active #9275 widened CODEX_NATIVE_UNPREFIXED_MODELS from a single id to gpt-5.5 plus the gpt-5.6-sol/terra/luna tiers, so bare Codex CLI ids would reach the ChatGPT subscription instead of fanning out to whichever provider won the inference race. The early return it added never consulted the active-provider set, which made the codex-only guard 30 lines below unreachable for every id in the set: if (CODEX_NATIVE_UNPREFIXED_MODELS.has(modelId)) return { provider: "codex", ... } An OpenAI-only install therefore had bare gpt-5.5 routed to codex and failed with 'no active credentials for provider: codex' on a model OpenAI serves, and an install whose codex connection was merely inactive failed identically. This also silently reverted #5887's compatibility boundary. The preference now only PREEMPTS another provider when a codex connection is active. Ids that no other provider catalogs (codex-auto-review) still resolve to codex with no connection at all — there is nothing to preempt and 'no codex credentials' is the honest error. With codex active the preference still beats OpenAI, which is the point of #9275, and an explicit openai/ prefix overrides it either way. Tests: the three assertions that encode the intended #9275 change now expect codex (plus a new one pinning the explicit-prefix override); the rest were already correct and pass again untouched. Adds a regression test for the OpenAI-only case. * docs(changelog): correct fragment id to #9447 * test(routing): seed an active codex connection in the bare-precedence guards The two files #9275 added assert that bare gpt-5.5 / gpt-5.6-sol reach codex, but they ran against an empty database — so they also pinned 'codex wins with no codex connection at all', which is the regression #9447 removes. That put them in direct contradiction with plan3-p0 / chat-helpers / codex-gpt55-routing-5887, which assert openai for the very same input: no implementation could satisfy both, which is why the release could not go green. Seeding an active codex connection keeps the contract these files were written to guard (codex beats openai for a Codex-native bare id) while dropping the accidental 'even with no codex configured' half. Cases that need no connection are left as they were: the tier-only ids and codex-auto-review have no alternative provider to preempt, and the explicit-prefix overrides are unaffected. --------- Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
16ed707148
commit
4a3dcf6b0b
1
changelog.d/fixes/9447-bare-model-codex-preemption.md
Normal file
1
changelog.d/fixes/9447-bare-model-codex-preemption.md
Normal file
@@ -0,0 +1 @@
|
||||
- **fix(routing):** a Codex-native bare model id (`gpt-5.5`, the `gpt-5.6-sol`/`terra`/`luna` tiers) no longer routes to `codex` when no codex connection is active — an OpenAI-only install was getting `no active credentials for provider: codex` for a model OpenAI serves, and an install whose codex connection was merely inactive failed the same way. With codex active the Codex preference still wins over OpenAI, and ids only codex catalogs (`codex-auto-review`) still resolve to codex with no connection at all ([#9447](https://github.com/diegosouzapw/OmniRoute/pull/9447))
|
||||
@@ -557,20 +557,36 @@ function parseAliasTarget(target: string): ResolvedModelTarget | null {
|
||||
}
|
||||
|
||||
async function resolveModelByProviderInference(modelId: string, extendedContext: boolean) {
|
||||
if (CODEX_NATIVE_UNPREFIXED_MODELS.has(modelId)) {
|
||||
return {
|
||||
provider: "codex",
|
||||
model: modelId,
|
||||
extendedContext,
|
||||
};
|
||||
}
|
||||
|
||||
const [activeProviders, activeSyncedProviders, preferClaudeCodeForUnprefixedClaudeModels] =
|
||||
await Promise.all([
|
||||
getActiveProviderSet(),
|
||||
getActiveSyncedProvidersForModel(modelId),
|
||||
getPreferClaudeCodeForUnprefixedClaudeModels(),
|
||||
]);
|
||||
|
||||
// Codex-native bare ids prefer the ChatGPT subscription, but the preference is only
|
||||
// allowed to PREEMPT another provider when a codex connection is actually active.
|
||||
// Returning "codex" unconditionally (as this did once the set grew past
|
||||
// `codex-auto-review` to cover gpt-5.5 / the gpt-5.6-sol tiers) hands ids that OpenAI
|
||||
// also serves to a provider the operator may not have configured: an OpenAI-only
|
||||
// install fails with "no active credentials for provider: codex" on a model that
|
||||
// works, and an install whose codex connection is merely *inactive* fails the same way.
|
||||
// Ids only codex catalogs (e.g. `codex-auto-review`) keep resolving to codex with no
|
||||
// connection at all — there is no alternative to preempt, and "no codex credentials"
|
||||
// is the honest error. With codex active the preference still beats OpenAI, and an
|
||||
// explicit `openai/…` prefix remains the per-request override either way.
|
||||
if (CODEX_NATIVE_UNPREFIXED_MODELS.has(modelId)) {
|
||||
const codexNativeAlternatives = (MODEL_TO_PROVIDERS.get(modelId) || []).filter(
|
||||
(p) => p !== "codex"
|
||||
);
|
||||
if (codexNativeAlternatives.length === 0 || activeProviders?.has("codex")) {
|
||||
return {
|
||||
provider: "codex",
|
||||
model: modelId,
|
||||
extendedContext,
|
||||
};
|
||||
}
|
||||
}
|
||||
// #FIX: synced catalogs (populated from `/v1/models` per connection) can
|
||||
// claim ownership of models the provider does not actually serve (e.g. a
|
||||
// `kiro` upstream briefly advertising `claude-opus-5` before it was
|
||||
|
||||
@@ -50,8 +50,13 @@ test("#5887(a) codex-only setup infers codex for unprefixed gpt-5.5", async () =
|
||||
assert.equal(info.model, "gpt-5.5", "codex inference keeps the bare gpt-5.5 id");
|
||||
});
|
||||
|
||||
// (b) Codex + OpenAI active → preserve the historical OpenAI default.
|
||||
test("#5887(b) active Codex and OpenAI connections keep gpt-5.5 on OpenAI", async () => {
|
||||
// (b) Codex + OpenAI active → Codex wins for a Codex-native bare id.
|
||||
// Reversed by #9275: `gpt-5.5` joined CODEX_NATIVE_UNPREFIXED_MODELS, so the
|
||||
// ChatGPT subscription is now the deliberate destination for bare Codex CLI ids
|
||||
// even with OpenAI active. The compatibility boundary this file documented moved
|
||||
// from "OpenAI wins the overlap" to "an explicit prefix wins the overlap" —
|
||||
// asserted in (b2) below so the override is not silently lost.
|
||||
test("#5887(b) active Codex and OpenAI connections route bare gpt-5.5 to Codex", async () => {
|
||||
const conn = await providersDb.createProviderConnection({
|
||||
provider: "openai",
|
||||
authType: "apikey",
|
||||
@@ -60,7 +65,18 @@ test("#5887(b) active Codex and OpenAI connections keep gpt-5.5 on OpenAI", asyn
|
||||
openaiConnectionId = (conn as { id?: number | string })?.id;
|
||||
|
||||
const info = await getModelInfoCore("gpt-5.5", null);
|
||||
assert.equal(info.provider, "openai", "OpenAI remains default when both providers are active");
|
||||
assert.equal(
|
||||
info.provider,
|
||||
"codex",
|
||||
"bare gpt-5.5 prefers the Codex subscription once both providers are active (#9275)"
|
||||
);
|
||||
assert.equal(info.model, "gpt-5.5");
|
||||
});
|
||||
|
||||
// (b2) …but the explicit prefix stays authoritative — the documented escape hatch.
|
||||
test("#5887(b2) an explicit openai/ prefix still overrides the Codex preference", async () => {
|
||||
const info = await getModelInfoCore("openai/gpt-5.5", null);
|
||||
assert.equal(info.provider, "openai", "explicit provider prefix beats the Codex-native set");
|
||||
assert.equal(info.model, "gpt-5.5");
|
||||
});
|
||||
|
||||
|
||||
@@ -64,13 +64,16 @@ test("bare GPT-5.6 model routes through Codex when it is the only active provide
|
||||
assert.equal(info.model, GPT_56_CODEX_MODEL);
|
||||
});
|
||||
|
||||
test("OpenAI remains the historical default when both providers advertise the bare model", async () => {
|
||||
// #9275 put the whole gpt-5.6-sol tier set into CODEX_NATIVE_UNPREFIXED_MODELS, so an
|
||||
// active Codex connection now claims the bare id ahead of OpenAI. Before, OpenAI won the
|
||||
// overlap; the escape hatch is the explicit prefix, covered by the last test in this file.
|
||||
test("Codex claims the bare model when both providers advertise it", async () => {
|
||||
await seedSyncedModel("codex", GPT_56_CODEX_MODEL);
|
||||
await seedSyncedModel("openai", GPT_56_CODEX_MODEL);
|
||||
|
||||
const info = await getModelInfoCore(GPT_56_CODEX_MODEL, null);
|
||||
|
||||
assert.equal(info.provider, "openai");
|
||||
assert.equal(info.provider, "codex");
|
||||
assert.equal(info.model, GPT_56_CODEX_MODEL);
|
||||
});
|
||||
|
||||
@@ -112,12 +115,24 @@ test("inactive Codex synchronized models do not influence bare-model routing", a
|
||||
assert.equal(info.model, GPT_56_CODEX_MODEL);
|
||||
});
|
||||
|
||||
test("OpenAI remains the historical default for overlapping static models", async () => {
|
||||
test("Codex claims an overlapping static model when both connections are active", async () => {
|
||||
await seedConnection("codex");
|
||||
await seedConnection("openai");
|
||||
|
||||
const info = await getModelInfoCore("gpt-5.5", null);
|
||||
|
||||
assert.equal(info.provider, "codex");
|
||||
assert.equal(info.model, "gpt-5.5");
|
||||
});
|
||||
|
||||
// The regression #9275 introduced and this file now guards: the Codex-native set must
|
||||
// never claim a bare id when no codex connection is active — an OpenAI-only install
|
||||
// would get "no active credentials for provider: codex" for a model OpenAI serves.
|
||||
test("a Codex-native bare id stays on OpenAI when no codex connection exists", async () => {
|
||||
await seedConnection("openai");
|
||||
|
||||
const info = await getModelInfoCore("gpt-5.5", null);
|
||||
|
||||
assert.equal(info.provider, "openai");
|
||||
assert.equal(info.model, "gpt-5.5");
|
||||
});
|
||||
|
||||
@@ -1,16 +1,44 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
|
||||
import {
|
||||
CODEX_NATIVE_UNPREFIXED_MODELS,
|
||||
getModelInfoCore,
|
||||
} from "../../open-sse/services/model.ts";
|
||||
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-bare-precedence-"));
|
||||
process.env.DATA_DIR = TEST_DATA_DIR;
|
||||
|
||||
// #FIX: bare Codex-default model ids must always route to the `codex`
|
||||
// provider (chatgpt.com OAuth) when no provider prefix is supplied, even
|
||||
// when other providers that also catalog the id (e.g. `agentrouter`,
|
||||
// `openai`) are active. The Codex cookie quota is the source of truth —
|
||||
// auto-fanning to other providers silently breaks the "default" experience.
|
||||
const core = await import("../../src/lib/db/core.ts");
|
||||
const providersDb = await import("../../src/lib/db/providers.ts");
|
||||
const { CODEX_NATIVE_UNPREFIXED_MODELS, getModelInfoCore } = await import(
|
||||
"../../open-sse/services/model.ts"
|
||||
);
|
||||
|
||||
// #FIX: bare Codex-default model ids must route to the `codex` provider
|
||||
// (chatgpt.com OAuth) when no provider prefix is supplied, even when other
|
||||
// providers that also catalog the id (e.g. `agentrouter`, `openai`) are
|
||||
// active. The Codex cookie quota is the source of truth — auto-fanning to
|
||||
// other providers silently breaks the "default" experience.
|
||||
//
|
||||
// #9447 bounded that precedence: it may only PREEMPT another provider when a
|
||||
// codex connection is actually ACTIVE. These cases therefore seed one first.
|
||||
// Without that bound, an OpenAI-only install had bare `gpt-5.5` sent to codex
|
||||
// and failed with "no active credentials for provider: codex" on a model
|
||||
// OpenAI serves. Ids that no other provider catalogs (the tier variants,
|
||||
// `codex-auto-review`) still resolve to codex with no connection at all —
|
||||
// there is no alternative to preempt — so those cases seed nothing.
|
||||
async function seedActiveCodexConnection() {
|
||||
await providersDb.createProviderConnection({
|
||||
provider: "codex",
|
||||
authType: "oauth",
|
||||
email: "codex@example.com",
|
||||
providerSpecificData: { workspaceId: "ws-precedence" },
|
||||
});
|
||||
}
|
||||
|
||||
test.after(() => {
|
||||
core.resetDbInstance();
|
||||
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
test("CODEX_NATIVE_UNPREFIXED_MODELS includes gpt-5.6-sol tier set", () => {
|
||||
for (const id of [
|
||||
@@ -40,6 +68,7 @@ test("CODEX_NATIVE_UNPREFIXED_MODELS includes gpt-5.6-sol tier set", () => {
|
||||
});
|
||||
|
||||
test("bare gpt-5.6-sol resolves to codex (provider native prefix wins)", async () => {
|
||||
await seedActiveCodexConnection();
|
||||
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");
|
||||
@@ -75,4 +104,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");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,18 +1,44 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
|
||||
import { getModelInfoCore } from "../../open-sse/services/model.ts";
|
||||
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-bare-routing-fallback-"));
|
||||
process.env.DATA_DIR = TEST_DATA_DIR;
|
||||
|
||||
const core = await import("../../src/lib/db/core.ts");
|
||||
const providersDb = await import("../../src/lib/db/providers.ts");
|
||||
const { getModelInfoCore } = await import("../../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, gpt-5.5, etc.) route to
|
||||
// `codex` ahead of any other provider that also catalogs them — bounded by
|
||||
// #9447 to installs where a codex connection is actually ACTIVE, so an
|
||||
// OpenAI-only install is not handed a provider it has no credentials for.
|
||||
// Ids that only codex catalogs (the tier variants) need no connection:
|
||||
// there is no alternative provider to preempt.
|
||||
// - 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
|
||||
// kiro-synced-catalog bug).
|
||||
// - Explicit `provider/model` prefixes always win over the bare inference.
|
||||
|
||||
test.before(async () => {
|
||||
await providersDb.createProviderConnection({
|
||||
provider: "codex",
|
||||
authType: "oauth",
|
||||
email: "codex@example.com",
|
||||
providerSpecificData: { workspaceId: "ws-routing-fallback" },
|
||||
});
|
||||
});
|
||||
|
||||
test.after(() => {
|
||||
core.resetDbInstance();
|
||||
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
test("bare gpt-5.6-sol routes to codex (precedence via CODEX_NATIVE_UNPREFIXED_MODELS)", async () => {
|
||||
const info = await getModelInfoCore("gpt-5.6-sol", null);
|
||||
assert.equal(
|
||||
|
||||
Reference in New Issue
Block a user