mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-24 16:12:23 +03:00
Validated on a worktree over the current tip: account-fallback-service 91/91 plus the five sibling lockout suites 24/24. The measurement in the body (40 of 111 passthroughModels providers uncovered on this branch) is the clincher — one lookup via getProviderById().passthroughModels beside the existing checks, closing the #11071 remainder for shared-registry gateways (port of #11075 which had only landed on main). Thank you @yourspraveen!
This commit is contained in:
committed by
GitHub
parent
ce66d319b1
commit
92ef3c71ea
@@ -0,0 +1 @@
|
|||||||
|
- **fix(resilience):** a missing-model `404` on a provider that declares `passthroughModels: true` in the shared registry (novita, uncloseai, orcarouter and 37 others) now locks out only that model instead of cooling the entire connection — `hasPerModelQuota()` previously read only the open-sse registry and the local/self-hosted families ([#11165](https://github.com/diegosouzapw/OmniRoute/pull/11165)) — thanks @yourspraveen
|
||||||
@@ -21,7 +21,11 @@ import {
|
|||||||
honorsRuleLockScope,
|
honorsRuleLockScope,
|
||||||
} from "../config/providerErrorRules.ts";
|
} from "../config/providerErrorRules.ts";
|
||||||
import * as rot from "./rotationConfig.ts";
|
import * as rot from "./rotationConfig.ts";
|
||||||
import { getPassthroughProviders, getProviderCategory, isLocalProvider } from "../config/providerRegistry.ts";
|
import {
|
||||||
|
getPassthroughProviders,
|
||||||
|
getProviderCategory,
|
||||||
|
isLocalProvider,
|
||||||
|
} from "../config/providerRegistry.ts";
|
||||||
import {
|
import {
|
||||||
DEFAULT_RESILIENCE_SETTINGS,
|
DEFAULT_RESILIENCE_SETTINGS,
|
||||||
resolveResilienceSettings,
|
resolveResilienceSettings,
|
||||||
@@ -37,7 +41,12 @@ import {
|
|||||||
type FailureKind,
|
type FailureKind,
|
||||||
} from "../../src/shared/utils/classify429";
|
} from "../../src/shared/utils/classify429";
|
||||||
import { recordProviderSuccess as resetCooldownFailureCount } from "./providerCooldownTracker.ts";
|
import { recordProviderSuccess as resetCooldownFailureCount } from "./providerCooldownTracker.ts";
|
||||||
import { resolveProviderId, isLocalProvider as isLocalProviderId, isSelfHostedChatProvider } from "../../src/shared/constants/providers";
|
import {
|
||||||
|
getProviderById,
|
||||||
|
resolveProviderId,
|
||||||
|
isLocalProvider as isLocalProviderId,
|
||||||
|
isSelfHostedChatProvider,
|
||||||
|
} from "../../src/shared/constants/providers";
|
||||||
import { resolveUseUpstream429BreakerHints } from "../../src/shared/utils/providerHints";
|
import { resolveUseUpstream429BreakerHints } from "../../src/shared/utils/providerHints";
|
||||||
import { getCodexModelScope } from "../config/codexQuotaScopes.ts";
|
import { getCodexModelScope } from "../config/codexQuotaScopes.ts";
|
||||||
import { getQuotaScopedModelForProvider } from "./antigravityQuotaFamily.ts";
|
import { getQuotaScopedModelForProvider } from "./antigravityQuotaFamily.ts";
|
||||||
@@ -797,6 +806,12 @@ export function hasPerModelQuota(
|
|||||||
if (canonicalId === "gemini" || canonicalId === "github") return true;
|
if (canonicalId === "gemini" || canonicalId === "github") return true;
|
||||||
if (canonicalId === "antigravity" || canonicalId === "agy") return true;
|
if (canonicalId === "antigravity" || canonicalId === "agy") return true;
|
||||||
if (getPassthroughProviders().has(canonicalId)) return true;
|
if (getPassthroughProviders().has(canonicalId)) return true;
|
||||||
|
// #11071: getPassthroughProviders() reads the open-sse REGISTRY. A provider can declare
|
||||||
|
// passthroughModels:true in the SHARED registry (src/shared/constants/providers/) and be
|
||||||
|
// absent from that set — 40 of them are, and they are neither local nor self-hosted, so the
|
||||||
|
// branch below never reaches them either. Without this lookup a missing-model 404 on one of
|
||||||
|
// those cools the whole connection instead of locking out the single model.
|
||||||
|
if (getProviderById(canonicalId)?.passthroughModels === true) return true;
|
||||||
if (isCompatibleProvider(canonicalId)) return true;
|
if (isCompatibleProvider(canonicalId)) return true;
|
||||||
if (isLocalProviderId(canonicalId) || isSelfHostedChatProvider(canonicalId)) return true;
|
if (isLocalProviderId(canonicalId) || isSelfHostedChatProvider(canonicalId)) return true;
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -453,6 +453,25 @@ test("hasPerModelQuota returns true for GitHub Copilot provider (#1624)", () =>
|
|||||||
assert.equal(hasPerModelQuota("github", "gpt-5-mini"), true);
|
assert.equal(hasPerModelQuota("github", "gpt-5-mini"), true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("hasPerModelQuota honors shared-registry passthrough providers (#11071)", () => {
|
||||||
|
// These declare passthroughModels:true in src/shared/constants/providers/, but are absent
|
||||||
|
// from the open-sse REGISTRY passthrough set and are neither local nor self-hosted — so the
|
||||||
|
// isLocalProvider/isSelfHostedChatProvider branch (#11078) never reaches them. Without the
|
||||||
|
// shared-registry lookup a missing model on one of these cools the WHOLE connection.
|
||||||
|
assert.equal(hasPerModelQuota("novita"), true);
|
||||||
|
assert.equal(hasPerModelQuota("uncloseai"), true);
|
||||||
|
assert.equal(hasPerModelQuota("orcarouter"), true);
|
||||||
|
|
||||||
|
// Already covered by the local/self-hosted branch — asserted so this port cannot regress it.
|
||||||
|
assert.equal(hasPerModelQuota("ollama-local"), true);
|
||||||
|
assert.equal(hasPerModelQuota("lm-studio"), true);
|
||||||
|
assert.equal(hasPerModelQuota("vllm"), true);
|
||||||
|
|
||||||
|
// Neither declared in the shared registry nor local: a failure here is still connection-wide.
|
||||||
|
assert.equal(hasPerModelQuota("openai"), false);
|
||||||
|
assert.equal(hasPerModelQuota("anthropic"), false);
|
||||||
|
});
|
||||||
|
|
||||||
test("Codex Spark 429s are scoped away from normal Codex models", () => {
|
test("Codex Spark 429s are scoped away from normal Codex models", () => {
|
||||||
const connectionId = `codex-${Date.now()}`;
|
const connectionId = `codex-${Date.now()}`;
|
||||||
clearModelLock("codex", connectionId, "gpt-5.3-codex-spark");
|
clearModelLock("codex", connectionId, "gpt-5.3-codex-spark");
|
||||||
@@ -1659,7 +1678,11 @@ test("#10460: model-unsupported 400 handles various phrasings", async () => {
|
|||||||
// Verify connection stays healthy after each iteration
|
// Verify connection stays healthy after each iteration
|
||||||
const conn = await providersDb.getProviderConnectionById(id);
|
const conn = await providersDb.getProviderConnectionById(id);
|
||||||
assert.ok(!conn.rateLimitedUntil, `"${errorText}" must not rate-limit connection`);
|
assert.ok(!conn.rateLimitedUntil, `"${errorText}" must not rate-limit connection`);
|
||||||
assert.notStrictEqual(conn.testStatus, "unavailable", `"${errorText}" must not mark unavailable`);
|
assert.notStrictEqual(
|
||||||
|
conn.testStatus,
|
||||||
|
"unavailable",
|
||||||
|
`"${errorText}" must not mark unavailable`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1692,7 +1715,11 @@ test("#10460: non-400 status with model-unsupported text does NOT trigger guard"
|
|||||||
"test-model"
|
"test-model"
|
||||||
);
|
);
|
||||||
|
|
||||||
assert.strictEqual(result.shouldFallback, true, "non-400 must not be short-circuited by model guard");
|
assert.strictEqual(
|
||||||
|
result.shouldFallback,
|
||||||
|
true,
|
||||||
|
"non-400 must not be short-circuited by model guard"
|
||||||
|
);
|
||||||
// The key assertion: guard returns shouldFallback:false. If we get here with
|
// The key assertion: guard returns shouldFallback:false. If we get here with
|
||||||
// shouldFallback:true, the guard did NOT fire (correct behavior).
|
// shouldFallback:true, the guard did NOT fire (correct behavior).
|
||||||
});
|
});
|
||||||
@@ -1723,7 +1750,11 @@ test("#10460: auth-credential 400 text does NOT match model-unsupported guard",
|
|||||||
|
|
||||||
// This text does NOT match MODEL_ACCESS_DENIED_PATTERNS (verified by regex test)
|
// This text does NOT match MODEL_ACCESS_DENIED_PATTERNS (verified by regex test)
|
||||||
// so it falls through to checkFallbackError which returns shouldFallback:false for generic 400
|
// so it falls through to checkFallbackError which returns shouldFallback:false for generic 400
|
||||||
assert.strictEqual(result.shouldFallback, false, "auth-credential 400 must not be caught by model guard");
|
assert.strictEqual(
|
||||||
|
result.shouldFallback,
|
||||||
|
false,
|
||||||
|
"auth-credential 400 must not be caught by model guard"
|
||||||
|
);
|
||||||
// The generic 400 path returns cooldownMs:0 — same as the guard, but the
|
// The generic 400 path returns cooldownMs:0 — same as the guard, but the
|
||||||
// connection was NOT touched (no rateLimitedUntil set). This distinguishes
|
// connection was NOT touched (no rateLimitedUntil set). This distinguishes
|
||||||
// it from the normal fallback path which would set a cooldown.
|
// it from the normal fallback path which would set a cooldown.
|
||||||
@@ -1737,7 +1768,11 @@ test("#10460: guard early return does not touch DB (distinguishes from normal pa
|
|||||||
|
|
||||||
// Guard path: model-unsupported 400 → shouldFallback:false, cooldownMs:0, no DB change
|
// Guard path: model-unsupported 400 → shouldFallback:false, cooldownMs:0, no DB change
|
||||||
const guardResult = await auth.markAccountUnavailable(
|
const guardResult = await auth.markAccountUnavailable(
|
||||||
connId, 400, "The requested model is not supported", "github", "test-model"
|
connId,
|
||||||
|
400,
|
||||||
|
"The requested model is not supported",
|
||||||
|
"github",
|
||||||
|
"test-model"
|
||||||
);
|
);
|
||||||
assert.strictEqual(guardResult.shouldFallback, false);
|
assert.strictEqual(guardResult.shouldFallback, false);
|
||||||
assert.strictEqual(guardResult.cooldownMs, 0);
|
assert.strictEqual(guardResult.cooldownMs, 0);
|
||||||
|
|||||||
Reference in New Issue
Block a user