fix(combo): keep Antigravity Gemini usable when Claude weekly is empty (#12637)

Validado em lote numa worktree combinada com os 4 PRs desta leva sobre o tip de `release/v3.8.51`: `typecheck:core` limpo e **119/119** nos 9 arquivos de teste que trazem.

Três dos quatro conflitavam apenas no `config/quality/file-size-baseline.json`, todos de forma aditiva (chaves `_rebaseline_` distintas que devem coexistir); resolvidos com validação de JSON a cada passo.

Registro que o **#12637 não é duplicata do #12566**, apesar do título quase idêntico: o autor documenta que aquele escopou o cooldown de preflight por família e este cobre o `genericQuotaFetcher`, que é o que o roteamento reset-aware efetivamente chama. Traz também validação ao vivo em VPS (imagem X500, `onmi-gemini3.6` → HTTP 200), satisfazendo a Hard Rule #18.

Obrigado, @HouMinXi.
This commit is contained in:
Bob.Hou
2026-09-03 22:47:28 -04:00
committed by GitHub
parent d36d077a4d
commit 2a6eff0aec
9 changed files with 406 additions and 47 deletions

View File

@@ -0,0 +1 @@
Keep Antigravity Gemini usable when the same connection's Claude weekly quota is empty; generic quota cache stays per-connection for every other provider.

View File

@@ -1,4 +1,5 @@
{
"_rebaseline_2026_09_03_reset_aware_model_family": "Own growth: open-sse/services/combo.ts 4036->4041 (+5). buildAutoCandidates now keys the reset-aware quota cache by getQuotaFetchScope and spreads requestedModel onto the connection so Gemini windows stay off a Claude-empty Antigravity account. Irreducible wiring at the existing fetchResetAwareQuotaWithCache call site; the family helper itself lives in antigravityQuotaFamily.ts. Covered by tests/unit/reset-aware-request-scope-12600.test.ts.",
"_rebaseline_2026_09_03_overloaded_not_provider_breaker": "fix/overloaded-not-provider-breaker own growth: open-sse/services/combo.ts 4036->4075 (check-file-size split-newline, +39). Circuit-open pre-skip now records the breaker retryAfter and, when every target was skipped that way, waits the short reset via resolveCircuitOpenWaitDecision (new leaf in comboCooldownRetry.ts) instead of crystallizing ALL_TARGETS_SKIPPED in ~43ms. skippedForCircuitOpen / earliestCircuitOpenRetryMs reset each setTry so a later iteration cannot inherit a stale retryAfter. Irreducible at the existing ALL_TARGETS_SKIPPED chokepoint (same pattern as #7301/#8213 cooldown-wait). Predicate itself lives in circuitBreaker.ts / comboPredicates.ts / chatPredicates.ts, all under cap. Covered by tests/unit/overloaded-not-provider-breaker.test.ts + combo-cooldown-retry.test.ts.",
"_rebaseline_2026_09_03_12649_free_tier_reaudit_gateways": "PR #12649 (fix/free-tier-quota-reaudit) own growth: src/shared/constants/providers/apikey/gateways.ts 1459->1462 (+3 = the nara authHint rewritten for the re-audited 7M/day plan now wraps to two lines, plus the Prettier reflow of two pre-existing >100-col authHint lines (oneminai, freebuff) that lint-staged enforces on any touch of the file; additive text at the existing registry chokepoint, same god-file no-split rationale as prior gateways.ts rebaselines: #11786 seekai, #10987 logfare, #10531 freebuff). Covered by tests/unit/free-tier-reaudit-2026-09.test.ts and tests/unit/free-providers-batch-2026-07.test.ts.",
"_rebaseline_2026_09_03_moonshot_native_quota": "PR feat/moonshot-native-quota own growth on release/v3.8.51: src/lib/db/migrationRunner.ts 1201->1206 (+5, case 172 retroactive guard for daily_quota_reset_* columns); src/sse/handlers/chat.ts 2434->2450 (+16, registerMoonshotQuotaFetcher + startup node scan at the existing quota-fetcher registration chokepoint); src/sse/services/auth.ts 3427->3450 (+23, resolveDailyResetForProvider + dailyReset arg on checkFallbackError); open-sse/services/accountFallback.ts 2422->2461 (+39, compatible-node credits_exhausted carve-out + TPD node-clock lock); tests/unit/account-fallback-service.test.ts 2008->2056 (+48, TPD/empty-wallet cases). Wiring at existing chokepoints; Moonshot host predicates, daily reset clock, and the balance fetcher live in new leaves under cap. Covered by tests/unit/moonshot-*.test.ts + account-fallback-service.test.ts (135/135 focused).",
@@ -424,6 +425,7 @@
"open-sse/mcp-server/server.ts": 1572,
"open-sse/services/accountFallback.ts": 2467,
"open-sse/services/adobeFireflyBrowserLogin.ts": 1401,
"open-sse/services/combo.ts": 4041,
"open-sse/services/combo.ts": 4075,
"open-sse/translator/response/openai-responses.ts": 1466,
"open-sse/utils/cursorAgentProtobuf.ts": 1547,

View File

@@ -55,6 +55,14 @@ export function getQuotaScopeLabelForProvider(
return getAntigravityQuotaFamily(model) === "other" ? "model" : "family";
}
export function getQuotaFetchScope(
provider: string | null | undefined,
model: string | null | undefined
): string {
if (provider !== "antigravity" && provider !== "agy") return "*";
return getQuotaScopedModelForProvider(provider, model) ?? "*";
}
export function isAntigravityQuotaProvider(provider: string | null | undefined): boolean {
return provider === "antigravity" || provider === "agy";
}

View File

@@ -69,6 +69,7 @@ import { resolveModelLockoutSettings } from "../../src/lib/resilience/modelLocko
import { fetchCodexQuota } from "./codexQuotaFetcher.ts";
import { evaluateQuotaCutoff, getQuotaFetcher, type QuotaInfo } from "./quotaPreflight.ts";
import { resolveProviderId } from "../../src/shared/constants/providers.ts";
import { getQuotaFetchScope } from "./antigravityQuotaFamily.ts";
import * as semaphore from "./rateLimitSemaphore.ts";
import { getCircuitBreaker } from "../../src/shared/utils/circuitBreaker";
import { parseModel } from "./model.ts";
@@ -596,14 +597,17 @@ export async function buildAutoCandidates(
statusPenaltyReason = connectionStatusReason;
}
if (fetcher && target.connectionId) {
const quotaKey = `${provider}:${target.connectionId}`;
const quotaScope = getQuotaFetchScope(provider, target.modelStr);
const quotaKey = `${provider}:${target.connectionId}:${quotaScope}`;
if (!quotaPromises.has(quotaKey)) {
quotaPromises.set(
quotaKey,
fetchResetAwareQuotaWithCache({
provider,
connectionId: target.connectionId,
connection,
connection: connection
? { ...connection, requestedModel: target.modelStr }
: connection,
fetcher,
config: resetWindowConfig,
log: {},
@@ -1393,7 +1397,8 @@ async function handleComboChatInner({
resilienceSettings,
quotaCutoffResetWindowConfig,
combo.name,
log, modelStr
log,
modelStr
);
if (quotaCutoff.blocked) {
log.info(

View File

@@ -119,7 +119,7 @@ export async function resolveQuotaExhaustionCutoffForTarget(
const quota = await fetchResetAwareQuotaWithCache({
provider,
connectionId,
connection,
connection: connection ? { ...connection, requestedModel } : connection,
fetcher,
config: resetWindowConfig,
log,

View File

@@ -46,6 +46,7 @@ import {
} from "./quotaScoring.ts";
import { rankByHeadroom, type HeadroomSaturation } from "./headroomRanking.ts";
import { preferAntigravityConnectionsWithStoredProject } from "../antigravityProjectPersist.ts";
import { getQuotaFetchScope } from "../antigravityQuotaFamily.ts";
import { isQuotaExhaustedForRequest } from "../../../src/domain/quotaCache.ts";
const RESET_AWARE_CONNECTION_CACHE_TTL_MS = 30_000;
@@ -269,14 +270,17 @@ async function scoreQuotaAwareTargets<TScore extends object>({
const provider = getResetAwareProvider(target);
const fetcher = provider ? getQuotaFetcher(provider) : null;
if (fetcher && provider && target.connectionId) {
const quotaKey = `${provider}:${target.connectionId}`;
const quotaKey = `${provider}:${target.connectionId}:${getQuotaFetchScope(provider, target.modelStr)}`;
if (!quotaPromises.has(quotaKey)) {
const connection = connectionById.get(target.connectionId);
quotaPromises.set(
quotaKey,
fetchResetAwareQuotaWithCache({
provider,
connectionId: target.connectionId,
connection: connectionById.get(target.connectionId),
connection: connection
? { ...connection, requestedModel: target.modelStr }
: connection,
fetcher,
config,
log,
@@ -354,7 +358,10 @@ export async function fetchResetAwareQuotaWithCache({
log: { debug?: (...args: unknown[]) => void; warn?: (...args: unknown[]) => void };
comboName: string;
}): Promise<unknown> {
const cacheKey = `${provider}:${connectionId}`;
const requestedModel =
typeof connection?.requestedModel === "string" ? connection.requestedModel : null;
const cacheScope = getQuotaFetchScope(provider, requestedModel);
const cacheKey = `${provider}:${connectionId}:${cacheScope}`;
const ttlMs = config.quotaCacheTtlMs;
const maxStaleMs = config.quotaCacheMaxStaleMs;
const now = Date.now();

View File

@@ -24,6 +24,10 @@ import {
type QuotaFetcher,
type QuotaInfo,
} from "./quotaPreflight.ts";
import {
getAntigravityQuotaFamily,
getQuotaFetchScope,
} from "./antigravityQuotaFamily.ts";
type UsageFetcher = (
connection: Parameters<typeof getUsageForProvider>[0],
@@ -54,7 +58,7 @@ export function __agePendingForceRefreshForTests(
connectionId: string,
ageMs: number
): void {
pendingForceRefresh.set(cacheKey(provider, connectionId), Date.now() - ageMs);
pendingForceRefresh.set(connectionKey(provider, connectionId), Date.now() - ageMs);
}
/** Test-only: backdate a convert-null miss so the 60s hammer-guard is unit-testable. */
@@ -63,7 +67,7 @@ export function __agePendingForceRefreshMissForTests(
connectionId: string,
ageMs: number
): void {
pendingForceRefreshMiss.set(cacheKey(provider, connectionId), Date.now() - ageMs);
pendingForceRefreshMiss.set(connectionKey(provider, connectionId), Date.now() - ageMs);
}
/** Test-only: drop all wrapper/flag maps so tests cannot leak across ids. */
@@ -80,10 +84,25 @@ interface CacheEntry {
const cache = new Map<string, CacheEntry>();
function cacheKey(provider: string, connectionId: string): string {
function connectionKey(provider: string, connectionId: string): string {
return `${provider.trim()}::${connectionId.trim()}`;
}
function quotaCacheScope(
provider: string,
requestedModel?: string | null
): string {
return getQuotaFetchScope(provider, requestedModel);
}
function cacheKey(
provider: string,
connectionId: string,
requestedModel?: string | null
): string {
return `${connectionKey(provider, connectionId)}::${quotaCacheScope(provider, requestedModel)}`;
}
function dropExpiredPendingForceRefresh(key: string, now: number): boolean {
const stampedAt = pendingForceRefresh.get(key);
if (stampedAt === undefined) return true;
@@ -216,7 +235,15 @@ interface ConnectionInputs {
* / shape-unknown / missing). Exported for unit testing — the production path
* is `fetchGenericQuota`, which adds caching + the upstream call.
*/
export function convertUsageToQuotaInfo(usage: unknown): QuotaInfo | null {
type UsageToQuotaContext = {
requestedModel?: string | null;
provider?: string | null;
};
export function convertUsageToQuotaInfo(
usage: unknown,
context: UsageToQuotaContext = {}
): QuotaInfo | null {
if (!usage || typeof usage !== "object") return null;
const usageRecord = usage as Record<string, unknown>;
if (
@@ -235,31 +262,51 @@ export function convertUsageToQuotaInfo(usage: unknown): QuotaInfo | null {
}
const windows: Record<string, { percentUsed: number; resetAt: string | null }> = {};
let worstPercent = 0;
let worstResetAt: string | null = null;
for (const [name, entry] of Object.entries(quotasObj as Record<string, unknown>)) {
const percentUsed = percentUsedForQuota(entry);
if (percentUsed === null) continue;
const resetAt = resetAtForQuota(entry);
windows[name] = { percentUsed, resetAt };
if (percentUsed > worstPercent) {
worstPercent = percentUsed;
worstResetAt = resetAt;
}
windows[name] = { percentUsed, resetAt: resetAtForQuota(entry) };
}
if (Object.keys(windows).length === 0) return null;
const normalized = normalizeQuotaWindows(windows);
const requestedFamily =
isAntigravityProvider(context.provider) && context.requestedModel
? getAntigravityQuotaFamily(context.requestedModel)
: null;
const providerScopedWindows =
requestedFamily === "gemini" || requestedFamily === "claude"
? Object.fromEntries(
Object.entries(windows).filter(([key]) => {
if (key.endsWith("_weekly")) {
return antigravityWeeklyWindowMatchesFamily(key, requestedFamily);
}
return getAntigravityQuotaFamily(key) === requestedFamily;
})
)
: windows;
if (Object.keys(providerScopedWindows).length === 0) return null;
const normalized = normalizeQuotaWindows(providerScopedWindows, context);
const scopedEntries = Object.values(providerScopedWindows);
const percentUsed = scopedEntries.reduce(
(worst, entry) => Math.max(worst, entry.percentUsed),
0
);
const resetAt =
scopedEntries.reduce<{ percentUsed: number; resetAt: string | null } | null>(
(worst, entry) => (!worst || entry.percentUsed > worst.percentUsed ? entry : worst),
null
)?.resetAt ?? null;
return {
used: 0,
total: 0,
percentUsed: worstPercent,
resetAt: worstResetAt,
windows,
percentUsed,
resetAt,
windows: providerScopedWindows,
...normalized,
limitReached: worstPercent >= 1 - 1e-9,
limitReached: percentUsed >= 1 - 1e-9,
};
}
@@ -269,12 +316,29 @@ export function convertUsageToQuotaInfo(usage: unknown): QuotaInfo | null {
* naming convention.
*
* - Claude: "session (5h)" → window5h, "weekly (7d)" → window7d
* - Antigravity: worst per-model quota → window5h; worst *_weekly quota → window7d
* - Antigravity: requested-family model quota → window5h; matching family weekly quota → window7d
*/
function isAntigravityProvider(provider: string | null | undefined): boolean {
return provider === "antigravity" || provider === "agy";
}
function antigravityWeeklyWindowMatchesFamily(
key: string,
family: "gemini" | "claude"
): boolean {
if (!key.endsWith("_weekly")) return false;
return family === "gemini" ? key === "gemini_weekly" : key === "claude_gpt_weekly";
}
function normalizeQuotaWindows(
windows: Record<string, { percentUsed: number; resetAt: string | null }>
windows: Record<string, { percentUsed: number; resetAt: string | null }>,
context: UsageToQuotaContext
): Record<string, { percentUsed: number; resetAt: string | null }> {
const normalized: Record<string, { percentUsed: number; resetAt: string | null }> = {};
const requestedFamily =
isAntigravityProvider(context.provider) && context.requestedModel
? getAntigravityQuotaFamily(context.requestedModel)
: null;
// Claude-style explicit time windows.
if (windows["session (5h)"] && !normalized.window5h) {
@@ -284,22 +348,31 @@ function normalizeQuotaWindows(
normalized.window7d = windows["weekly (7d)"];
}
// Antigravity-style per-model 5h windows: pick the worst (most used) model quota.
// Antigravity-style per-model windows: pick worst only inside requested family.
const modelWindows = Object.entries(windows).filter(
([key]) =>
key !== "credits" &&
!key.endsWith("_weekly") &&
!key.startsWith("window") &&
!key.includes("(5h)") &&
!key.includes("(7d)")
!key.includes("(7d)") &&
(requestedFamily === null ||
requestedFamily === "other" ||
getAntigravityQuotaFamily(key) === requestedFamily)
);
if (modelWindows.length > 0 && !normalized.window5h) {
const worst = modelWindows.reduce((a, b) => (a[1].percentUsed > b[1].percentUsed ? a : b));
normalized.window5h = worst[1];
}
// Antigravity-style weekly family buckets: pick the worst *_weekly quota.
const weeklyWindows = Object.entries(windows).filter(([key]) => key.endsWith("_weekly"));
// Antigravity-style weekly buckets: pick worst only inside requested family.
const weeklyWindows = Object.entries(windows).filter(([key]) => {
const hasFamilyScope = requestedFamily === "gemini" || requestedFamily === "claude";
return (
key.endsWith("_weekly") &&
(!hasFamilyScope || antigravityWeeklyWindowMatchesFamily(key, requestedFamily))
);
});
if (weeklyWindows.length > 0 && !normalized.window7d) {
const worst = weeklyWindows.reduce((a, b) => (a[1].percentUsed > b[1].percentUsed ? a : b));
normalized.window7d = worst[1];
@@ -320,18 +393,21 @@ export const fetchGenericQuota: QuotaFetcher = async (connectionId, connection)
const provider = typeof conn.provider === "string" ? conn.provider.trim() : "";
if (!provider) return null;
const key = cacheKey(provider, connectionId);
const requestedModel =
typeof connection.requestedModel === "string" ? connection.requestedModel : undefined;
const key = cacheKey(provider, connectionId, requestedModel);
const forceKey = connectionKey(provider, connectionId);
const now = Date.now();
const forceRefresh = isPendingForceRefresh(key, now);
const forceRefresh = isPendingForceRefresh(forceKey, now);
const hit = cachedQuotaIfFresh(key, forceRefresh, now);
if (hit) return hit;
// convert-null / throw keep the force-refresh flag (agy inner caches are
// still stale) but must not hammer those endpoints on every routing tick.
if (isForceRefreshMissCooling(key, forceRefresh, now)) return null;
if (isForceRefreshMissCooling(forceKey, forceRefresh, now)) return null;
// Capture before await: a 429 during fetchUsage re-stamps this; writing
// the pre-429 snapshot would wipe that flag and recache stale quota.
const refreshStamp = pendingForceRefresh.get(key);
const refreshStamp = pendingForceRefresh.get(forceKey);
let usage: unknown;
try {
@@ -340,28 +416,29 @@ export const fetchGenericQuota: QuotaFetcher = async (connectionId, connection)
...(forceRefresh ? { forceRefresh: true } : {}),
});
} catch {
markPendingForceRefreshMiss(key);
markPendingForceRefreshMiss(forceKey);
return null;
}
const quota = convertUsageToQuotaInfo(usage);
const quota = convertUsageToQuotaInfo(usage, { provider, requestedModel });
if (!quota) {
markPendingForceRefreshMiss(key);
markPendingForceRefreshMiss(forceKey);
return null;
}
// Concurrent 429 re-stamped a still-live flag — do not recache the
// pre-429 snapshot. A vanished or expired stamp is not a 429.
if (isConcurrentForceRefresh(key, refreshStamp)) {
if (isConcurrentForceRefresh(forceKey, refreshStamp)) {
return quota;
}
pendingForceRefresh.delete(key);
pendingForceRefreshMiss.delete(key);
pendingForceRefresh.delete(forceKey);
pendingForceRefreshMiss.delete(forceKey);
// Refresh the static window catalog so the dashboard can render the right
// modal inputs without waiting for the user to open the page.
registerQuotaWindows(provider, Object.keys(quota.windows || {}));
// Refresh the static window catalog from the unscoped usage payload so a
// family-scoped request cannot hide sibling-family dashboard controls.
const unscopedQuota = convertUsageToQuotaInfo(usage, { provider });
registerQuotaWindows(provider, Object.keys(unscopedQuota?.windows || quota.windows || {}));
cache.set(key, { quota, fetchedAt: Date.now() });
return quota;
@@ -373,13 +450,16 @@ export const fetchGenericQuota: QuotaFetcher = async (connectionId, connection)
* fresh data instead of a 60s stale window.
*/
export function invalidateGenericQuotaCache(provider: string, connectionId: string): void {
const key = cacheKey(provider, connectionId);
cache.delete(key);
const forceKey = connectionKey(provider, connectionId);
const prefix = `${forceKey}::`;
for (const key of cache.keys()) {
if (key.startsWith(prefix)) cache.delete(key);
}
// Next fetch must bypass provider-inner usage caches (agy retrieveUserQuota /
// weekly are 60s5min). Without this, dropping the 60s wrapper recaches stale.
// TTL matches those inner caches: after 5min the flag is a no-op.
pendingForceRefresh.set(key, Date.now());
pendingForceRefreshMiss.delete(key);
pendingForceRefresh.set(forceKey, Date.now());
pendingForceRefreshMiss.delete(forceKey);
}
/**
@@ -415,3 +495,15 @@ export function registerGenericQuotaFetchers(): void {
registerQuotaFetcher(provider, fetchGenericQuota);
}
}
export const __testing = {
setUsageFetcher(fetcher: UsageFetcher): void {
usageFetcherOverride = fetcher;
},
resetUsageFetcher(): void {
usageFetcherOverride = null;
},
clearCache(): void {
cache.clear();
},
};

View File

@@ -17,6 +17,8 @@ const { clearAllStickyBindings } =
const { invalidateCodexQuotaCache, registerCodexConnection, registerCodexQuotaFetcher } =
await import("../../open-sse/services/codexQuotaFetcher.ts");
const { registerQuotaFetcher } = await import("../../open-sse/services/quotaPreflight.ts");
const { getQuotaScopedModelForProvider } =
await import("../../open-sse/services/antigravityQuotaFamily.ts");
const combosDb = await import("../../src/lib/db/combos.ts");
const providersDb = await import("../../src/lib/db/providers.ts");
const { recordComboRequest } = await import("../../open-sse/services/comboMetrics.ts");
@@ -434,6 +436,14 @@ test("reset-aware strategy avoids accounts near 5h exhaustion", async (t) => {
assert.equal(await selectedConnectionFor(combo), healthy5h.id);
});
test("Antigravity aliases share one family-scoped cache key", () => {
assert.equal(getQuotaScopedModelForProvider("agy", "gemini-3.7-flash-high"), "family:gemini");
assert.equal(
getQuotaScopedModelForProvider("antigravity", "gemini-3.7-flash-high"),
"family:gemini"
);
});
test("reset-aware strategy rotates similar scores with round-robin tie breaking", async () => {
const provider = `tie-provider-${randomUUID()}`;
const first = `first-${randomUUID()}`;

View File

@@ -0,0 +1,234 @@
import test, { afterEach } from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
const genericModule = await import("../../open-sse/services/genericQuotaFetcher.ts");
const scoringModule = await import("../../open-sse/services/combo/quotaScoring.ts");
const familyModule = await import("../../open-sse/services/antigravityQuotaFamily.ts");
const preflightModule = await import("../../open-sse/services/quotaPreflight.ts");
const { convertUsageToQuotaInfo, fetchGenericQuota, invalidateGenericQuotaCache } = genericModule;
const { scoreResetAwareQuota, resolveResetAwareConfig } = scoringModule;
const { getQuotaFetchScope } = familyModule;
const { getQuotaWindows } = preflightModule;
const resetAt5h = new Date(Date.now() + 4 * 60 * 60 * 1000).toISOString();
const resetAt7d = new Date(Date.now() + 6 * 24 * 60 * 60 * 1000).toISOString();
const usage = {
quotas: {
"gemini-3.7-flash-high": {
used: 30,
total: 1000,
remainingPercentage: 97,
resetAt: resetAt5h,
},
"claude-opus-4-6-thinking": {
used: 1000,
total: 1000,
remainingPercentage: 0,
resetAt: resetAt7d,
},
"gpt-oss-120b-medium": {
used: 900,
total: 1000,
remainingPercentage: 10,
resetAt: resetAt5h,
},
gemini_weekly: {
used: 10,
total: 1000,
remainingPercentage: 99,
resetAt: resetAt7d,
},
claude_gpt_weekly: {
used: 1000,
total: 1000,
remainingPercentage: 0,
resetAt: resetAt7d,
},
unrelated_weekly: {
used: 1000,
total: 1000,
remainingPercentage: 0,
resetAt: resetAt7d,
},
},
};
test("reset-aware Gemini scoring ignores depleted Claude family quota", () => {
const quota = convertUsageToQuotaInfo(usage, {
provider: "agy",
requestedModel: "agy/gemini-3.7-flash-high",
});
assert.ok(quota);
assert.equal(quota.window5h?.percentUsed, 0.03);
assert.equal(quota.window7d?.percentUsed, 0.01);
assert.equal(quota.percentUsed, 0.03);
assert.equal(quota.limitReached, false);
assert.equal(quota.windows?.["claude-opus-4-6-thinking"], undefined);
assert.equal(quota.windows?.["gpt-oss-120b-medium"], undefined);
assert.equal(quota.windows?.claude_gpt_weekly, undefined);
assert.equal(quota.windows?.unrelated_weekly, undefined);
assert.ok(scoreResetAwareQuota(quota, resolveResetAwareConfig({})).score > 0.3);
});
test("opposite-family-only telemetry fails open as unknown", () => {
const gemini = convertUsageToQuotaInfo(
{ quotas: { claude_gpt_weekly: usage.quotas.claude_gpt_weekly } },
{ provider: "agy", requestedModel: "gemini-3.7-flash-high" }
);
const claude = convertUsageToQuotaInfo(
{ quotas: { gemini_weekly: usage.quotas.gemini_weekly } },
{ provider: "antigravity", requestedModel: "claude-opus-4-6-thinking" }
);
assert.equal(gemini, null);
assert.equal(claude, null);
assert.equal(scoreResetAwareQuota(gemini, resolveResetAwareConfig({})).score, 0.5);
assert.equal(scoreResetAwareQuota(claude, resolveResetAwareConfig({})).score, 0.5);
});
test("Claude family excludes unknown weekly buckets", () => {
const quota = convertUsageToQuotaInfo(
{
quotas: {
"claude-opus-4-6-thinking": {
used: 100,
total: 1000,
remainingPercentage: 90,
resetAt: resetAt5h,
},
claude_gpt_weekly: {
used: 100,
total: 1000,
remainingPercentage: 90,
resetAt: resetAt7d,
},
unrelated_weekly: usage.quotas.unrelated_weekly,
},
},
{ provider: "agy", requestedModel: "claude-opus-4-6-thinking" }
);
assert.ok(quota);
assert.equal(quota.limitReached, false);
assert.equal(quota.windows?.unrelated_weekly, undefined);
assert.equal(quota.window7d?.percentUsed, 0.1);
});
test("unscoped provider-limits conversion retains conservative global windows", () => {
const quota = convertUsageToQuotaInfo(usage);
assert.ok(quota);
assert.equal(quota.window5h?.percentUsed, 1);
assert.equal(quota.window7d?.percentUsed, 1);
assert.equal(quota.limitReached, true);
});
test("reset-aware fetch scope is family-wide for Antigravity and * otherwise", () => {
assert.equal(getQuotaFetchScope("agy", "gemini-3.7-flash-high"), "family:gemini");
assert.equal(getQuotaFetchScope("antigravity", "claude-opus-4-6-thinking"), "family:claude");
assert.equal(getQuotaFetchScope("codex", "gpt-5"), "*");
});
test("buildAutoCandidates uses the shared Antigravity fetch-scope helper", () => {
const combo = fs.readFileSync(new URL("../../open-sse/services/combo.ts", import.meta.url), "utf8");
const strategies = fs.readFileSync(
new URL("../../open-sse/services/combo/quotaStrategies.ts", import.meta.url),
"utf8"
);
assert.match(combo, /getQuotaFetchScope\(/);
assert.doesNotMatch(
combo,
/provider === "antigravity" \|\| provider === "agy"\s*\n\s*\? getQuotaScopedModelForProvider/
);
assert.match(strategies, /getQuotaFetchScope\(/);
assert.doesNotMatch(strategies, /function getQuotaFetchScope/);
});
afterEach(() => {
genericModule.__testing?.resetUsageFetcher?.();
genericModule.__testing?.clearCache?.();
});
test("fetchGenericQuota scopes Gemini windows and still catalogs sibling families", async () => {
let fetches = 0;
genericModule.__testing.setUsageFetcher(async () => {
fetches += 1;
return usage;
});
const quota = await fetchGenericQuota("conn-gemini", {
provider: "agy",
requestedModel: "agy/gemini-3.7-flash-high",
});
assert.ok(quota);
assert.equal(quota.window5h?.percentUsed, 0.03);
assert.equal(quota.limitReached, false);
assert.equal(quota.windows?.claude_gpt_weekly, undefined);
assert.equal(quota.windows?.["claude-opus-4-6-thinking"], undefined);
const windows = getQuotaWindows("agy");
assert.equal(windows.includes("claude_gpt_weekly"), true);
assert.equal(windows.includes("gemini_weekly"), true);
assert.equal(fetches, 1);
});
test("invalidateGenericQuotaCache clears every family-scoped entry for a connection", async () => {
let fetches = 0;
genericModule.__testing.setUsageFetcher(async () => {
fetches += 1;
return usage;
});
const connectionId = "conn-both-families";
await fetchGenericQuota(connectionId, {
provider: "agy",
requestedModel: "gemini-3.7-flash-high",
});
await fetchGenericQuota(connectionId, {
provider: "agy",
requestedModel: "claude-opus-4-6-thinking",
});
assert.equal(fetches, 2);
await fetchGenericQuota(connectionId, {
provider: "agy",
requestedModel: "gemini-3.7-flash-high",
});
assert.equal(fetches, 2);
invalidateGenericQuotaCache("agy", connectionId);
await fetchGenericQuota(connectionId, {
provider: "agy",
requestedModel: "gemini-3.7-flash-high",
});
await fetchGenericQuota(connectionId, {
provider: "agy",
requestedModel: "claude-opus-4-6-thinking",
});
assert.equal(fetches, 4);
});
test("non-Antigravity generic quota cache stays per connection, not per model", async () => {
let fetches = 0;
genericModule.__testing.setUsageFetcher(async () => {
fetches += 1;
return usage;
});
const connectionId = "conn-kimi";
await fetchGenericQuota(connectionId, {
provider: "kimi",
requestedModel: "kimi-k2.5",
});
await fetchGenericQuota(connectionId, {
provider: "kimi",
requestedModel: "kimi-k2.7",
});
assert.equal(fetches, 1);
});