Files
2026-07-13 13:39:12 +08:00

795 lines
28 KiB
TypeScript

/**
* usage/antigravity.ts — Antigravity (Gemini Code Assist) usage fetcher + quota helpers.
*
* Extracted from services/usage.ts (god-file decomposition): the full Antigravity family —
* local-usage fallback, code-assist tier/plan mapping, credit-balance probing, the user-quota
* + available-models fetchers (with their module-level caches), and getAntigravityUsage. The
* 4 data caches + their proactive TTL-purge setInterval move here as a self-contained unit
* (previously the purge timer lived in usage.ts; that timer was
* split so each module owns its own caches + cleanup). usage.ts imports getAntigravityUsage
* (dispatcher) + getAntigravityPlanLabel/mapCodeAssist* (__testing). Behavior-preserving move.
*/
import { PROVIDERS } from "../../config/constants.ts";
import {
getAntigravityFetchAvailableModelsUrls,
ANTIGRAVITY_BASE_URLS,
} from "../../config/antigravityUpstream.ts";
import {
isUserCallableAntigravityModelId,
toClientAntigravityQuotaModelId,
} from "../../config/antigravityModelAliases.ts";
import { isUserCallableAgyModelId } from "../../config/agyModels.ts";
import { getDbInstance } from "@/lib/db/core";
import {
applyAntigravityClientProfileHeaders,
getAntigravityBootstrapHeaders,
getAntigravityClientProfile,
} from "../antigravityClientProfile.ts";
import {
antigravityUserAgent,
getAntigravityHeaders,
getAntigravityLoadCodeAssistMetadata,
} from "../antigravityHeaders.ts";
import {
getAntigravityRemainingCredits,
updateAntigravityRemainingCredits,
} from "../../executors/antigravity.ts";
import { getCreditsMode } from "../antigravityCredits.ts";
import { generateAntigravityRequestId, getAntigravitySessionId } from "../antigravityIdentity.ts";
import {
extractCodeAssistOnboardTierId,
extractCodeAssistSubscriptionTier,
} from "../codeAssistSubscription.ts";
import { toRecord, toNumber, getFieldValue } from "./scalars.ts";
import { type UsageQuota, parseResetTime } from "./quota.ts";
type JsonRecord = Record<string, unknown>;
type SubscriptionCacheEntry = {
data: unknown;
fetchedAt: number;
};
const ANTIGRAVITY_CONFIG = {
quotaApiUrls: getAntigravityFetchAvailableModelsUrls(),
loadProjectApiUrl: "https://daily-cloudcode-pa.sandbox.googleapis.com/v1internal:loadCodeAssist",
tokenUrl: "https://oauth2.googleapis.com/token",
get clientId() {
return PROVIDERS.antigravity.clientId;
},
get clientSecret() {
return PROVIDERS.antigravity.clientSecret;
},
get userAgent() {
return antigravityUserAgent();
},
};
const _antigravitySubCache = new Map<string, SubscriptionCacheEntry>();
const ANTIGRAVITY_CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes
const ANTIGRAVITY_MODELS_CACHE_TTL_MS = 60 * 1000;
const ANTIGRAVITY_CREDIT_PROBE_TTL_MS = 5 * 60 * 1000;
const _antigravityAvailableModelsCache = new Map<string, { data: unknown; fetchedAt: number }>();
const _antigravityAvailableModelsInflight = new Map<string, Promise<unknown>>();
const _antigravityUserQuotaCache = new Map<string, { data: unknown; fetchedAt: number }>();
const _antigravityUserQuotaInflight = new Map<string, Promise<unknown>>();
const _antigravityCreditProbeCache = new Map<string, { data: number | null; fetchedAt: number }>();
const _antigravityCreditProbeInflight = new Map<string, Promise<number | null>>();
// ── Proactive TTL purging for the Antigravity module-level caches ──────────
// Split out of the shared usage.ts cleanup timer (god-file decomposition): this
// leaf owns its 4 data caches, so it owns their purge too. The 2 inflight Maps
// self-clean when their Promise settles, so they are NOT swept here.
const _antigravityCacheCleanupTimer = setInterval(
() => {
const now = Date.now();
for (const [key, entry] of _antigravitySubCache) {
if (now - entry.fetchedAt > ANTIGRAVITY_CACHE_TTL_MS) _antigravitySubCache.delete(key);
}
for (const [key, entry] of _antigravityAvailableModelsCache) {
if (now - entry.fetchedAt > ANTIGRAVITY_MODELS_CACHE_TTL_MS)
_antigravityAvailableModelsCache.delete(key);
}
for (const [key, entry] of _antigravityUserQuotaCache) {
if (now - entry.fetchedAt > ANTIGRAVITY_MODELS_CACHE_TTL_MS)
_antigravityUserQuotaCache.delete(key);
}
for (const [key, entry] of _antigravityCreditProbeCache) {
if (now - entry.fetchedAt > ANTIGRAVITY_CREDIT_PROBE_TTL_MS)
_antigravityCreditProbeCache.delete(key);
}
},
5 * 60 * 1000
); // every 5 minutes
_antigravityCacheCleanupTimer.unref?.(); // Don't prevent process exit
interface AntigravityUsageOptions {
forceRefresh?: boolean;
}
const ANTIGRAVITY_LOCAL_USAGE_WINDOW_MS = 5 * 60 * 60 * 1000;
const ANTIGRAVITY_LOCAL_USAGE_TOKENS_PER_UNIT = 1000;
// `toClientAntigravityQuotaModelId` was an inline if-ladder here; it is now the single
// source of truth in open-sse/config/antigravityModelAliases.ts (imported above), shared
// with the provider-limits cache sanitizer. (#3821-review LEDGER-5)
function getAntigravityLocalUsageUnits(
provider: "antigravity" | "agy",
connectionId: string | undefined,
modelId: string,
resetAt: string | null
): number {
if (!connectionId || !modelId || !resetAt) return 0;
const resetMs = Date.parse(resetAt);
if (!Number.isFinite(resetMs)) return 0;
const windowStart = new Date(resetMs - ANTIGRAVITY_LOCAL_USAGE_WINDOW_MS).toISOString();
const windowEnd = new Date(resetMs).toISOString();
try {
const db = getDbInstance() as unknown as {
prepare: (sql: string) => { get: (...params: unknown[]) => unknown };
};
const row = db
.prepare(
`SELECT COALESCE(SUM(
COALESCE(tokens_input, 0) + COALESCE(tokens_output, 0) + COALESCE(tokens_reasoning, 0)
), 0) AS tokens
FROM usage_history
WHERE provider = ?
AND connection_id = ?
AND model = ?
AND success = 1
AND timestamp >= ?
AND timestamp < ?`
)
.get(provider, connectionId, modelId, windowStart, windowEnd) as
| { tokens?: unknown }
| undefined;
const tokens = Number(row?.tokens || 0);
if (!Number.isFinite(tokens) || tokens <= 0) return 0;
return Math.max(1, Math.ceil(tokens / ANTIGRAVITY_LOCAL_USAGE_TOKENS_PER_UNIT));
} catch {
return 0;
}
}
function applyLocalUsageFallback(
quota: UsageQuota,
provider: "antigravity" | "agy",
connectionId: string | undefined,
modelId: string
): UsageQuota {
if (quota.quotaSource !== "fetchAvailableModels" || quota.used > 0 || quota.unlimited) {
return quota;
}
const localUsed = getAntigravityLocalUsageUnits(provider, connectionId, modelId, quota.resetAt);
if (localUsed <= 0 || quota.total <= 0) return quota;
const used = Math.min(quota.total, localUsed);
return {
...quota,
used,
remainingPercentage: Math.max(0, ((quota.total - used) / quota.total) * 100),
quotaSource: "localUsageHistory",
};
}
function buildAntigravityUsageCacheKey(accessToken: string, projectId?: string | null): string {
return `${accessToken.substring(0, 16)}:${projectId || "default"}`;
}
async function fetchAntigravityAvailableModelsCached(
accessToken: string,
projectId?: string | null,
options: AntigravityUsageOptions = {}
): Promise<unknown> {
if (!accessToken) throw new Error("Access token is required");
const cacheKey = buildAntigravityUsageCacheKey(accessToken, projectId);
const cached = _antigravityAvailableModelsCache.get(cacheKey);
if (
!options.forceRefresh &&
cached &&
Date.now() - cached.fetchedAt < ANTIGRAVITY_MODELS_CACHE_TTL_MS
) {
return cached.data;
}
const inflight = _antigravityAvailableModelsInflight.get(cacheKey);
if (inflight) return inflight;
const promise = (async () => {
let response: Response | null = null;
let lastError: Error | null = null;
for (const quotaApiUrl of ANTIGRAVITY_CONFIG.quotaApiUrls) {
try {
response = await fetch(quotaApiUrl, {
method: "POST",
headers: getAntigravityHeaders("fetchAvailableModels", accessToken),
body: JSON.stringify(projectId ? { project: projectId } : {}),
signal: AbortSignal.timeout(10000),
});
if (response.ok || response.status === 401 || response.status === 403) {
break;
}
} catch (error) {
lastError = error as Error;
}
}
if (!response) {
throw lastError || new Error("Antigravity API unavailable");
}
if (response.status === 403) {
return { __antigravityForbidden: true };
}
if (!response.ok) {
throw new Error(`Antigravity API error: ${response.status}`);
}
const data = await response.json();
_antigravityAvailableModelsCache.set(cacheKey, { data, fetchedAt: Date.now() });
return data;
})().finally(() => {
_antigravityAvailableModelsInflight.delete(cacheKey);
});
_antigravityAvailableModelsInflight.set(cacheKey, promise);
return promise;
}
async function fetchAntigravityUserQuotaCached(
accessToken: string,
projectId?: string | null,
options: AntigravityUsageOptions = {}
): Promise<unknown | null> {
if (!accessToken || !projectId) return null;
const cacheKey = buildAntigravityUsageCacheKey(accessToken, projectId);
const cached = _antigravityUserQuotaCache.get(cacheKey);
if (
!options.forceRefresh &&
cached &&
Date.now() - cached.fetchedAt < ANTIGRAVITY_MODELS_CACHE_TTL_MS
) {
return cached.data;
}
const inflight = _antigravityUserQuotaInflight.get(cacheKey);
if (inflight) return inflight;
const promise = (async () => {
try {
const response = await fetch(
"https://cloudcode-pa.googleapis.com/v1internal:retrieveUserQuota",
{
method: "POST",
headers: {
Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ project: projectId }),
signal: AbortSignal.timeout(10000),
}
);
if (!response.ok) return null;
const data = await response.json();
_antigravityUserQuotaCache.set(cacheKey, { data, fetchedAt: Date.now() });
return data;
} catch {
return null;
}
})().finally(() => {
_antigravityUserQuotaInflight.delete(cacheKey);
});
_antigravityUserQuotaInflight.set(cacheKey, promise);
return promise;
}
function extractCodeAssistTierId(subscription: JsonRecord): string {
const tierId = extractCodeAssistOnboardTierId(subscription);
if (tierId === "legacy-tier") return "";
const upper = tierId.toUpperCase();
return mapCodeAssistTierIdToLabel(upper) ? upper : "";
}
export function mapCodeAssistTierIdToLabel(tierId: string): string | null {
const upper = tierId.toUpperCase();
if (upper.includes("ULTRA")) return "Ultra";
if (
upper.includes("PRO") ||
upper.includes("PREMIUM") ||
upper.includes("GOOGLE_ONE") ||
upper.includes("ONE_AI")
)
return "Pro";
if (upper.includes("ENTERPRISE")) return "Enterprise";
if (upper.includes("BUSINESS") || upper.includes("STANDARD")) return "Business";
if (upper.includes("PLUS")) return "Plus";
if (upper.includes("LITE") || upper.includes("LIGHT")) return "Lite";
if (upper.includes("FREE") || upper.includes("INDIVIDUAL") || upper.includes("LEGACY"))
return "Free";
return null;
}
export function mapSubscriptionTierStringToPlanLabel(tierText: string): string | null {
const upper = tierText.toUpperCase();
if (upper.includes("ULTRA")) return "Ultra";
if (upper.includes("PRO") || upper.includes("PREMIUM") || upper.includes("GOOGLE ONE"))
return "Pro";
if (upper.includes("ENTERPRISE")) return "Enterprise";
if (upper.includes("STANDARD") || upper.includes("BUSINESS")) return "Business";
if (upper.includes("PLUS")) return "Plus";
if (upper.includes("LITE")) return "Lite";
if (upper.includes("INDIVIDUAL") || upper.includes("FREE")) return "Free";
// Strip a trailing "(RESTRICTED)" marker. Match the fixed literal anywhere then
// trim, instead of /\s*\(RESTRICTED\)\s*$/ whose overlapping \s* runs backtrack
// polynomially on whitespace-heavy upstream input (js/polynomial-redos).
const normalizedId = upper.replace(/\(RESTRICTED\)/i, "").trim();
if (normalizedId) {
const mapped = mapCodeAssistTierIdToLabel(normalizedId);
if (mapped) return mapped;
}
return null;
}
export function mapCodeAssistSubscriptionToPlanLabel(subscriptionInfo: unknown): string {
const subscription = toRecord(subscriptionInfo);
if (Object.keys(subscription).length === 0) return "Free";
const subscriptionTier = extractCodeAssistSubscriptionTier(subscriptionInfo);
if (subscriptionTier) {
const mapped = mapSubscriptionTierStringToPlanLabel(subscriptionTier);
if (mapped) return mapped;
if (subscriptionTier.toLowerCase() !== "free") {
return subscriptionTier.charAt(0).toUpperCase() + subscriptionTier.slice(1).toLowerCase();
}
}
const currentTier = toRecord(subscription.currentTier);
const tierName = String(
getFieldValue(currentTier, "name", "displayName") ||
subscription.subscriptionType ||
subscription.tier ||
""
);
const mappedName = tierName ? mapSubscriptionTierStringToPlanLabel(tierName) : null;
if (mappedName) return mappedName;
const tierId = extractCodeAssistTierId(subscription);
if (tierId) {
const mapped = mapCodeAssistTierIdToLabel(tierId);
if (mapped) return mapped;
}
if (currentTier.upgradeSubscriptionType) return "Free";
if (tierName) return tierName.charAt(0).toUpperCase() + tierName.slice(1).toLowerCase();
return "Free";
}
const KNOWN_ANTIGRAVITY_PLAN_LABELS = new Set([
"Ultra",
"Pro",
"Enterprise",
"Business",
"Plus",
"Lite",
]);
/**
* Map raw loadCodeAssist tier data to short display labels (Antigravity Manager parity).
*/
export function getAntigravityPlanLabel(subscriptionInfo: unknown, fallbackInfo?: unknown): string {
const livePlan = mapCodeAssistSubscriptionToPlanLabel(subscriptionInfo);
const fallbackPlan = mapCodeAssistSubscriptionToPlanLabel(fallbackInfo);
if (KNOWN_ANTIGRAVITY_PLAN_LABELS.has(livePlan)) return livePlan;
if (KNOWN_ANTIGRAVITY_PLAN_LABELS.has(fallbackPlan)) return fallbackPlan;
if (livePlan !== "Free") return livePlan;
return fallbackPlan !== "Free" ? fallbackPlan : livePlan;
}
/**
* Proactive credit balance probe for Antigravity.
*
* Fires a minimal streamGenerateContent request with GOOGLE_ONE_AI credits enabled
* and maxOutputTokens=1 to extract the `remainingCredits` field from the SSE stream.
* This uses ~1 credit but lets us show the balance on the dashboard without waiting
* for a real user request.
*
* Returns the credit balance, or null if the probe failed.
*/
async function probeAntigravityCreditBalance(
accessToken: string,
accountId: string,
projectId?: string | null,
options: AntigravityUsageOptions = {},
providerSpecificData: JsonRecord = {}
): Promise<number | null> {
if (!accessToken) return null;
const cacheKey = buildAntigravityUsageCacheKey(accessToken, projectId || accountId);
const cached = _antigravityCreditProbeCache.get(cacheKey);
if (
!options.forceRefresh &&
cached &&
Date.now() - cached.fetchedAt < ANTIGRAVITY_CREDIT_PROBE_TTL_MS
) {
return cached.data;
}
const inflight = _antigravityCreditProbeInflight.get(cacheKey);
if (inflight) return inflight;
const promise = probeAntigravityCreditBalanceUncached(
accessToken,
accountId,
projectId,
providerSpecificData
)
.then(
(data) => {
_antigravityCreditProbeCache.set(cacheKey, { data, fetchedAt: Date.now() });
return data;
},
(error) => {
_antigravityCreditProbeCache.set(cacheKey, { data: null, fetchedAt: Date.now() });
throw error;
}
)
.finally(() => {
_antigravityCreditProbeInflight.delete(cacheKey);
});
_antigravityCreditProbeInflight.set(cacheKey, promise);
return promise;
}
async function probeAntigravityCreditBalanceUncached(
accessToken: string,
accountId: string,
projectId?: string | null,
providerSpecificData: JsonRecord = {}
): Promise<number | null> {
try {
if (!projectId) return null;
// Try all base URLs (some accounts only work with specific endpoints)
for (const baseUrl of ANTIGRAVITY_BASE_URLS) {
const url = `${baseUrl}/v1internal:streamGenerateContent?alt=sse`;
const sessionId = getAntigravitySessionId({ connectionId: accountId, projectId });
const body = {
project: projectId,
model: "gemini-2-flash",
userAgent: "antigravity",
requestType: "agent",
requestId: generateAntigravityRequestId(),
enabledCreditTypes: ["GOOGLE_ONE_AI"],
request: {
model: "gemini-2-flash",
contents: [{ role: "user", parts: [{ text: "hi" }] }],
generationConfig: { maxOutputTokens: 1 },
sessionId,
},
};
const headers: Record<string, string> = {
"Content-Type": "application/json",
Authorization: `Bearer ${accessToken}`,
Accept: "text/event-stream",
};
applyAntigravityClientProfileHeaders(
headers,
{ connectionId: accountId, projectId, providerSpecificData },
body
);
try {
const res = await fetch(url, {
method: "POST",
headers,
body: JSON.stringify(body),
signal: AbortSignal.timeout(10_000),
});
if (!res.ok) continue;
// Read the full SSE response and scan for remainingCredits
const rawSSE = await res.text();
const lines = rawSSE.split("\n");
for (const line of lines) {
const trimmed = line.trim();
if (!trimmed.startsWith("data:")) continue;
const payload = trimmed.slice(5).trim();
if (payload === "[DONE]") break;
try {
const parsed = JSON.parse(payload);
if (Array.isArray(parsed?.remainingCredits)) {
const googleCredit = parsed.remainingCredits.find(
(c: { creditType?: string }) => c?.creditType === "GOOGLE_ONE_AI"
);
if (googleCredit) {
const balance = parseInt(googleCredit.creditAmount, 10);
if (!isNaN(balance)) {
updateAntigravityRemainingCredits(accountId, balance);
return balance;
}
}
}
} catch {
// Skip malformed SSE lines
}
}
} catch {
// Individual endpoint failure; try next
}
}
return null;
} catch {
// Probe is best-effort — don't let it break the usage fetch
return null;
}
}
/**
* Antigravity Usage - Fetch quota from Google Cloud Code API.
* fetchAvailableModels is catalog/eligibility data and may keep reporting full buckets
* after real usage. retrieveUserQuota is the consumption signal for Gemini-family
* buckets, so prefer it when present and fall back to fetchAvailableModels only for
* models that have no retrieveUserQuota entry (for example Claude/GPT OSS buckets).
*/
export async function getAntigravityUsage(
provider: "antigravity" | "agy",
accessToken?: string,
providerSpecificData?: JsonRecord,
connectionProjectId?: string,
connectionId?: string,
options: AntigravityUsageOptions = {}
) {
if (!accessToken) {
return { plan: "Free", message: "Antigravity access token not available." };
}
let subscriptionInfo: unknown = null;
try {
subscriptionInfo = await getAntigravitySubscriptionInfoCached(
accessToken,
providerSpecificData,
options
);
const savedProjectId =
typeof providerSpecificData?.projectId === "string" && providerSpecificData.projectId.trim()
? providerSpecificData.projectId.trim()
: null;
const subscriptionProject = toRecord(subscriptionInfo).cloudaicompanionProject;
const projectId =
savedProjectId ||
connectionProjectId ||
(typeof subscriptionProject === "string"
? subscriptionProject
: typeof toRecord(subscriptionProject).id === "string"
? (toRecord(subscriptionProject).id as string)
: null);
// Derive accountId for credit balance cache.
// Must match executor key: credentials.connectionId
const accountId: string = connectionId || "unknown";
// Read cached credit balance (hydrated from DB on first access)
let creditBalance = getAntigravityRemainingCredits(accountId);
// If no cached balance and credits mode is enabled, fire a minimal probe
const creditsMode = getCreditsMode();
if ((options.forceRefresh || creditBalance === null) && creditsMode !== "off") {
creditBalance = await probeAntigravityCreditBalance(
accessToken,
accountId,
projectId,
options,
providerSpecificData || {}
);
}
const [data, userQuotaData] = await Promise.all([
fetchAntigravityAvailableModelsCached(accessToken, projectId, options),
fetchAntigravityUserQuotaCached(accessToken, projectId, options),
]);
const dataObj = toRecord(data);
if (dataObj.__antigravityForbidden === true) {
return { message: "Antigravity access forbidden. Check subscription." };
}
const modelEntries = toRecord(dataObj.models);
const userQuotaEntries = new Map<string, JsonRecord>();
const userQuotaObj = toRecord(userQuotaData);
if (Array.isArray(userQuotaObj.buckets)) {
for (const bucketValue of userQuotaObj.buckets) {
const bucket = toRecord(bucketValue);
const modelId = toClientAntigravityQuotaModelId(String(bucket.modelId || "").trim());
if (!modelId) continue;
userQuotaEntries.set(modelId, bucket);
}
}
const quotas: Record<string, UsageQuota> = {};
// Parse per-model quota info from fetchAvailableModels response.
for (const [rawModelKey, infoValue] of Object.entries(modelEntries)) {
const info = toRecord(infoValue);
const quotaInfo = toRecord(info.quotaInfo);
const modelKey = toClientAntigravityQuotaModelId(rawModelKey);
// Skip internal, excluded, and models without quota info
if (
!modelKey ||
info.isInternal === true ||
!(provider === "agy"
? isUserCallableAgyModelId(modelKey)
: isUserCallableAntigravityModelId(modelKey)) ||
Object.keys(quotaInfo).length === 0
) {
continue;
}
const liveQuota = userQuotaEntries.get(modelKey);
const quotaSource = liveQuota || quotaInfo;
const rawFraction = toNumber(quotaSource.remainingFraction, -1);
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;
if (!fractionReported) {
console.warn(
`[Antigravity] model ${modelKey} returned no remainingFraction — quota unknown`
);
}
const remainingFraction = fractionReported ? Math.max(0, Math.min(1, rawFraction)) : 0;
// 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 remaining = Math.round(total * remainingFraction);
const used = isUnlimited ? 0 : Math.max(0, total - remaining);
quotas[modelKey] = applyLocalUsageFallback(
{
used,
total: isUnlimited ? 0 : total,
resetAt,
remainingPercentage: isUnlimited ? 100 : remainingPercentage,
unlimited: isUnlimited,
fractionReported,
quotaSource: liveQuota ? "retrieveUserQuota" : "fetchAvailableModels",
},
provider,
connectionId,
modelKey
);
}
// Include retrieveUserQuota buckets not listed in the static/public Antigravity catalog yet.
// This keeps Provider Limits honest when Google adds a new Gemini tier before our catalog is
// updated. Hidden/internal catalog entries above are still filtered by the public pass.
for (const [modelKey, bucket] of userQuotaEntries) {
if (
quotas[modelKey] ||
!(provider === "agy"
? isUserCallableAgyModelId(modelKey)
: isUserCallableAntigravityModelId(modelKey))
) {
continue;
}
const rawFraction = toNumber(bucket.remainingFraction, -1);
if (rawFraction < 0) continue;
const remainingFraction = Math.max(0, Math.min(1, rawFraction));
const resetAt = parseResetTime(bucket.resetTime);
const isUnlimited = !resetAt && remainingFraction >= 1;
const QUOTA_NORMALIZED_BASE = 1000;
const total = QUOTA_NORMALIZED_BASE;
const remaining = Math.round(total * remainingFraction);
quotas[modelKey] = {
used: isUnlimited ? 0 : Math.max(0, total - remaining),
total: isUnlimited ? 0 : total,
resetAt,
remainingPercentage: isUnlimited ? 100 : remainingFraction * 100,
unlimited: isUnlimited,
fractionReported: true,
quotaSource: "retrieveUserQuota",
};
}
return {
plan: getAntigravityPlanLabel(subscriptionInfo, providerSpecificData),
quotas: {
...quotas,
...(creditBalance !== null && {
credits: {
used: 0,
total: 0,
remaining: creditBalance,
unlimited: false,
resetAt: null,
},
}),
},
subscriptionInfo,
};
} catch (error) {
return {
plan: getAntigravityPlanLabel(subscriptionInfo, providerSpecificData),
subscriptionInfo,
message: `Antigravity error: ${(error as Error).message}`,
};
}
}
/**
* Get Antigravity subscription info (cached, 5 min TTL)
* Prevents duplicate loadCodeAssist calls within the same quota cycle.
*/
async function getAntigravitySubscriptionInfoCached(
accessToken: string,
providerSpecificData?: JsonRecord,
options: AntigravityUsageOptions = {}
): Promise<unknown> {
const profile = getAntigravityClientProfile({ providerSpecificData });
const cacheKey = `${accessToken.substring(0, 16)}:${profile}`;
if (options.forceRefresh) {
_antigravitySubCache.delete(cacheKey);
} else {
const cached = _antigravitySubCache.get(cacheKey);
if (cached && Date.now() - cached.fetchedAt < ANTIGRAVITY_CACHE_TTL_MS) {
return cached.data;
}
}
const data = await getAntigravitySubscriptionInfo(accessToken, providerSpecificData);
if (data != null) {
_antigravitySubCache.set(cacheKey, { data, fetchedAt: Date.now() });
}
return data;
}
/**
* Get Antigravity subscription info using correct Antigravity headers.
* Must match the headers used in providers.js postExchange (not CLI headers).
*/
async function getAntigravitySubscriptionInfo(
accessToken: string,
providerSpecificData?: JsonRecord
): Promise<unknown | null> {
try {
const profile = getAntigravityClientProfile({ providerSpecificData });
const response = await fetch(ANTIGRAVITY_CONFIG.loadProjectApiUrl, {
method: "POST",
headers:
profile === "harness"
? getAntigravityBootstrapHeaders(profile, accessToken)
: getAntigravityHeaders("loadCodeAssist", accessToken),
body: JSON.stringify({ metadata: getAntigravityLoadCodeAssistMetadata() }),
});
if (!response.ok) return null;
return await response.json();
} catch {
return null;
}
}