mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-31 04:12:10 +03:00
fix(combo): keep passthrough quota fallback scoped (#4194)
Integrated into release/v3.8.29 — reconciled onto post-split combo.ts (#4186); RED->GREEN validated. Thanks @Svetznaniy33.
This commit is contained in:
@@ -16,6 +16,7 @@ import {
|
||||
recordModelLockoutFailure,
|
||||
recordProviderFailure,
|
||||
isProviderExhaustedReason,
|
||||
hasPerModelQuota,
|
||||
type ProviderProfile,
|
||||
} from "./accountFallback.ts";
|
||||
import { FETCH_TIMEOUT_MS, RateLimitReason } from "../config/constants.ts";
|
||||
@@ -2521,8 +2522,12 @@ export async function handleComboChat({
|
||||
// same-provider targets are skipped immediately. API-key 429s still use
|
||||
// the short resilience cooldown, but explicit quota text should stop the
|
||||
// combo from trying another target for the same provider in this request.
|
||||
// Passthrough/per-model-quota providers multiplex independent upstream
|
||||
// models behind one provider connection; a quota 429 for one model must
|
||||
// not skip fallback targets for another model on the same provider.
|
||||
const providerExhausted =
|
||||
Boolean(provider && provider !== "unknown") &&
|
||||
!hasPerModelQuota(provider, rawModel) &&
|
||||
(isProviderExhaustedReason(fallbackResult) ||
|
||||
classifyErrorText(errorText) === RateLimitReason.QUOTA_EXHAUSTED);
|
||||
if (providerExhausted) {
|
||||
@@ -3269,8 +3274,12 @@ async function handleRoundRobinCombo({
|
||||
// same-provider targets are skipped immediately. API-key 429s still use
|
||||
// the short resilience cooldown, but explicit quota text should stop the
|
||||
// combo from trying another target for the same provider in this request.
|
||||
// Passthrough/per-model-quota providers multiplex independent upstream
|
||||
// models behind one provider connection; a quota 429 for one model must
|
||||
// not skip fallback targets for another model on the same provider.
|
||||
const providerExhausted =
|
||||
Boolean(provider && provider !== "unknown") &&
|
||||
!hasPerModelQuota(provider, parseModel(modelStr).model || modelStr) &&
|
||||
(isProviderExhaustedReason(fallbackResult) ||
|
||||
classifyErrorText(errorText) === RateLimitReason.QUOTA_EXHAUSTED ||
|
||||
isAllAccountsRateLimited);
|
||||
|
||||
@@ -203,3 +203,41 @@ test("pre-screen: backward compatible with all targets available", async () => {
|
||||
assert.equal(calls.length, 1);
|
||||
assert.equal(calls[0], "p1/m1");
|
||||
});
|
||||
|
||||
test("priority combo: quota 429 on passthrough provider does not skip another model on same provider", async () => {
|
||||
const calls: string[] = [];
|
||||
|
||||
const combo = await combosDb.createCombo({
|
||||
name: "passthrough-quota-scope",
|
||||
strategy: "priority",
|
||||
models: ["antigravity/claude-opus-4-6-thinking", "antigravity/gemini-3-flash-agent"],
|
||||
});
|
||||
|
||||
const response = await handleComboChat({
|
||||
body: { ...reqBody, model: combo.name },
|
||||
combo,
|
||||
allCombos: [combo],
|
||||
isModelAvailable: async () => true,
|
||||
relayOptions: undefined,
|
||||
signal: undefined,
|
||||
settings: {},
|
||||
log: makeLog(),
|
||||
handleSingleModel: async (_body: unknown, modelStr: string) => {
|
||||
calls.push(modelStr);
|
||||
if (modelStr.includes("claude-opus")) {
|
||||
return Response.json(
|
||||
{ error: { message: "quota exhausted for claude-opus-4-6-thinking" } },
|
||||
{ status: 429 }
|
||||
);
|
||||
}
|
||||
return okResponse(modelStr);
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
assert.equal(calls.at(-1), "antigravity/gemini-3-flash-agent");
|
||||
assert.ok(
|
||||
calls.includes("antigravity/claude-opus-4-6-thinking"),
|
||||
"first passthrough model should be attempted before fallback"
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user