@oh-my-pi/pi-ai 6.9.0 → 7.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,479 @@
1
+ /**
2
+ * GitHub Copilot usage provider.
3
+ *
4
+ * Normalizes Copilot quota usage into the shared UsageReport schema.
5
+ */
6
+
7
+ import type {
8
+ UsageAmount,
9
+ UsageCacheEntry,
10
+ UsageFetchContext,
11
+ UsageFetchParams,
12
+ UsageLimit,
13
+ UsageProvider,
14
+ UsageReport,
15
+ UsageStatus,
16
+ UsageWindow,
17
+ } from "../usage";
18
+
19
+ const COPILOT_HEADERS = {
20
+ "User-Agent": "GitHubCopilotChat/0.35.0",
21
+ "Editor-Version": "vscode/1.107.0",
22
+ "Editor-Plugin-Version": "copilot-chat/0.35.0",
23
+ "Copilot-Integration-Id": "vscode-chat",
24
+ } as const;
25
+
26
+ const DEFAULT_CACHE_TTL_MS = 60_000;
27
+ const MAX_CACHE_TTL_MS = 300_000;
28
+
29
+ type CopilotQuotaDetail = {
30
+ entitlement: number;
31
+ overage_count: number;
32
+ overage_permitted: boolean;
33
+ percent_remaining: number;
34
+ quota_id: string;
35
+ quota_remaining: number;
36
+ remaining: number;
37
+ unlimited: boolean;
38
+ };
39
+
40
+ type CopilotQuotaSnapshots = {
41
+ chat?: CopilotQuotaDetail;
42
+ completions?: CopilotQuotaDetail;
43
+ premium_interactions?: CopilotQuotaDetail;
44
+ };
45
+
46
+ type CopilotUsageResponse = {
47
+ copilot_plan: string;
48
+ quota_reset_date: string;
49
+ quota_snapshots: CopilotQuotaSnapshots;
50
+ };
51
+
52
+ type BillingUsageItem = {
53
+ product: string;
54
+ sku: string;
55
+ model?: string;
56
+ unitType: string;
57
+ grossQuantity: number;
58
+ netQuantity: number;
59
+ limit?: number;
60
+ };
61
+
62
+ type BillingUsageResponse = {
63
+ timePeriod: { year: number; month?: number };
64
+ user: string;
65
+ usageItems: BillingUsageItem[];
66
+ };
67
+
68
+ function toNumber(value: unknown): number | undefined {
69
+ if (typeof value === "number" && Number.isFinite(value)) return value;
70
+ if (typeof value === "string" && value.trim()) {
71
+ const parsed = Number(value);
72
+ return Number.isFinite(parsed) ? parsed : undefined;
73
+ }
74
+ return undefined;
75
+ }
76
+
77
+ function toBoolean(value: unknown): boolean | undefined {
78
+ return typeof value === "boolean" ? value : undefined;
79
+ }
80
+
81
+ function isRecord(value: unknown): value is Record<string, unknown> {
82
+ return !!value && typeof value === "object" && !Array.isArray(value);
83
+ }
84
+
85
+ function resolveGitHubApiBaseUrl(params: UsageFetchParams): string {
86
+ const baseUrl = params.baseUrl?.replace(/\/$/, "");
87
+ if (baseUrl && !baseUrl.includes("githubcopilot.com")) return baseUrl;
88
+ const enterpriseUrl = params.credential.enterpriseUrl?.trim();
89
+ if (!enterpriseUrl) return "https://api.github.com";
90
+ if (enterpriseUrl.startsWith("http://") || enterpriseUrl.startsWith("https://")) {
91
+ return enterpriseUrl.replace(/\/$/, "");
92
+ }
93
+ if (enterpriseUrl.startsWith("api.")) {
94
+ return `https://${enterpriseUrl}`;
95
+ }
96
+ return `https://api.${enterpriseUrl}`;
97
+ }
98
+
99
+ function buildCacheKey(params: UsageFetchParams): string {
100
+ const parts: string[] = [params.provider];
101
+ const { credential } = params;
102
+ if (credential.accountId) parts.push(credential.accountId);
103
+ if (credential.email) parts.push(credential.email);
104
+ const token =
105
+ credential.apiKey || credential.accessToken || credential.refreshToken || credential.metadata?.username;
106
+ if (token && typeof token === "string") {
107
+ const fingerprint = Bun.hash(token).toString(16);
108
+ parts.push(fingerprint);
109
+ }
110
+ return parts.join(":");
111
+ }
112
+
113
+ function buildWindow(resetDate: string | undefined, now: number): UsageWindow | undefined {
114
+ if (!resetDate) return undefined;
115
+ const resetAt = Date.parse(resetDate);
116
+ if (!Number.isFinite(resetAt)) return undefined;
117
+ return {
118
+ id: "monthly",
119
+ label: "Monthly",
120
+ resetsAt: resetAt,
121
+ resetInMs: resetAt - now,
122
+ };
123
+ }
124
+
125
+ function buildAmount(used: number | undefined, limit: number | undefined, unit: UsageAmount["unit"]): UsageAmount {
126
+ const safeLimit = limit !== undefined && Number.isFinite(limit) ? limit : undefined;
127
+ const safeUsed = used !== undefined && Number.isFinite(used) ? used : undefined;
128
+ const remaining = safeLimit !== undefined && safeUsed !== undefined ? Math.max(0, safeLimit - safeUsed) : undefined;
129
+ const usedFraction =
130
+ safeLimit !== undefined && safeUsed !== undefined && safeLimit > 0 ? safeUsed / safeLimit : undefined;
131
+ const remainingFraction =
132
+ safeLimit !== undefined && remaining !== undefined && safeLimit > 0 ? remaining / safeLimit : undefined;
133
+ return {
134
+ used: safeUsed,
135
+ limit: safeLimit,
136
+ remaining,
137
+ usedFraction,
138
+ remainingFraction,
139
+ unit,
140
+ };
141
+ }
142
+
143
+ function deriveStatus(amount: UsageAmount, unlimited: boolean): UsageStatus {
144
+ if (unlimited) return "ok";
145
+ if (amount.remainingFraction === undefined) return "unknown";
146
+ if (amount.remainingFraction <= 0) return "exhausted";
147
+ if (amount.remainingFraction <= 0.1) return "warning";
148
+ return "ok";
149
+ }
150
+
151
+ function parseQuotaDetail(value: unknown): CopilotQuotaDetail | null {
152
+ if (!isRecord(value)) return null;
153
+ const entitlement = toNumber(value.entitlement);
154
+ const remaining = toNumber(value.remaining);
155
+ const percentRemaining = toNumber(value.percent_remaining);
156
+ const unlimited = toBoolean(value.unlimited);
157
+ if (
158
+ entitlement === undefined ||
159
+ remaining === undefined ||
160
+ percentRemaining === undefined ||
161
+ unlimited === undefined
162
+ ) {
163
+ return null;
164
+ }
165
+ const overageCount = toNumber(value.overage_count) ?? 0;
166
+ const overagePermitted = toBoolean(value.overage_permitted) ?? false;
167
+ const quotaId = typeof value.quota_id === "string" ? value.quota_id : "";
168
+ const quotaRemaining = toNumber(value.quota_remaining) ?? remaining;
169
+ return {
170
+ entitlement,
171
+ overage_count: overageCount,
172
+ overage_permitted: overagePermitted,
173
+ percent_remaining: percentRemaining,
174
+ quota_id: quotaId,
175
+ quota_remaining: quotaRemaining,
176
+ remaining,
177
+ unlimited,
178
+ };
179
+ }
180
+
181
+ async function fetchJson(ctx: UsageFetchContext, url: string, init: RequestInit): Promise<unknown> {
182
+ const response = await ctx.fetch(url, init);
183
+ if (!response.ok) {
184
+ const text = await response.text();
185
+ throw new Error(`${response.status} ${response.statusText}: ${text}`);
186
+ }
187
+ return response.json();
188
+ }
189
+
190
+ async function resolveGitHubUsername(
191
+ ctx: UsageFetchContext,
192
+ baseUrl: string,
193
+ token: string,
194
+ signal?: AbortSignal,
195
+ ): Promise<string | undefined> {
196
+ try {
197
+ const data = await fetchJson(ctx, `${baseUrl}/user`, {
198
+ headers: {
199
+ Accept: "application/vnd.github+json",
200
+ Authorization: `Bearer ${token}`,
201
+ "X-GitHub-Api-Version": "2022-11-28",
202
+ },
203
+ signal,
204
+ });
205
+ if (!isRecord(data)) return undefined;
206
+ return typeof data.login === "string" ? data.login : undefined;
207
+ } catch {
208
+ return undefined;
209
+ }
210
+ }
211
+
212
+ async function fetchInternalUsage(
213
+ ctx: UsageFetchContext,
214
+ githubApiBaseUrl: string,
215
+ token: string,
216
+ signal?: AbortSignal,
217
+ ): Promise<CopilotUsageResponse> {
218
+ const headers: Record<string, string> = {
219
+ "Content-Type": "application/json",
220
+ Accept: "application/json",
221
+ Authorization: `Bearer ${token}`,
222
+ ...COPILOT_HEADERS,
223
+ };
224
+ const data = await fetchJson(ctx, `${githubApiBaseUrl}/copilot_internal/user`, { headers, signal });
225
+ if (!isRecord(data)) throw new Error("Invalid Copilot usage response");
226
+ return data as CopilotUsageResponse;
227
+ }
228
+
229
+ async function fetchBillingUsage(
230
+ ctx: UsageFetchContext,
231
+ baseUrl: string,
232
+ username: string,
233
+ token: string,
234
+ signal?: AbortSignal,
235
+ ): Promise<BillingUsageResponse> {
236
+ const data = await fetchJson(
237
+ ctx,
238
+ `${baseUrl}/users/${encodeURIComponent(username)}/settings/billing/premium_request/usage`,
239
+ {
240
+ headers: {
241
+ Accept: "application/vnd.github+json",
242
+ Authorization: `Bearer ${token}`,
243
+ "X-GitHub-Api-Version": "2022-11-28",
244
+ },
245
+ signal,
246
+ },
247
+ );
248
+
249
+ if (!isRecord(data)) throw new Error("Invalid Copilot billing usage response");
250
+ return data as BillingUsageResponse;
251
+ }
252
+
253
+ function buildLimitFromQuota(
254
+ key: string,
255
+ label: string,
256
+ quota: CopilotQuotaDetail,
257
+ plan: string,
258
+ window: UsageWindow | undefined,
259
+ accountId?: string,
260
+ ): UsageLimit {
261
+ const used = quota.unlimited ? undefined : Math.max(0, quota.entitlement - quota.remaining);
262
+ const limit = quota.unlimited ? undefined : quota.entitlement;
263
+ const amount = buildAmount(used, limit, "requests");
264
+ const notes: string[] = [];
265
+ if (quota.unlimited) notes.push("Unlimited");
266
+ if (quota.overage_count > 0) {
267
+ notes.push(`Overage requests: ${quota.overage_count}`);
268
+ }
269
+ return {
270
+ id: `copilot:${key}`,
271
+ label,
272
+ scope: {
273
+ provider: "github-copilot",
274
+ accountId,
275
+ tier: plan,
276
+ windowId: window?.id,
277
+ },
278
+ window,
279
+ amount,
280
+ status: deriveStatus(amount, quota.unlimited),
281
+ notes: notes.length > 0 ? notes : undefined,
282
+ };
283
+ }
284
+
285
+ function normalizeQuotaSnapshots(
286
+ data: CopilotUsageResponse,
287
+ now: number,
288
+ accountId?: string,
289
+ ): { limits: UsageLimit[]; window?: UsageWindow } {
290
+ const window = buildWindow(data.quota_reset_date, now);
291
+ const snapshots = data.quota_snapshots ?? {};
292
+ const limits: UsageLimit[] = [];
293
+ const premium = parseQuotaDetail(snapshots.premium_interactions);
294
+ if (premium) {
295
+ limits.push(buildLimitFromQuota("premium", "Premium Requests", premium, data.copilot_plan, window, accountId));
296
+ }
297
+ const chat = parseQuotaDetail(snapshots.chat);
298
+ if (chat && !chat.unlimited) {
299
+ limits.push(buildLimitFromQuota("chat", "Chat Requests", chat, data.copilot_plan, window, accountId));
300
+ }
301
+ const completions = parseQuotaDetail(snapshots.completions);
302
+ if (completions && !completions.unlimited) {
303
+ limits.push(buildLimitFromQuota("completions", "Completions", completions, data.copilot_plan, window, accountId));
304
+ }
305
+ return { limits, window };
306
+ }
307
+
308
+ function normalizeBillingUsage(data: BillingUsageResponse): UsageLimit[] {
309
+ const limits: UsageLimit[] = [];
310
+ const periodLabel = data.timePeriod.month
311
+ ? `${data.timePeriod.year}-${String(data.timePeriod.month).padStart(2, "0")}`
312
+ : `${data.timePeriod.year}`;
313
+ const window: UsageWindow = {
314
+ id: "billing-period",
315
+ label: periodLabel,
316
+ };
317
+
318
+ const premiumItems = data.usageItems.filter(
319
+ (item) => item.sku === "Copilot Premium Request" || item.sku.includes("Premium"),
320
+ );
321
+ const totalUsed = premiumItems.reduce((sum, item) => sum + item.grossQuantity, 0);
322
+ const totalLimit = premiumItems.reduce((sum, item) => sum + (item.limit ?? 0), 0) || undefined;
323
+ const totalAmount = buildAmount(totalUsed, totalLimit, "requests");
324
+ limits.push({
325
+ id: "copilot:premium",
326
+ label: "Premium Requests",
327
+ scope: {
328
+ provider: "github-copilot",
329
+ accountId: data.user,
330
+ windowId: window.id,
331
+ },
332
+ window,
333
+ amount: totalAmount,
334
+ status: deriveStatus(totalAmount, false),
335
+ });
336
+
337
+ for (const item of data.usageItems) {
338
+ if (!item.model) continue;
339
+ if (item.grossQuantity <= 0) continue;
340
+ const amount = buildAmount(item.grossQuantity, item.limit, "requests");
341
+ limits.push({
342
+ id: `copilot:model:${item.model}`,
343
+ label: `Model ${item.model}`,
344
+ scope: {
345
+ provider: "github-copilot",
346
+ accountId: data.user,
347
+ modelId: item.model,
348
+ windowId: window.id,
349
+ },
350
+ window,
351
+ amount,
352
+ status: deriveStatus(amount, false),
353
+ });
354
+ }
355
+
356
+ return limits;
357
+ }
358
+
359
+ function resolveCacheTtl(now: number, report: UsageReport | null): UsageCacheEntry["expiresAt"] {
360
+ if (!report) return now + DEFAULT_CACHE_TTL_MS;
361
+ const resetInMs = report.limits
362
+ .map((limit) => limit.window?.resetInMs)
363
+ .find((value): value is number => typeof value === "number" && Number.isFinite(value));
364
+ if (!resetInMs || resetInMs <= 0) return now + DEFAULT_CACHE_TTL_MS;
365
+ return now + Math.min(MAX_CACHE_TTL_MS, resetInMs);
366
+ }
367
+
368
+ export const githubCopilotUsageProvider: UsageProvider = {
369
+ id: "github-copilot",
370
+ supports: ({ provider, credential }) => {
371
+ if (provider !== "github-copilot") return false;
372
+ if (credential.type === "oauth") {
373
+ return Boolean(credential.refreshToken || credential.accessToken);
374
+ }
375
+ return Boolean(credential.apiKey);
376
+ },
377
+ fetchUsage: async (params, ctx) => {
378
+ if (!githubCopilotUsageProvider.supports?.(params)) return null;
379
+ const now = ctx.now();
380
+ const cacheKey = buildCacheKey(params);
381
+ const cached = await ctx.cache.get(cacheKey);
382
+ if (cached && cached.expiresAt > now) return cached.value;
383
+
384
+ const githubApiBaseUrl = resolveGitHubApiBaseUrl(params);
385
+ let report: UsageReport | null = null;
386
+
387
+ if (params.credential.type === "api_key") {
388
+ let username: string | undefined;
389
+ const candidate =
390
+ params.credential.accountId || params.credential.metadata?.username || params.credential.metadata?.user;
391
+ if (typeof candidate === "string" && candidate.trim()) {
392
+ username = candidate.trim();
393
+ }
394
+ if (!username && params.credential.apiKey) {
395
+ username = await resolveGitHubUsername(ctx, githubApiBaseUrl, params.credential.apiKey, params.signal);
396
+ }
397
+ if (!username) {
398
+ ctx.logger?.warn("Copilot usage requires username for billing API", { provider: params.provider });
399
+ } else if (params.credential.apiKey) {
400
+ try {
401
+ const billing = await fetchBillingUsage(
402
+ ctx,
403
+ githubApiBaseUrl,
404
+ username,
405
+ params.credential.apiKey,
406
+ params.signal,
407
+ );
408
+ report = {
409
+ provider: "github-copilot",
410
+ fetchedAt: now,
411
+ limits: normalizeBillingUsage(billing),
412
+ metadata: {
413
+ accountId: billing.user,
414
+ account: billing.user,
415
+ period: billing.timePeriod,
416
+ },
417
+ };
418
+ } catch (error) {
419
+ ctx.logger?.warn("Copilot usage fetch failed", { error: String(error) });
420
+ }
421
+ }
422
+ if (!report && params.credential.apiKey) {
423
+ try {
424
+ const usage = await fetchInternalUsage(ctx, githubApiBaseUrl, params.credential.apiKey, params.signal);
425
+ const normalized = normalizeQuotaSnapshots(usage, now, username);
426
+ report = {
427
+ provider: "github-copilot",
428
+ fetchedAt: now,
429
+ limits: normalized.limits,
430
+ metadata: {
431
+ accountId: username,
432
+ plan: usage.copilot_plan,
433
+ quotaResetDate: usage.quota_reset_date,
434
+ },
435
+ raw: usage,
436
+ };
437
+ } catch (error) {
438
+ ctx.logger?.warn("Copilot usage fetch failed", { error: String(error) });
439
+ }
440
+ }
441
+ } else {
442
+ const { refreshToken, accessToken } = params.credential;
443
+ if (!refreshToken && !accessToken) return null;
444
+ const oauthToken = refreshToken || accessToken;
445
+ if (!oauthToken) return null;
446
+ const githubToken = refreshToken ?? accessToken;
447
+ if (!githubToken) return null;
448
+ try {
449
+ const usage = await fetchInternalUsage(ctx, githubApiBaseUrl, githubToken, params.signal);
450
+ let accountId = params.credential.accountId;
451
+ if (!accountId && refreshToken) {
452
+ accountId = await resolveGitHubUsername(ctx, githubApiBaseUrl, refreshToken, params.signal);
453
+ }
454
+ if (!accountId && accessToken) {
455
+ accountId = await resolveGitHubUsername(ctx, githubApiBaseUrl, accessToken, params.signal);
456
+ }
457
+ const normalized = normalizeQuotaSnapshots(usage, now, accountId);
458
+ report = {
459
+ provider: "github-copilot",
460
+ fetchedAt: now,
461
+ limits: normalized.limits,
462
+ metadata: {
463
+ accountId,
464
+ email: params.credential.email,
465
+ plan: usage.copilot_plan,
466
+ quotaResetDate: usage.quota_reset_date,
467
+ },
468
+ raw: usage,
469
+ };
470
+ } catch (error) {
471
+ ctx.logger?.warn("Copilot usage fetch failed", { error: String(error) });
472
+ }
473
+ }
474
+
475
+ const expiresAt = resolveCacheTtl(now, report);
476
+ await ctx.cache.set(cacheKey, { value: report, expiresAt });
477
+ return report;
478
+ },
479
+ };
@@ -0,0 +1,218 @@
1
+ import type {
2
+ UsageAmount,
3
+ UsageFetchContext,
4
+ UsageFetchParams,
5
+ UsageLimit,
6
+ UsageProvider,
7
+ UsageReport,
8
+ UsageStatus,
9
+ UsageWindow,
10
+ } from "../usage";
11
+ import { refreshAntigravityToken } from "../utils/oauth/google-antigravity";
12
+
13
+ interface AntigravityQuotaInfo {
14
+ remainingFraction?: number;
15
+ resetTime?: string;
16
+ tier?: string;
17
+ windowId?: string;
18
+ windowLabel?: string;
19
+ }
20
+
21
+ interface AntigravityModelInfo {
22
+ displayName?: string;
23
+ quotaInfo?: AntigravityQuotaInfo | AntigravityQuotaInfo[];
24
+ quotaInfos?: AntigravityQuotaInfo[];
25
+ quotaInfoByTier?: Record<string, AntigravityQuotaInfo | AntigravityQuotaInfo[]>;
26
+ }
27
+
28
+ interface AntigravityUsageResponse {
29
+ models: Record<string, AntigravityModelInfo>;
30
+ }
31
+
32
+ const DEFAULT_ENDPOINT = "https://daily-cloudcode-pa.googleapis.com";
33
+ const FETCH_AVAILABLE_MODELS_PATH = "/v1internal:fetchAvailableModels";
34
+ const USER_AGENT = "antigravity/1.11.9 windows/amd64";
35
+ const DEFAULT_CACHE_TTL_MS = 60_000;
36
+
37
+ function clampFraction(value: number | undefined): number | undefined {
38
+ if (value === undefined || !Number.isFinite(value)) return undefined;
39
+ if (value < 0) return 0;
40
+ if (value > 1) return 1;
41
+ return value;
42
+ }
43
+
44
+ function getUsageStatus(remainingFraction: number | undefined): UsageStatus | undefined {
45
+ if (remainingFraction === undefined) return "unknown";
46
+ if (remainingFraction <= 0) return "exhausted";
47
+ if (remainingFraction <= 0.1) return "warning";
48
+ return "ok";
49
+ }
50
+
51
+ function parseWindow(info: AntigravityQuotaInfo, nowMs: number): UsageWindow | undefined {
52
+ if (!info.resetTime) return undefined;
53
+ const resetAt = Date.parse(info.resetTime);
54
+ if (!Number.isFinite(resetAt)) return undefined;
55
+ return {
56
+ id: info.windowId ?? "default",
57
+ label: info.windowLabel ?? "Default",
58
+ resetsAt: resetAt,
59
+ resetInMs: Math.max(0, resetAt - nowMs),
60
+ };
61
+ }
62
+
63
+ function buildAmount(info: AntigravityQuotaInfo): UsageAmount {
64
+ const remainingFraction = clampFraction(info.remainingFraction);
65
+ const amount: UsageAmount = { unit: "percent" };
66
+ if (remainingFraction === undefined) return amount;
67
+ const usedFraction = clampFraction(1 - remainingFraction);
68
+ amount.remainingFraction = remainingFraction;
69
+ amount.usedFraction = usedFraction;
70
+ amount.remaining = remainingFraction * 100;
71
+ amount.used = usedFraction !== undefined ? usedFraction * 100 : undefined;
72
+ amount.limit = 100;
73
+ return amount;
74
+ }
75
+
76
+ function normalizeQuotaInfos(info: AntigravityModelInfo): AntigravityQuotaInfo[] {
77
+ const results: AntigravityQuotaInfo[] = [];
78
+ const addInfo = (value: AntigravityQuotaInfo, tier?: string) => {
79
+ results.push({ ...value, ...(tier ? { tier } : {}) });
80
+ };
81
+ const addArray = (values?: AntigravityQuotaInfo[]) => {
82
+ if (!values) return;
83
+ for (const value of values) addInfo(value);
84
+ };
85
+
86
+ if (Array.isArray(info.quotaInfo)) {
87
+ addArray(info.quotaInfo);
88
+ } else if (info.quotaInfo) {
89
+ addInfo(info.quotaInfo);
90
+ }
91
+ addArray(info.quotaInfos);
92
+
93
+ if (info.quotaInfoByTier) {
94
+ for (const [tier, value] of Object.entries(info.quotaInfoByTier)) {
95
+ if (Array.isArray(value)) {
96
+ for (const entry of value) addInfo(entry, tier);
97
+ } else if (value) {
98
+ addInfo(value, tier);
99
+ }
100
+ }
101
+ }
102
+
103
+ return results;
104
+ }
105
+
106
+ function buildCacheKey(params: UsageFetchParams): string {
107
+ const credential = params.credential;
108
+ const accountPart = credential.accountId ?? credential.email ?? "unknown";
109
+ const projectPart = credential.projectId ?? "unknown";
110
+ return `usage:${params.provider}:${accountPart}:${projectPart}`;
111
+ }
112
+
113
+ async function resolveAccessToken(params: UsageFetchParams, ctx: UsageFetchContext): Promise<string | undefined> {
114
+ const { credential } = params;
115
+ if (credential.accessToken && (!credential.expiresAt || credential.expiresAt > ctx.now() + 60_000)) {
116
+ return credential.accessToken;
117
+ }
118
+ if (!credential.refreshToken || !credential.projectId) return undefined;
119
+ try {
120
+ const refreshed = await refreshAntigravityToken(credential.refreshToken, credential.projectId);
121
+ return refreshed.access;
122
+ } catch (error) {
123
+ ctx.logger?.warn("Antigravity usage token refresh failed", { error: String(error) });
124
+ return undefined;
125
+ }
126
+ }
127
+
128
+ async function fetchAntigravityUsage(params: UsageFetchParams, ctx: UsageFetchContext): Promise<UsageReport | null> {
129
+ const credential = params.credential;
130
+ if (!credential.projectId) return null;
131
+
132
+ const cacheKey = buildCacheKey(params);
133
+ const cached = await ctx.cache.get(cacheKey);
134
+ const nowMs = ctx.now();
135
+ if (cached && cached.expiresAt > nowMs) {
136
+ return cached.value;
137
+ }
138
+
139
+ const accessToken = await resolveAccessToken(params, ctx);
140
+ if (!accessToken) return null;
141
+
142
+ const baseUrl = params.baseUrl?.replace(/\/+$/, "") || DEFAULT_ENDPOINT;
143
+ const url = `${baseUrl}${FETCH_AVAILABLE_MODELS_PATH}`;
144
+ const response = await ctx.fetch(url, {
145
+ method: "POST",
146
+ headers: {
147
+ Authorization: `Bearer ${accessToken}`,
148
+ "Content-Type": "application/json",
149
+ "User-Agent": USER_AGENT,
150
+ },
151
+ body: JSON.stringify({ project: credential.projectId }),
152
+ signal: params.signal,
153
+ });
154
+
155
+ if (!response.ok) {
156
+ ctx.logger?.warn("Antigravity usage fetch failed", {
157
+ status: response.status,
158
+ statusText: response.statusText,
159
+ });
160
+ return null;
161
+ }
162
+
163
+ const data = (await response.json()) as AntigravityUsageResponse;
164
+ const limits: UsageLimit[] = [];
165
+ let earliestReset: number | undefined;
166
+
167
+ for (const [modelId, modelInfo] of Object.entries(data.models ?? {})) {
168
+ const quotaInfos = normalizeQuotaInfos(modelInfo);
169
+ for (const quotaInfo of quotaInfos) {
170
+ const amount = buildAmount(quotaInfo);
171
+ const window = parseWindow(quotaInfo, nowMs);
172
+ if (window?.resetsAt) {
173
+ earliestReset = earliestReset ? Math.min(earliestReset, window.resetsAt) : window.resetsAt;
174
+ }
175
+ const labelBase = modelInfo.displayName || modelId;
176
+ const label = quotaInfo.tier ? `${labelBase} (${quotaInfo.tier})` : labelBase;
177
+ const windowId = window?.id ?? "default";
178
+ limits.push({
179
+ id: `${modelId}:${quotaInfo.tier ?? "default"}:${windowId}`,
180
+ label,
181
+ scope: {
182
+ provider: params.provider,
183
+ accountId: credential.accountId,
184
+ projectId: credential.projectId,
185
+ modelId,
186
+ tier: quotaInfo.tier,
187
+ windowId,
188
+ },
189
+ window,
190
+ amount,
191
+ status: getUsageStatus(amount.remainingFraction),
192
+ });
193
+ }
194
+ }
195
+
196
+ const report: UsageReport = {
197
+ provider: params.provider,
198
+ fetchedAt: nowMs,
199
+ limits,
200
+ metadata: {
201
+ endpoint: url,
202
+ projectId: credential.projectId,
203
+ },
204
+ raw: data,
205
+ };
206
+
207
+ const expiresAt = earliestReset
208
+ ? Math.min(earliestReset, nowMs + DEFAULT_CACHE_TTL_MS)
209
+ : nowMs + DEFAULT_CACHE_TTL_MS;
210
+ await ctx.cache.set(cacheKey, { value: report, expiresAt });
211
+ return report;
212
+ }
213
+
214
+ export const antigravityUsageProvider: UsageProvider = {
215
+ id: "google-antigravity",
216
+ fetchUsage: fetchAntigravityUsage,
217
+ supports: (params) => params.provider === "google-antigravity",
218
+ };