mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-05 14:52:09 +03:00
fix(fallback): normalize provider error rule headers (#5473)
Co-authored-by: KooshaPari <koosha@example.com>
This commit is contained in:
@@ -92,9 +92,7 @@ function buildMinimaxRules(): ProviderErrorRule[] {
|
||||
// If any model reports 0 remaining, the request was rejected for that
|
||||
// model. We classify as quota_exhausted so lockModel is called with
|
||||
// scope=model instead of poisoning the whole connection.
|
||||
const exhausted = headerVal
|
||||
.split(",")
|
||||
.some((pair) => pair.split("=")[1]?.trim() === "0");
|
||||
const exhausted = headerVal.split(",").some((pair) => pair.split("=")[1]?.trim() === "0");
|
||||
if (exhausted) {
|
||||
return { reason: "quota_exhausted", scope: "model" };
|
||||
}
|
||||
@@ -129,7 +127,7 @@ export function getProviderErrorRuleMatch(
|
||||
body?: unknown
|
||||
): ProviderErrorRuleMatch | null {
|
||||
if (!provider) return null;
|
||||
const rules = providerRuleRegistry.get(provider);
|
||||
const rules = providerRuleRegistry.get(provider.toLowerCase());
|
||||
if (!rules) return null;
|
||||
// Normalize headers: accept either a `Headers` object (from `fetch()`) or
|
||||
// a plain record. Provider rules access headers via plain object indexing.
|
||||
@@ -137,7 +135,12 @@ export function getProviderErrorRuleMatch(
|
||||
? {}
|
||||
: typeof (headers as Headers).get === "function"
|
||||
? Object.fromEntries((headers as Headers).entries())
|
||||
: (headers as Record<string, string>);
|
||||
: Object.fromEntries(
|
||||
Object.entries(headers as Record<string, string>).map(([key, value]) => [
|
||||
key.toLowerCase(),
|
||||
value,
|
||||
])
|
||||
);
|
||||
for (const rule of rules) {
|
||||
const match = rule.match({ status, headers: safeHeaders, body });
|
||||
if (match) return match;
|
||||
|
||||
@@ -14,12 +14,9 @@ import assert from "node:assert/strict";
|
||||
* - Anything else: falls back to global ERROR_RULES.
|
||||
*/
|
||||
|
||||
const { classifyError, checkFallbackError } = await import(
|
||||
"../../open-sse/services/accountFallback.ts"
|
||||
);
|
||||
const { RateLimitReason } = await import(
|
||||
"../../open-sse/config/constants.ts"
|
||||
);
|
||||
const { classifyError, checkFallbackError } =
|
||||
await import("../../open-sse/services/accountFallback.ts");
|
||||
const { RateLimitReason } = await import("../../open-sse/config/constants.ts");
|
||||
|
||||
test("S1: Opencode 429 with x-ratelimit-remaining-requests=0 → QUOTA_EXHAUSTED, not RATE_LIMIT_EXCEEDED", () => {
|
||||
// Opencode uses account-wide quota. The header `x-ratelimit-remaining-requests: 0`
|
||||
@@ -43,9 +40,8 @@ test("S2: Minimax 429 with x-model-quota-remaining header → QUOTA_EXHAUSTED wi
|
||||
// signals ONLY that model is locked; other models on the same connection must
|
||||
// remain available. classifyError returns the reason; the caller (combo.ts)
|
||||
// reads the scope from providerRuleMatch to decide lockModel vs updateProviderConnection.
|
||||
const { providerRuleRegistry, getProviderErrorRuleMatch } = await import(
|
||||
"../../open-sse/config/providerErrorRules.ts"
|
||||
);
|
||||
const { providerRuleRegistry, getProviderErrorRuleMatch } =
|
||||
await import("../../open-sse/config/providerErrorRules.ts");
|
||||
|
||||
// The registry must be loaded for minimax
|
||||
const minimaxRules = providerRuleRegistry.get("minimax");
|
||||
@@ -67,6 +63,24 @@ test("S2: Minimax 429 with x-model-quota-remaining header → QUOTA_EXHAUSTED wi
|
||||
);
|
||||
});
|
||||
|
||||
test("S2b: provider error rules match canonical-cased plain header records", async () => {
|
||||
const { getProviderErrorRuleMatch } = await import("../../open-sse/config/providerErrorRules.ts");
|
||||
|
||||
const opencodeMatch = getProviderErrorRuleMatch("OpenCode", 429, {
|
||||
"X-RateLimit-Remaining-Requests": "0",
|
||||
});
|
||||
assert.ok(opencodeMatch, "Opencode quota headers must be case-insensitive");
|
||||
assert.equal(opencodeMatch.reason, "quota_exhausted");
|
||||
assert.equal(opencodeMatch.scope, "provider");
|
||||
|
||||
const minimaxMatch = getProviderErrorRuleMatch("Minimax", 429, {
|
||||
"X-Model-Quota-Remaining": "haiku=0,sonnet=42",
|
||||
});
|
||||
assert.ok(minimaxMatch, "Minimax quota headers must be case-insensitive");
|
||||
assert.equal(minimaxMatch.reason, "quota_exhausted");
|
||||
assert.equal(minimaxMatch.scope, "model");
|
||||
});
|
||||
|
||||
test("S3: Regression — provider with no rules falls back to global ERROR_RULES unchanged", () => {
|
||||
// A provider not in the registry (e.g. "unknown-vendor") must NOT cause
|
||||
// classifyError to crash or return a different result. It must behave
|
||||
|
||||
Reference in New Issue
Block a user