Files
OmniRoute/tests/unit/fix-bare-routing-fallback.test.ts
Diego Rodrigues de Sa e Souza a72e1656eb fix(routing): bare model ids route to codex first; validate synced candidates (#9275)
* fix(routing): bare model ids route to codex first; validate synced candidates

Two bare-model-routing bugs surfaced in the field when an OmniRoute
deployment had a codex subscription whose cookie quota was exhausted
(retry-after 429047s / ~5 days) AND an active kiro connection whose
upstream sync briefly advertised 'claude-opus-5' before kiro vendored
it into the static registry.

  1. Bare 'gpt-5.6-sol' (and friends) routed to the codex provider even
     when the user had explicitly configured 'agentrouter' as their
     provider (via model_provider in codex CLI). With codex in cooldown,
     every bare request 429'd. Fix: extend CODEX_NATIVE_UNPREFIXED_MODELS
     to include the full gpt-5.6-sol tier set + gpt-5.5 + the related
     codex-native ids. The Codex CLI default is now actually honored;
     users can still prefix 'agentrouter/gpt-5.6-sol' to opt into a
     specific provider.

  2. Bare 'claude-opus-5' silently routed to 'kiro' when kiro's synced
     /v1/models catalog had that id (likely from a transient upstream
     quirk). kiro's static registry never cataloged claude-opus-5, so
     the upstream call 404'd. Fix: validate activeSyncedProviders against
     MODEL_TO_PROVIDERS before merging them into the candidate list.
     Auto-discovery still wins when the model id has no static entry
     (brand-new models from upstream keep working).

Bonus: when handleNoCredentials returns a 404 'No active credentials for
provider: X' error, surface the top-3 candidate aliases (e.g.
'anthropic/claude-opus-5, claude/claude-opus-5, agentrouter/claude-opus-5')
so the operator can pick a working prefix instead of staring at a wall.

Tests (all pass, 25 regression tests preserved):
  - tests/unit/fix-bare-model-precedence.test.ts (7 tests)
  - tests/unit/fix-synced-model-validation.test.ts (3 tests)
  - tests/unit/fix-error-message-candidates.test.ts (3 tests)
  - tests/unit/fix-bare-routing-fallback.test.ts (7 tests)

* fix(tests): replace lorem ipsum with neutral text to avoid agentrouter WAF

The agentrouter.org WAF blocks requests containing 'lorem ipsum' in
messages[].content. When Claude Code reads test files via the Read tool,
the content appears in tool_result blocks which can trigger the filter.

Replace 'lorem ipsum dolor sit amet' with 'example content for testing
purposes' in compression harness test to avoid false positives.

---------

Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
2026-08-03 18:09:31 -03:00

62 lines
2.6 KiB
TypeScript

import test from "node:test";
import assert from "node:assert/strict";
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 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("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(
info.provider,
"codex",
"bare gpt-5.6-sol must route to codex — the Codex CLI default"
);
});
test("bare gpt-5.5 routes to codex", async () => {
const info = await getModelInfoCore("gpt-5.5", null);
assert.equal(info.provider, "codex");
});
test("bare gpt-5.6-sol-xhigh (a tier id) routes to codex", async () => {
const info = await getModelInfoCore("gpt-5.6-sol-xhigh", null);
assert.equal(info.provider, "codex");
});
test("explicit prefix overrides bare precedence (agentrouter/gpt-5.6-sol)", async () => {
const info = await getModelInfoCore("agentrouter/gpt-5.6-sol", null);
assert.equal(info.provider, "agentrouter");
});
test("explicit prefix overrides bare precedence (openai/gpt-5.6-sol)", async () => {
const info = await getModelInfoCore("openai/gpt-5.6-sol", null);
assert.equal(info.provider, "openai");
});
test("bare claude-opus-5 never resolves to kiro (synced-catalog validation)", async () => {
// The bug: a kiro connection had claude-opus-5 in its synced /v1/models
// cache (likely from a brief upstream quirk). The bare-routing path
// accepted it as a candidate and routed traffic there, which then 404'd
// because kiro's static registry never cataloged claude-opus-5.
// The fix: validated synced candidates against the static registry.
const info = await getModelInfoCore("claude-opus-5", null);
assert.notEqual(
info.provider,
"kiro",
`kiro must NOT win bare claude-opus-5 routing — it does not catalog the model`
);
});
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");
});