mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-07 15:52:52 +03:00
* 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>
78 lines
2.6 KiB
TypeScript
78 lines
2.6 KiB
TypeScript
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
|
|
import { handleNoCredentials } from "../../src/sse/handlers/chatHelpers.ts";
|
|
|
|
// #FIX: the "No active credentials for provider: X" 404 response used to be
|
|
// a wall of silence — operators had no way to know which providers actually
|
|
// catalog the model id they requested. Surface a hint listing the top 3
|
|
// candidate aliases (provider/model prefix form) so the operator can
|
|
// prefix and route to a working provider on the next request.
|
|
|
|
test("handleNoCredentials includes candidate aliases hint when supplied", async () => {
|
|
const res = handleNoCredentials(
|
|
/* credentials */ {},
|
|
/* excludeConnectionId */ null,
|
|
/* provider */ "kiro",
|
|
/* model */ "claude-opus-5",
|
|
/* lastError */ null,
|
|
/* lastStatus */ null,
|
|
/* candidateAliases */ ["anthropic", "claude", "agentrouter"]
|
|
);
|
|
|
|
assert.equal(res.status, 404);
|
|
const body = (await res.json()) as { error?: { message?: string } };
|
|
const message = body?.error?.message ?? "";
|
|
assert.match(
|
|
message,
|
|
/No active credentials for provider: kiro/,
|
|
"must keep the original error prefix"
|
|
);
|
|
assert.match(
|
|
message,
|
|
/Try one of: anthropic\/claude-opus-5, claude\/claude-opus-5, agentrouter\/claude-opus-5/,
|
|
"must append a candidate-prefix hint when candidates are provided"
|
|
);
|
|
});
|
|
|
|
test("handleNoCredentials omits hint when no candidates supplied", async () => {
|
|
const res = handleNoCredentials(
|
|
{},
|
|
null,
|
|
"kiro",
|
|
"claude-opus-5",
|
|
null,
|
|
null
|
|
/* no candidateAliases */
|
|
);
|
|
|
|
assert.equal(res.status, 404);
|
|
const body = (await res.json()) as { error?: { message?: string } };
|
|
const message = body?.error?.message ?? "";
|
|
assert.match(message, /No active credentials for provider: kiro/);
|
|
assert.doesNotMatch(
|
|
message,
|
|
/Try one of:/,
|
|
"must NOT append a hint when no candidates are provided"
|
|
);
|
|
});
|
|
|
|
test("handleNoCredentials trims candidate list to top 3", async () => {
|
|
const res = handleNoCredentials(
|
|
{},
|
|
null,
|
|
"kiro",
|
|
"claude-opus-5",
|
|
null,
|
|
null,
|
|
["anthropic", "claude", "agentrouter", "github", "vertex-partner"]
|
|
);
|
|
|
|
const body = (await res.json()) as { error?: { message?: string } };
|
|
const message = body?.error?.message ?? "";
|
|
// Top-3 (anthropic, claude, agentrouter) — github and vertex-partner are
|
|
// dropped to keep the hint actionable.
|
|
assert.match(message, /Try one of: anthropic\/claude-opus-5, claude\/claude-opus-5, agentrouter\/claude-opus-5/);
|
|
assert.doesNotMatch(message, /github\/claude-opus-5/);
|
|
assert.doesNotMatch(message, /vertex-partner\/claude-opus-5/);
|
|
}); |