mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-21 06:12:17 +03:00
* fix(combo): scope Claude model failures to the model, not the account A priority combo whose steps are five models on one Claude OAuth connection stops at step 1. hasPerModelQuota() returns false for the claude provider, so markAccountUnavailable() records a model-specific 404 or 5xx against the connection row, and getPersistedConnectionCooldownSkipReason() then skips every sibling step before dispatch. Verified with a combo whose first step names a model that cannot exist: the 404 lands and step 2 is never tried, while the same combo works as soon as step 2 points at a different account. A Claude OAuth connection multiplexes Fable 5, Opus 5/4.8/4.7/4.6, Sonnet and Haiku behind one credential, which is the multiplexing hasPerModelQuota already describes. Its quota is a separate question: a 429 on a Max subscription is account-wide, and shouldMarkAccountExhaustedFrom429 pins that. So this adds hasPerModelFailureScope() for the non-quota statuses and leaves 429 alone. Combo exhaustion gets the same treatment for 404, which names one model the account cannot serve rather than a bad connection. The empty-content 502 was already exempt through isEmptyContentFailure. Closes #12334 * fix(combo): extract Claude per-model failure scope to keep file-size caps Move hasPerModelFailureScope into a leaf so frozen accountFallback.ts and auth.ts stay at their file-size caps after #12334. Register combo-claude-per-model-scope.test.ts in Stryker tap.testFiles so the mutation-test-coverage gate sees the covering unit test. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
60 lines
2.7 KiB
TypeScript
60 lines
2.7 KiB
TypeScript
// tests/unit/combo-claude-per-model-scope.test.ts
|
|
// Regression test for #12334: a priority combo whose steps are different models on one
|
|
// Claude OAuth connection stopped at step 1. The model failure was recorded against the
|
|
// connection, so the pre-dispatch cooldown gate skipped every sibling step.
|
|
import { test } from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import {
|
|
hasPerModelQuota,
|
|
hasPerModelFailureScope,
|
|
shouldMarkAccountExhaustedFrom429,
|
|
} from "../../open-sse/services/accountFallback.ts";
|
|
import { getPersistedConnectionCooldownSkipReason } from "../../open-sse/services/combo/comboPredicates.ts";
|
|
|
|
test("claude scopes non-quota failures to the model", () => {
|
|
assert.equal(hasPerModelFailureScope("claude", "claude-fable-5-max"), true);
|
|
assert.equal(hasPerModelFailureScope("claude", "claude-opus-5-max"), true);
|
|
});
|
|
|
|
test("claude quota stays account-wide", () => {
|
|
assert.equal(hasPerModelQuota("claude", "claude-opus-5-max"), false);
|
|
assert.equal(shouldMarkAccountExhaustedFrom429("claude", "claude-sonnet-4-6"), true);
|
|
assert.equal(hasPerModelFailureScope("claude", "claude-opus-5-max", undefined, 429), false);
|
|
assert.equal(hasPerModelFailureScope("gemini", "gemma-4-31b-it", undefined, 429), true);
|
|
assert.equal(hasPerModelFailureScope("claude", "claude-opus-5-max", undefined, 404), true);
|
|
assert.equal(hasPerModelFailureScope("claude", "claude-opus-5-max", undefined, 502), true);
|
|
});
|
|
|
|
test("providers that already scope per model keep both scopes", () => {
|
|
assert.equal(hasPerModelFailureScope("gemini", "gemma-4-31b-it"), true);
|
|
assert.equal(hasPerModelFailureScope("github", "some-model"), true);
|
|
});
|
|
|
|
test("providers with neither scope are unchanged", () => {
|
|
assert.equal(hasPerModelFailureScope("openai", "gpt-4"), false);
|
|
});
|
|
|
|
test("a connection-level override still wins over the provider default", () => {
|
|
assert.equal(hasPerModelFailureScope("claude", "claude-opus-5-max", false), false);
|
|
});
|
|
|
|
test("a sibling step is skipped while its connection carries a real cooldown", () => {
|
|
const skip = getPersistedConnectionCooldownSkipReason(
|
|
{ modelStr: "claude/claude-opus-5-max", connectionId: "conn-1" },
|
|
{ rateLimitedUntil: new Date(Date.now() + 60_000).toISOString(), testStatus: "unavailable" }
|
|
);
|
|
assert.ok(skip, "the gate must still protect siblings when the cooldown is connection-wide");
|
|
});
|
|
|
|
test("a sibling step dispatches once no cooldown is recorded", () => {
|
|
const skip = getPersistedConnectionCooldownSkipReason(
|
|
{ modelStr: "claude/claude-opus-5-max", connectionId: "conn-1" },
|
|
{ rateLimitedUntil: null, testStatus: "active" }
|
|
);
|
|
assert.equal(
|
|
skip,
|
|
null,
|
|
"step 2 of a drop chain must dispatch after step 1 failed with a model-scoped error"
|
|
);
|
|
});
|