fix(antigravity): keep unreported quota fraction unknown (#7138)

Merged after batch validation on a combined worktree cut from `release/v3.8.51` with #9944 and #9908.

**Evidence**
- Focused tests: 36/36 pass on the combined tree, including `convertUsageToQuotaInfo skips Antigravity quota entries with an unknown fraction` and the `#6295` regression that guards the same class of bug on another provider.
- Gates on the combined tree: `check-complexity` PASS, `check-cognitive-complexity` PASS, `typecheck:core` PASS, `check-changelog-integrity` PASS.
- The red `check-file-size` reproduces byte-identical on the pure `release/v3.8.51` tip — inherited base-red, not from this PR. The red CI run here dates from 2026-09-15 against an older base.

Thanks, @Ardem2025 — this is the smallest diff of your batch and arguably the one with the widest blast radius avoided. Writing `remainingPercentage: 0` for an unreported fraction made "we don't know" numerically indistinguishable from "fully exhausted" to every downstream consumer of the quota cache; omitting the field so preflight fails open is the correct read of the upstream's silence.
This commit is contained in:
Dmitry Kuznetsov
2026-09-17 01:59:58 +03:00
committed by GitHub
parent 00860f3278
commit a2c6f7188d
4 changed files with 29 additions and 10 deletions

View File

@@ -0,0 +1 @@
- Fix Antigravity quota parsing treating an unreported `remainingFraction` as 0% remaining instead of unknown, which made a genuinely exhausted quota indistinguishable from one the upstream simply didn't report.

View File

@@ -654,12 +654,12 @@ export async function getAntigravityUsage(
const liveQuota = userQuotaEntries.get(modelKey);
const quotaSource = liveQuota || quotaInfo;
const rawFraction = toNumber(quotaSource.remainingFraction, -1);
const rawFraction = toNumber(quotaSource.remainingFraction, Number.NaN);
const resetAt = parseResetTime(quotaSource.resetTime);
// Distinguish "upstream did not report remainingFraction" from "remaining is 0%".
// fetchAvailableModels is a catalog view and can be stale/full; retrieveUserQuota is
// the source of truth for actual Gemini consumption when it includes the model.
const fractionReported = rawFraction >= 0;
const fractionReported = Number.isFinite(rawFraction);
if (!fractionReported) {
console.warn(
`[Antigravity] model ${modelKey} returned no remainingFraction — quota unknown`
@@ -669,18 +669,22 @@ export async function getAntigravityUsage(
// Models with no resetTime AND a reported full fraction are unlimited
// (e.g. tab-completion models). Unreported fraction is NEVER unlimited.
const isUnlimited = fractionReported && !resetAt && remainingFraction >= 1;
const remainingPercentage = remainingFraction * 100;
const QUOTA_NORMALIZED_BASE = 1000;
const total = QUOTA_NORMALIZED_BASE;
const total = fractionReported ? QUOTA_NORMALIZED_BASE : 0;
const remaining = Math.round(total * remainingFraction);
const used = isUnlimited ? 0 : Math.max(0, total - remaining);
quotas[modelKey] = applyLocalUsageFallback(
{
// An omitted fraction is unknown, not a 0% sentinel. Keep the reset
// timestamp for display, but omit numeric quota fields so cache and
// preflight fail open rather than turning uncertainty into exhaustion.
used,
total: isUnlimited ? 0 : total,
resetAt,
remainingPercentage: isUnlimited ? 100 : remainingPercentage,
...(fractionReported && {
remainingPercentage: isUnlimited ? 100 : remainingFraction * 100,
}),
unlimited: isUnlimited,
fractionReported,
quotaSource: liveQuota ? "retrieveUserQuota" : "fetchAvailableModels",

View File

@@ -2,7 +2,7 @@
* Tests for open-sse/services/usage.ts — Antigravity quota parsing.
*
* Verifies that remainingFraction is correctly parsed:
* - undefined → 0% remaining (exhausted quota)
* - undefined → unknown quota (not exhausted)
* - 0 → 0% remaining (exhausted quota, explicit)
* - 1.0 → 100% remaining (full quota)
* - 1.0 without resetTime → unlimited (e.g. tab-completion)
@@ -28,7 +28,7 @@ describe("getUsageForProvider (antigravity in usage.ts)", () => {
projectId: undefined,
};
it("defaults to 0% remaining when remainingFraction is undefined", async () => {
it("treats a missing remainingFraction as unknown rather than exhausted", async () => {
const originalFetch = globalThis.fetch;
globalThis.fetch = async () =>
({
@@ -53,9 +53,10 @@ describe("getUsageForProvider (antigravity in usage.ts)", () => {
if ("quotas" in result) {
const quota = result.quotas["gemini-3.7-flash-high"];
assert.ok(quota, "should have quota for gemini-3.7-flash-high");
assert.equal(quota.remainingPercentage, 0, "remaining should be 0%");
assert.equal(quota.remainingPercentage, undefined, "unknown quota must not become 0%");
assert.equal(quota.fractionReported, false, "missing fraction should be marked unknown");
assert.equal(quota.unlimited, false, "should not be unlimited");
assert.equal(quota.used > 0, true, "used should be > 0 when quota is exhausted");
assert.equal(quota.used, 0, "unknown quota must not report usage");
}
} finally {
globalThis.fetch = originalFetch;

View File

@@ -142,6 +142,15 @@ test("registerGenericQuotaFetchers registers Claude, GLM, and OpenCode Go via th
// semantics are exercised by the source code review.
});
test("convertUsageToQuotaInfo skips Antigravity quota entries with an unknown fraction", () => {
const result = convertUsageToQuotaInfo({
quotas: {
gemini: { fractionReported: false, resetAt: "2026-05-14T20:00:00Z" },
},
});
assert.equal(result, null);
});
test.afterEach(() => {
__setGenericUsageFetcherForTests(null);
__resetGenericQuotaFetcherForTests();
@@ -329,7 +338,11 @@ test("in-flight fetch must not drop a concurrent 429 force-refresh", async () =>
assert.equal(first?.percentUsed, 0.2);
const second = await fetchGenericQuota(connectionId, connection);
assert.equal(calls.length, 2, "concurrent 429 must not let the in-flight recache wipe force-refresh");
assert.equal(
calls.length,
2,
"concurrent 429 must not let the in-flight recache wipe force-refresh"
);
assert.equal(calls[1]?.forceRefresh, true);
assert.equal(second?.percentUsed, 0.9);
invalidateGenericQuotaCache("agy", connectionId);