From 87719f238142f70752c32c9ab94d1e2b6d409493 Mon Sep 17 00:00:00 2001 From: Markus Hartung Date: Thu, 20 Aug 2026 20:29:38 -0300 Subject: [PATCH] fix(domain): treat unreported Antigravity quota fraction as unknown, not exhausted (#10095) --- ...ity-multiaccount-quota-false-exhaustion.md | 1 + src/domain/quotaCache.ts | 28 ++++- ...ntigravity-fraction-reported-10095.test.ts | 107 ++++++++++++++++++ 3 files changed, 130 insertions(+), 6 deletions(-) create mode 100644 changelog.d/fixes/10095-antigravity-multiaccount-quota-false-exhaustion.md create mode 100644 tests/unit/quota-cache-antigravity-fraction-reported-10095.test.ts diff --git a/changelog.d/fixes/10095-antigravity-multiaccount-quota-false-exhaustion.md b/changelog.d/fixes/10095-antigravity-multiaccount-quota-false-exhaustion.md new file mode 100644 index 0000000000..579005e943 --- /dev/null +++ b/changelog.d/fixes/10095-antigravity-multiaccount-quota-false-exhaustion.md @@ -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) diff --git a/src/domain/quotaCache.ts b/src/domain/quotaCache.ts index 908411cf3f..1fae2cb08d 100644 --- a/src/domain/quotaCache.ts +++ b/src/domain/quotaCache.ts @@ -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): 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): Record 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, }; } diff --git a/tests/unit/quota-cache-antigravity-fraction-reported-10095.test.ts b/tests/unit/quota-cache-antigravity-fraction-reported-10095.test.ts new file mode 100644 index 0000000000..3cffea7ec4 --- /dev/null +++ b/tests/unit/quota-cache-antigravity-fraction-reported-10095.test.ts @@ -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" + ); + }); +});