mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-21 06:32:16 +03:00
Merge pull request #10912 from diegosouzapw/fix/10095-antigravity-multiaccount-quota
fix(domain): treat unreported Antigravity quota fraction as unknown, not exhausted (#10095)
This commit is contained in:
@@ -0,0 +1 @@
|
||||
- fix(domain): stop treating an unreported Antigravity quota fraction (`fractionReported:false`) as 0% remaining in `quotaCache.ts`, which was falsely marking every fresh/newly-connected account as exhausted and blocking multi-account rotation (#10095)
|
||||
@@ -44,6 +44,13 @@ import { getAntigravityQuotaFamily } from "@omniroute/open-sse/services/antigrav
|
||||
interface QuotaInfo {
|
||||
remainingPercentage: number;
|
||||
resetAt: string | null;
|
||||
// #10095 — upstream explicitly told us it did NOT report this window's
|
||||
// fraction (e.g. a fresh Antigravity account or a newly-launched
|
||||
// -tiered model id Google hasn't wired quota telemetry for yet).
|
||||
// `undefined`/`true` means the value is a real, upstream-reported
|
||||
// percentage; `false` means "unknown", so callers must not treat the
|
||||
// defaulted-to-0 `remainingPercentage` as genuine exhaustion.
|
||||
fractionReported?: boolean;
|
||||
}
|
||||
|
||||
interface QuotaCacheEntry {
|
||||
@@ -113,7 +120,10 @@ const MAX_CONCURRENT_REFRESHES = 5;
|
||||
function isExhausted(quotas: Record<string, QuotaInfo>): boolean {
|
||||
const entries = Object.values(quotas);
|
||||
if (entries.length === 0) return false;
|
||||
return entries.every((q) => q.remainingPercentage <= 0);
|
||||
// #10095 — a window whose fraction was never reported by upstream must
|
||||
// never single-handedly flip the whole connection to exhausted; treat it
|
||||
// as available (mirrors the guard in genericQuotaFetcher.ts).
|
||||
return entries.every((q) => q.fractionReported !== false && q.remainingPercentage <= 0);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -237,6 +247,9 @@ function normalizeQuotas(rawQuotas: Record<string, any>): Record<string, QuotaIn
|
||||
safePercentage(q.remainingPercentage) ??
|
||||
(q.total > 0 ? Math.round(((q.total - (q.used || 0)) / q.total) * 100) : 0),
|
||||
resetAt: q.resetAt || null,
|
||||
// #10095 — thread through the "did upstream actually report this
|
||||
// window's fraction" signal (see UsageQuota in usage/quota.ts).
|
||||
fractionReported: q.fractionReported === false ? false : undefined,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -641,11 +654,14 @@ export function getQuotaWindowStatus(
|
||||
usedPercentage,
|
||||
resetAt,
|
||||
// If reset time has already passed, avoid stale cached percentages blocking selection.
|
||||
reachedThreshold: windowExpired
|
||||
? false
|
||||
: remainingPercentage <= 0
|
||||
? true
|
||||
: usedPercentage >= thresholdPercent,
|
||||
// #10095 — a window whose fraction upstream never reported is "unknown",
|
||||
// not "0% remaining"; never let it reach the exhaustion threshold.
|
||||
reachedThreshold:
|
||||
windowExpired || window.fractionReported === false
|
||||
? false
|
||||
: remainingPercentage <= 0
|
||||
? true
|
||||
: usedPercentage >= thresholdPercent,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
// #10095 — Antigravity multi-account "all exhausted" false positive.
|
||||
//
|
||||
// src/domain/quotaCache.ts is the FIRST, unconditional gate every chat request
|
||||
// passes through (src/sse/services/auth.ts::getProviderCredentialsWithQuotaPreflight).
|
||||
// When Google's Cloud Code API doesn't report `remainingFraction` for a model
|
||||
// (fresh accounts, newly-launched -tiered model ids), open-sse/services/usage/
|
||||
// antigravity.ts writes `fractionReported:false` but defaults
|
||||
// `remainingPercentage` to 0 — quotaCache.ts previously read only the numeric
|
||||
// percentage and treated that as genuine 0%-remaining exhaustion, so every
|
||||
// freshly-connected Antigravity account looked simultaneously (and falsely)
|
||||
// dead, and getProviderCredentials returned "All antigravity accounts have
|
||||
// exhausted their quota" before ever trying one.
|
||||
import { test, describe, before, after } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
describe("#10095 — quotaCache respects Antigravity fractionReported:false", () => {
|
||||
before(async () => {
|
||||
const { __clearForTests } = await import("../../src/domain/quotaCache.ts");
|
||||
__clearForTests();
|
||||
});
|
||||
after(async () => {
|
||||
const { __clearForTests } = await import("../../src/domain/quotaCache.ts");
|
||||
__clearForTests();
|
||||
});
|
||||
|
||||
test("unreported quota window (fractionReported:false) is NOT treated as exhausted", async () => {
|
||||
const { setQuotaCache, isQuotaExhaustedForRequest } = await import(
|
||||
"../../src/domain/quotaCache.ts"
|
||||
);
|
||||
const connectionId = "10095-fresh-account";
|
||||
// Exact shape open-sse/services/usage/antigravity.ts:660-694 writes when
|
||||
// Google's API omits remainingFraction for this model.
|
||||
setQuotaCache(connectionId, "antigravity", {
|
||||
"gemini-3.7-flash-tiered": {
|
||||
used: 0,
|
||||
total: 1000,
|
||||
resetAt: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(),
|
||||
remainingPercentage: 0,
|
||||
unlimited: false,
|
||||
fractionReported: false,
|
||||
quotaSource: "fetchAvailableModels",
|
||||
},
|
||||
});
|
||||
const exhausted = isQuotaExhaustedForRequest(
|
||||
connectionId,
|
||||
"antigravity",
|
||||
"agy/gemini-3.7-flash-tiered"
|
||||
);
|
||||
assert.equal(exhausted, false, "must NOT treat an unreported quota window as exhausted");
|
||||
});
|
||||
|
||||
test("companion: a REAL 0% window (fractionReported:true) still reports exhausted", async () => {
|
||||
const { setQuotaCache, isQuotaExhaustedForRequest } = await import(
|
||||
"../../src/domain/quotaCache.ts"
|
||||
);
|
||||
const connectionId = "10095-genuinely-exhausted-account";
|
||||
setQuotaCache(connectionId, "antigravity", {
|
||||
"gemini-3.7-flash-tiered": {
|
||||
used: 1000,
|
||||
total: 1000,
|
||||
resetAt: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(),
|
||||
remainingPercentage: 0,
|
||||
unlimited: false,
|
||||
fractionReported: true,
|
||||
quotaSource: "retrieveUserQuota",
|
||||
},
|
||||
});
|
||||
const exhausted = isQuotaExhaustedForRequest(
|
||||
connectionId,
|
||||
"antigravity",
|
||||
"agy/gemini-3.7-flash-tiered"
|
||||
);
|
||||
assert.equal(
|
||||
exhausted,
|
||||
true,
|
||||
"the fix must not blanket-disable exhaustion detection — a genuinely reported 0% window still exhausts"
|
||||
);
|
||||
});
|
||||
|
||||
test("issue follow-up model id shape (agy/gemini-3.7-flash-tiered) still resolves its family", async () => {
|
||||
const { setQuotaCache, isQuotaExhaustedForRequest } = await import(
|
||||
"../../src/domain/quotaCache.ts"
|
||||
);
|
||||
const connectionId = "10095-agy-tiered-family";
|
||||
setQuotaCache(connectionId, "agy", {
|
||||
"agy/gemini-3.7-flash-tiered": {
|
||||
used: 500,
|
||||
total: 1000,
|
||||
resetAt: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(),
|
||||
remainingPercentage: 50,
|
||||
unlimited: false,
|
||||
fractionReported: true,
|
||||
quotaSource: "retrieveUserQuota",
|
||||
},
|
||||
});
|
||||
const exhausted = isQuotaExhaustedForRequest(
|
||||
connectionId,
|
||||
"agy",
|
||||
"agy/gemini-3.7-flash-tiered"
|
||||
);
|
||||
assert.equal(
|
||||
exhausted,
|
||||
false,
|
||||
"non-regression: family resolution for the agy/gemini-3.7-flash-tiered id must keep working"
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user