@f5-sales-demo/pi-ai 19.51.2

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.
Files changed (128) hide show
  1. package/CHANGELOG.md +1997 -0
  2. package/README.md +1160 -0
  3. package/package.json +135 -0
  4. package/src/api-registry.ts +95 -0
  5. package/src/auth-storage.ts +2694 -0
  6. package/src/cli.ts +493 -0
  7. package/src/index.ts +42 -0
  8. package/src/model-cache.ts +97 -0
  9. package/src/model-manager.ts +349 -0
  10. package/src/model-thinking.ts +561 -0
  11. package/src/models.json +49439 -0
  12. package/src/models.json.d.ts +9 -0
  13. package/src/models.ts +56 -0
  14. package/src/prompts/turn-aborted-guidance.md +4 -0
  15. package/src/provider-details.ts +81 -0
  16. package/src/provider-models/descriptors.ts +285 -0
  17. package/src/provider-models/google.ts +90 -0
  18. package/src/provider-models/index.ts +4 -0
  19. package/src/provider-models/openai-compat.ts +2074 -0
  20. package/src/provider-models/special.ts +106 -0
  21. package/src/providers/amazon-bedrock.ts +706 -0
  22. package/src/providers/anthropic.ts +1682 -0
  23. package/src/providers/azure-openai-responses.ts +391 -0
  24. package/src/providers/cursor/gen/agent_pb.ts +15274 -0
  25. package/src/providers/cursor/proto/agent.proto +3526 -0
  26. package/src/providers/cursor/proto/buf.gen.yaml +6 -0
  27. package/src/providers/cursor/proto/buf.yaml +17 -0
  28. package/src/providers/cursor.ts +2218 -0
  29. package/src/providers/github-copilot-headers.ts +140 -0
  30. package/src/providers/gitlab-duo.ts +381 -0
  31. package/src/providers/google-gemini-cli.ts +1133 -0
  32. package/src/providers/google-shared.ts +354 -0
  33. package/src/providers/google-vertex.ts +436 -0
  34. package/src/providers/google.ts +381 -0
  35. package/src/providers/kimi.ts +151 -0
  36. package/src/providers/openai-codex/constants.ts +43 -0
  37. package/src/providers/openai-codex/request-transformer.ts +158 -0
  38. package/src/providers/openai-codex/response-handler.ts +81 -0
  39. package/src/providers/openai-codex-responses.ts +2345 -0
  40. package/src/providers/openai-completions-compat.ts +159 -0
  41. package/src/providers/openai-completions.ts +1290 -0
  42. package/src/providers/openai-responses-shared.ts +452 -0
  43. package/src/providers/openai-responses.ts +519 -0
  44. package/src/providers/register-builtins.ts +329 -0
  45. package/src/providers/synthetic.ts +154 -0
  46. package/src/providers/transform-messages.ts +234 -0
  47. package/src/rate-limit-utils.ts +84 -0
  48. package/src/stream.ts +728 -0
  49. package/src/types.ts +546 -0
  50. package/src/usage/claude.ts +337 -0
  51. package/src/usage/gemini.ts +248 -0
  52. package/src/usage/github-copilot.ts +421 -0
  53. package/src/usage/google-antigravity.ts +200 -0
  54. package/src/usage/kimi.ts +286 -0
  55. package/src/usage/minimax-code.ts +31 -0
  56. package/src/usage/openai-codex.ts +387 -0
  57. package/src/usage/zai.ts +247 -0
  58. package/src/usage.ts +130 -0
  59. package/src/utils/abort.ts +36 -0
  60. package/src/utils/anthropic-auth.ts +293 -0
  61. package/src/utils/discovery/antigravity.ts +261 -0
  62. package/src/utils/discovery/codex.ts +371 -0
  63. package/src/utils/discovery/cursor.ts +306 -0
  64. package/src/utils/discovery/gemini.ts +248 -0
  65. package/src/utils/discovery/index.ts +5 -0
  66. package/src/utils/discovery/openai-compatible.ts +224 -0
  67. package/src/utils/event-stream.ts +209 -0
  68. package/src/utils/http-inspector.ts +165 -0
  69. package/src/utils/idle-iterator.ts +176 -0
  70. package/src/utils/json-parse.ts +28 -0
  71. package/src/utils/oauth/alibaba-coding-plan.ts +59 -0
  72. package/src/utils/oauth/anthropic.ts +134 -0
  73. package/src/utils/oauth/api-key-validation.ts +92 -0
  74. package/src/utils/oauth/callback-server.ts +276 -0
  75. package/src/utils/oauth/cerebras.ts +59 -0
  76. package/src/utils/oauth/cloudflare-ai-gateway.ts +48 -0
  77. package/src/utils/oauth/cursor.ts +157 -0
  78. package/src/utils/oauth/github-copilot.ts +358 -0
  79. package/src/utils/oauth/gitlab-duo.ts +123 -0
  80. package/src/utils/oauth/google-antigravity.ts +275 -0
  81. package/src/utils/oauth/google-gemini-cli.ts +334 -0
  82. package/src/utils/oauth/huggingface.ts +62 -0
  83. package/src/utils/oauth/index.ts +512 -0
  84. package/src/utils/oauth/kagi.ts +47 -0
  85. package/src/utils/oauth/kilo.ts +87 -0
  86. package/src/utils/oauth/kimi.ts +251 -0
  87. package/src/utils/oauth/litellm.ts +81 -0
  88. package/src/utils/oauth/lm-studio.ts +40 -0
  89. package/src/utils/oauth/minimax-code.ts +78 -0
  90. package/src/utils/oauth/moonshot.ts +59 -0
  91. package/src/utils/oauth/nanogpt.ts +51 -0
  92. package/src/utils/oauth/nvidia.ts +70 -0
  93. package/src/utils/oauth/oauth.html +199 -0
  94. package/src/utils/oauth/ollama.ts +47 -0
  95. package/src/utils/oauth/openai-codex.ts +190 -0
  96. package/src/utils/oauth/opencode.ts +49 -0
  97. package/src/utils/oauth/parallel.ts +46 -0
  98. package/src/utils/oauth/perplexity.ts +200 -0
  99. package/src/utils/oauth/pkce.ts +18 -0
  100. package/src/utils/oauth/qianfan.ts +58 -0
  101. package/src/utils/oauth/qwen-portal.ts +60 -0
  102. package/src/utils/oauth/synthetic.ts +60 -0
  103. package/src/utils/oauth/tavily.ts +46 -0
  104. package/src/utils/oauth/together.ts +59 -0
  105. package/src/utils/oauth/types.ts +89 -0
  106. package/src/utils/oauth/venice.ts +59 -0
  107. package/src/utils/oauth/vercel-ai-gateway.ts +47 -0
  108. package/src/utils/oauth/vllm.ts +40 -0
  109. package/src/utils/oauth/xiaomi.ts +88 -0
  110. package/src/utils/oauth/zai.ts +60 -0
  111. package/src/utils/oauth/zenmux.ts +51 -0
  112. package/src/utils/overflow.ts +134 -0
  113. package/src/utils/retry-after.ts +110 -0
  114. package/src/utils/retry.ts +93 -0
  115. package/src/utils/schema/CONSTRAINTS.md +160 -0
  116. package/src/utils/schema/adapt.ts +20 -0
  117. package/src/utils/schema/compatibility.ts +397 -0
  118. package/src/utils/schema/dereference.ts +93 -0
  119. package/src/utils/schema/equality.ts +93 -0
  120. package/src/utils/schema/fields.ts +147 -0
  121. package/src/utils/schema/index.ts +9 -0
  122. package/src/utils/schema/normalize-cca.ts +479 -0
  123. package/src/utils/schema/sanitize-google.ts +212 -0
  124. package/src/utils/schema/strict-mode.ts +385 -0
  125. package/src/utils/schema/types.ts +5 -0
  126. package/src/utils/tool-choice.ts +81 -0
  127. package/src/utils/validation.ts +664 -0
  128. package/src/utils.ts +147 -0
@@ -0,0 +1,337 @@
1
+ import type {
2
+ CredentialRankingStrategy,
3
+ UsageAmount,
4
+ UsageFetchContext,
5
+ UsageFetchParams,
6
+ UsageLimit,
7
+ UsageProvider,
8
+ UsageReport,
9
+ UsageStatus,
10
+ UsageWindow,
11
+ } from "../usage";
12
+ import { isRecord, toNumber } from "../utils";
13
+
14
+ const DEFAULT_ENDPOINT = "https://api.anthropic.com/api/oauth";
15
+ const FIVE_HOURS_MS = 5 * 60 * 60 * 1000;
16
+ const SEVEN_DAYS_MS = 7 * 24 * 60 * 60 * 1000;
17
+ const MAX_RETRIES = 3;
18
+ const BASE_RETRY_DELAY_MS = 500;
19
+
20
+ const CLAUDE_HEADERS = {
21
+ accept: "application/json, text/plain, */*",
22
+ "accept-encoding": "gzip, compress, deflate, br",
23
+ "anthropic-beta":
24
+ "claude-code-20250219,oauth-2025-04-20,interleaved-thinking-2025-05-14,context-management-2025-06-27,prompt-caching-scope-2026-01-05",
25
+ "content-type": "application/json",
26
+ "user-agent": "claude-cli/2.1.63 (external, cli)",
27
+ connection: "keep-alive",
28
+ } as const;
29
+
30
+ function normalizeClaudeBaseUrl(baseUrl?: string): string {
31
+ if (!baseUrl?.trim()) return DEFAULT_ENDPOINT;
32
+ const trimmed = baseUrl.trim().replace(/\/+$/, "");
33
+ const lower = trimmed.toLowerCase();
34
+ if (lower.endsWith("/api/oauth")) return trimmed;
35
+ let url: URL;
36
+ try {
37
+ url = new URL(trimmed);
38
+ } catch {
39
+ return DEFAULT_ENDPOINT;
40
+ }
41
+ let path = url.pathname.replace(/\/+$/, "");
42
+ if (path === "/") path = "";
43
+ if (path.toLowerCase().endsWith("/v1")) {
44
+ path = path.slice(0, -3);
45
+ }
46
+ if (!path) return `${url.origin}/api/oauth`;
47
+ return `${url.origin}${path}/api/oauth`;
48
+ }
49
+
50
+ interface ClaudeUsageBucket {
51
+ utilization?: number;
52
+ resets_at?: string;
53
+ }
54
+
55
+ interface ParsedUsageBucket {
56
+ utilization?: number;
57
+ resetsAt?: number;
58
+ }
59
+
60
+ interface ClaudeUsageResponse {
61
+ five_hour?: ClaudeUsageBucket | null;
62
+ seven_day?: ClaudeUsageBucket | null;
63
+ seven_day_opus?: ClaudeUsageBucket | null;
64
+ seven_day_sonnet?: ClaudeUsageBucket | null;
65
+ }
66
+
67
+ type ClaudeUsagePayload = {
68
+ payload: ClaudeUsageResponse;
69
+ orgId?: string;
70
+ };
71
+
72
+ function parseIsoTime(value: string | undefined): number | undefined {
73
+ if (!value) return undefined;
74
+ const parsed = Date.parse(value);
75
+ return Number.isFinite(parsed) ? parsed : undefined;
76
+ }
77
+
78
+ function parseBucket(bucket: unknown): ParsedUsageBucket | undefined {
79
+ if (!isRecord(bucket)) return undefined;
80
+ const utilization = toNumber(bucket.utilization);
81
+ const resetsAt = parseIsoTime(typeof bucket.resets_at === "string" ? bucket.resets_at : undefined);
82
+ if (utilization === undefined && resetsAt === undefined) {
83
+ return undefined;
84
+ }
85
+ return { utilization, resetsAt };
86
+ }
87
+
88
+ function getPayloadString(payload: Record<string, unknown>, key: string): string | undefined {
89
+ const value = payload[key];
90
+ return typeof value === "string" && value.trim() ? value.trim() : undefined;
91
+ }
92
+
93
+ function extractUsageIdentity(payload: ClaudeUsageResponse, orgId?: string): { accountId?: string; email?: string } {
94
+ if (!isRecord(payload)) return { accountId: orgId };
95
+ const accountId =
96
+ getPayloadString(payload, "account_id") ??
97
+ getPayloadString(payload, "accountId") ??
98
+ getPayloadString(payload, "user_id") ??
99
+ getPayloadString(payload, "userId") ??
100
+ getPayloadString(payload, "org_id") ??
101
+ getPayloadString(payload, "orgId") ??
102
+ orgId;
103
+ const email =
104
+ getPayloadString(payload, "email") ??
105
+ getPayloadString(payload, "user_email") ??
106
+ getPayloadString(payload, "userEmail");
107
+ return { accountId, email };
108
+ }
109
+
110
+ function hasUsageData(payload: ClaudeUsageResponse): boolean {
111
+ return Boolean(payload.five_hour || payload.seven_day || payload.seven_day_opus || payload.seven_day_sonnet);
112
+ }
113
+
114
+ async function fetchUsagePayload(
115
+ url: string,
116
+ headers: Record<string, string>,
117
+ ctx: UsageFetchContext,
118
+ signal?: AbortSignal,
119
+ ): Promise<ClaudeUsagePayload | null> {
120
+ let lastPayload: ClaudeUsageResponse | null = null;
121
+ let lastOrgId: string | undefined;
122
+ for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
123
+ try {
124
+ const response = await ctx.fetch(url, { headers, signal });
125
+ if (!response.ok) {
126
+ ctx.logger?.warn("Claude usage fetch failed", { status: response.status, statusText: response.statusText });
127
+ return null;
128
+ }
129
+ const payload = (await response.json()) as ClaudeUsageResponse;
130
+ lastPayload = payload;
131
+ const orgId = response.headers.get("anthropic-organization-id")?.trim() || undefined;
132
+ lastOrgId = orgId ?? lastOrgId;
133
+ if (payload && isRecord(payload) && hasUsageData(payload)) {
134
+ return { payload, orgId };
135
+ }
136
+ } catch (error) {
137
+ ctx.logger?.warn("Claude usage fetch error", { error: String(error) });
138
+ return null;
139
+ }
140
+
141
+ if (attempt < MAX_RETRIES - 1) {
142
+ await Bun.sleep(BASE_RETRY_DELAY_MS * 2 ** attempt);
143
+ }
144
+ }
145
+
146
+ return lastPayload ? { payload: lastPayload, orgId: lastOrgId } : null;
147
+ }
148
+
149
+ interface ClaudeProfile {
150
+ account?: {
151
+ uuid?: string;
152
+ email?: string;
153
+ };
154
+ }
155
+
156
+ async function fetchProfile(
157
+ baseUrl: string,
158
+ headers: Record<string, string>,
159
+ ctx: UsageFetchContext,
160
+ signal?: AbortSignal,
161
+ ): Promise<ClaudeProfile | null> {
162
+ const url = `${baseUrl}/profile`;
163
+ try {
164
+ const response = await ctx.fetch(url, { headers, signal });
165
+ if (!response.ok) return null;
166
+ return (await response.json()) as ClaudeProfile;
167
+ } catch {
168
+ return null;
169
+ }
170
+ }
171
+
172
+ async function resolveEmail(
173
+ params: UsageFetchParams,
174
+ ctx: UsageFetchContext,
175
+ baseUrl: string,
176
+ headers: Record<string, string>,
177
+ ): Promise<string | undefined> {
178
+ if (params.credential.email) return params.credential.email;
179
+
180
+ const profile = await fetchProfile(baseUrl, headers, ctx, params.signal);
181
+ return profile?.account?.email;
182
+ }
183
+
184
+ function buildUsageAmount(utilization: number | undefined): UsageAmount | undefined {
185
+ if (utilization === undefined) return undefined;
186
+ const clamped = Math.min(Math.max(utilization, 0), 100);
187
+ const usedFraction = clamped / 100;
188
+ return {
189
+ used: clamped,
190
+ limit: 100,
191
+ remaining: Math.max(0, 100 - clamped),
192
+ usedFraction,
193
+ remainingFraction: Math.max(0, 1 - usedFraction),
194
+ unit: "percent",
195
+ };
196
+ }
197
+
198
+ function buildUsageStatus(usedFraction: number | undefined): UsageStatus | undefined {
199
+ if (usedFraction === undefined) return undefined;
200
+ if (usedFraction >= 1) return "exhausted";
201
+ if (usedFraction >= 0.9) return "warning";
202
+ return "ok";
203
+ }
204
+
205
+ function buildUsageLimit(args: {
206
+ id: string;
207
+ label: string;
208
+ windowId: string;
209
+ windowLabel: string;
210
+ durationMs: number;
211
+ bucket: ParsedUsageBucket | undefined;
212
+ provider: "anthropic";
213
+ tier?: string;
214
+ shared?: boolean;
215
+ }): UsageLimit | null {
216
+ if (!args.bucket) return null;
217
+ const amount = buildUsageAmount(args.bucket.utilization);
218
+ if (!amount) return null;
219
+ const window: UsageWindow = {
220
+ id: args.windowId,
221
+ label: args.windowLabel,
222
+ durationMs: args.durationMs,
223
+ ...(args.bucket.resetsAt !== undefined ? { resetsAt: args.bucket.resetsAt } : {}),
224
+ };
225
+ return {
226
+ id: args.id,
227
+ label: args.label,
228
+ scope: {
229
+ provider: args.provider,
230
+ windowId: args.windowId,
231
+ tier: args.tier,
232
+ shared: args.shared,
233
+ },
234
+ window,
235
+ amount,
236
+ status: buildUsageStatus(amount.usedFraction),
237
+ };
238
+ }
239
+
240
+ async function fetchClaudeUsage(params: UsageFetchParams, ctx: UsageFetchContext): Promise<UsageReport | null> {
241
+ if (params.provider !== "anthropic") return null;
242
+ const credential = params.credential;
243
+ if (credential.type !== "oauth" || !credential.accessToken) return null;
244
+
245
+ const baseUrl = normalizeClaudeBaseUrl(params.baseUrl);
246
+ const url = `${baseUrl}/usage`;
247
+ const headers: Record<string, string> = {
248
+ ...CLAUDE_HEADERS,
249
+ authorization: `Bearer ${credential.accessToken}`,
250
+ };
251
+
252
+ const payloadResult = await fetchUsagePayload(url, headers, ctx, params.signal);
253
+ if (!payloadResult || !isRecord(payloadResult.payload)) return null;
254
+ const { payload, orgId } = payloadResult;
255
+
256
+ const fiveHour = parseBucket(payload.five_hour);
257
+ const sevenDay = parseBucket(payload.seven_day);
258
+ const sevenDayOpus = parseBucket(payload.seven_day_opus);
259
+ const sevenDaySonnet = parseBucket(payload.seven_day_sonnet);
260
+
261
+ const limits = [
262
+ buildUsageLimit({
263
+ id: "anthropic:5h",
264
+ label: "Claude 5 Hour",
265
+ windowId: "5h",
266
+ windowLabel: "5 Hour",
267
+ durationMs: FIVE_HOURS_MS,
268
+ bucket: fiveHour,
269
+ provider: "anthropic",
270
+ shared: true,
271
+ }),
272
+ buildUsageLimit({
273
+ id: "anthropic:7d",
274
+ label: "Claude 7 Day",
275
+ windowId: "7d",
276
+ windowLabel: "7 Day",
277
+ durationMs: SEVEN_DAYS_MS,
278
+ bucket: sevenDay,
279
+ provider: "anthropic",
280
+ shared: true,
281
+ }),
282
+ buildUsageLimit({
283
+ id: "anthropic:7d:opus",
284
+ label: "Claude 7 Day (Opus)",
285
+ windowId: "7d",
286
+ windowLabel: "7 Day",
287
+ durationMs: SEVEN_DAYS_MS,
288
+ bucket: sevenDayOpus,
289
+ provider: "anthropic",
290
+ tier: "opus",
291
+ }),
292
+ buildUsageLimit({
293
+ id: "anthropic:7d:sonnet",
294
+ label: "Claude 7 Day (Sonnet)",
295
+ windowId: "7d",
296
+ windowLabel: "7 Day",
297
+ durationMs: SEVEN_DAYS_MS,
298
+ bucket: sevenDaySonnet,
299
+ provider: "anthropic",
300
+ tier: "sonnet",
301
+ }),
302
+ ].filter((limit): limit is UsageLimit => limit !== null);
303
+
304
+ if (limits.length === 0) return null;
305
+ const identity = extractUsageIdentity(payload, orgId);
306
+ const accountId = identity.accountId ?? credential.accountId;
307
+ const email = identity.email ?? (await resolveEmail(params, ctx, baseUrl, headers));
308
+
309
+ const report: UsageReport = {
310
+ provider: params.provider,
311
+ fetchedAt: Date.now(),
312
+ limits,
313
+ metadata: {
314
+ accountId,
315
+ email,
316
+ endpoint: url,
317
+ },
318
+ raw: payload,
319
+ };
320
+
321
+ return report;
322
+ }
323
+
324
+ export const claudeUsageProvider: UsageProvider = {
325
+ id: "anthropic",
326
+ fetchUsage: fetchClaudeUsage,
327
+ supports: params => params.provider === "anthropic" && params.credential.type === "oauth",
328
+ };
329
+
330
+ export const claudeRankingStrategy: CredentialRankingStrategy = {
331
+ findWindowLimits(report) {
332
+ const primary = report.limits.find(l => l.id === "anthropic:5h");
333
+ const secondary = report.limits.find(l => l.id === "anthropic:7d");
334
+ return { primary, secondary };
335
+ },
336
+ windowDefaults: { primaryMs: 5 * 60 * 60 * 1000, secondaryMs: 7 * 24 * 60 * 60 * 1000 },
337
+ };
@@ -0,0 +1,248 @@
1
+ import { getGeminiCliHeaders } from "../providers/google-gemini-cli";
2
+ import type {
3
+ UsageAmount,
4
+ UsageFetchContext,
5
+ UsageFetchParams,
6
+ UsageLimit,
7
+ UsageProvider,
8
+ UsageReport,
9
+ UsageWindow,
10
+ } from "../usage";
11
+ import { refreshGoogleCloudToken } from "../utils/oauth/google-gemini-cli";
12
+
13
+ const DEFAULT_ENDPOINT = "https://cloudcode-pa.googleapis.com";
14
+
15
+ const GEMINI_TIER_MAP: Array<{ tier: string; models: string[] }> = [
16
+ {
17
+ tier: "3-Flash",
18
+ models: ["gemini-3-flash-preview", "gemini-3-flash"],
19
+ },
20
+ {
21
+ tier: "Flash",
22
+ models: ["gemini-2.5-flash", "gemini-2.5-flash-lite", "gemini-2.0-flash", "gemini-1.5-flash"],
23
+ },
24
+ {
25
+ tier: "Pro",
26
+ models: ["gemini-2.5-pro", "gemini-3-pro-preview", "gemini-3.1-pro-preview", "gemini-3-pro", "gemini-1.5-pro"],
27
+ },
28
+ ];
29
+
30
+ interface LoadCodeAssistResponse {
31
+ cloudaicompanionProject?: string | { id?: string };
32
+ currentTier?: { id?: string; name?: string };
33
+ }
34
+
35
+ interface RetrieveUserQuotaResponse {
36
+ buckets?: Array<{
37
+ modelId?: string;
38
+ remainingFraction?: number;
39
+ resetTime?: string;
40
+ }>;
41
+ }
42
+
43
+ function getProjectId(payload: LoadCodeAssistResponse | undefined): string | undefined {
44
+ if (!payload) return undefined;
45
+ if (typeof payload.cloudaicompanionProject === "string") {
46
+ return payload.cloudaicompanionProject;
47
+ }
48
+ if (payload.cloudaicompanionProject && typeof payload.cloudaicompanionProject === "object") {
49
+ return payload.cloudaicompanionProject.id;
50
+ }
51
+ return undefined;
52
+ }
53
+
54
+ function getModelTier(modelId: string): string | undefined {
55
+ for (const entry of GEMINI_TIER_MAP) {
56
+ if (entry.models.includes(modelId)) {
57
+ return entry.tier;
58
+ }
59
+ }
60
+ const normalized = modelId.toLowerCase();
61
+ if (normalized.includes("flash")) return "Flash";
62
+ if (normalized.includes("pro")) return "Pro";
63
+ return undefined;
64
+ }
65
+
66
+ function parseWindow(resetTime: string | undefined): UsageWindow {
67
+ if (!resetTime) {
68
+ return {
69
+ id: "quota",
70
+ label: "Quota window",
71
+ };
72
+ }
73
+ const resetsAt = Date.parse(resetTime);
74
+ if (Number.isNaN(resetsAt)) {
75
+ return {
76
+ id: "quota",
77
+ label: "Quota window",
78
+ };
79
+ }
80
+ return {
81
+ id: `reset-${resetsAt}`,
82
+ label: "Quota window",
83
+ resetsAt,
84
+ };
85
+ }
86
+
87
+ function buildAmount(remainingFraction: number | undefined): UsageAmount {
88
+ if (remainingFraction === undefined || !Number.isFinite(remainingFraction)) {
89
+ return { unit: "percent" };
90
+ }
91
+ const remaining = Math.min(Math.max(remainingFraction, 0), 1);
92
+ const used = Math.min(Math.max(1 - remaining, 0), 1);
93
+ return {
94
+ unit: "percent",
95
+ used: Math.round(used * 1000) / 10,
96
+ remaining: Math.round(remaining * 1000) / 10,
97
+ limit: 100,
98
+ usedFraction: used,
99
+ remainingFraction: remaining,
100
+ };
101
+ }
102
+
103
+ async function resolveAccessToken(params: UsageFetchParams, ctx: UsageFetchContext): Promise<string | undefined> {
104
+ const { credential } = params;
105
+ if (credential.type !== "oauth") return undefined;
106
+ if (credential.accessToken && (!credential.expiresAt || credential.expiresAt > Date.now() + 60_000)) {
107
+ return credential.accessToken;
108
+ }
109
+ if (!credential.refreshToken || !credential.projectId) return credential.accessToken;
110
+ try {
111
+ const refreshed = await refreshGoogleCloudToken(credential.refreshToken, credential.projectId);
112
+ return refreshed.access;
113
+ } catch (error) {
114
+ ctx.logger?.warn("Gemini CLI token refresh failed", { error: String(error) });
115
+ return credential.accessToken;
116
+ }
117
+ }
118
+
119
+ async function loadCodeAssist(
120
+ params: UsageFetchParams,
121
+ ctx: UsageFetchContext,
122
+ accessToken: string,
123
+ baseUrl: string,
124
+ projectId?: string,
125
+ ): Promise<LoadCodeAssistResponse | undefined> {
126
+ const response = await ctx.fetch(`${baseUrl}/v1internal:loadCodeAssist`, {
127
+ method: "POST",
128
+ headers: {
129
+ Authorization: `Bearer ${accessToken}`,
130
+ "Content-Type": "application/json",
131
+ ...getGeminiCliHeaders(),
132
+ },
133
+ body: JSON.stringify({
134
+ ...(projectId ? { cloudaicompanionProject: projectId } : {}),
135
+ metadata: {
136
+ ideType: "IDE_UNSPECIFIED",
137
+ platform: "PLATFORM_UNSPECIFIED",
138
+ pluginType: "GEMINI",
139
+ },
140
+ }),
141
+ signal: params.signal,
142
+ });
143
+
144
+ if (!response.ok) {
145
+ const errorText = await response.text();
146
+ ctx.logger?.warn("Gemini CLI loadCodeAssist failed", {
147
+ status: response.status,
148
+ error: errorText,
149
+ });
150
+ return undefined;
151
+ }
152
+
153
+ return (await response.json()) as LoadCodeAssistResponse;
154
+ }
155
+
156
+ async function fetchQuota(
157
+ params: UsageFetchParams,
158
+ ctx: UsageFetchContext,
159
+ accessToken: string,
160
+ baseUrl: string,
161
+ projectId?: string,
162
+ ): Promise<RetrieveUserQuotaResponse | undefined> {
163
+ const response = await ctx.fetch(`${baseUrl}/v1internal:retrieveUserQuota`, {
164
+ method: "POST",
165
+ headers: {
166
+ Authorization: `Bearer ${accessToken}`,
167
+ "Content-Type": "application/json",
168
+ ...getGeminiCliHeaders(),
169
+ },
170
+ body: JSON.stringify(projectId ? { project: projectId } : {}),
171
+ signal: params.signal,
172
+ });
173
+
174
+ if (!response.ok) {
175
+ const errorText = await response.text();
176
+ ctx.logger?.warn("Gemini CLI retrieveUserQuota failed", {
177
+ status: response.status,
178
+ error: errorText,
179
+ });
180
+ return undefined;
181
+ }
182
+
183
+ return (await response.json()) as RetrieveUserQuotaResponse;
184
+ }
185
+
186
+ export const googleGeminiCliUsageProvider: UsageProvider = {
187
+ id: "google-gemini-cli",
188
+ supports: ({ credential }) => credential.type === "oauth" && !!credential.accessToken,
189
+ async fetchUsage(params, ctx) {
190
+ const { credential } = params;
191
+ if (credential.type !== "oauth") {
192
+ return null;
193
+ }
194
+ const accessToken = await resolveAccessToken(params, ctx);
195
+ if (!accessToken) {
196
+ return null;
197
+ }
198
+
199
+ const baseUrl = (params.baseUrl?.trim() || DEFAULT_ENDPOINT).replace(/\/$/, "");
200
+
201
+ const loadResponse = await loadCodeAssist(params, ctx, accessToken, baseUrl, credential.projectId);
202
+ const projectId = credential.projectId ?? getProjectId(loadResponse);
203
+ const quotaResponse = await fetchQuota(params, ctx, accessToken, baseUrl, projectId);
204
+ if (!quotaResponse) {
205
+ return null;
206
+ }
207
+
208
+ const limits: UsageLimit[] = [];
209
+ const buckets = quotaResponse.buckets ?? [];
210
+
211
+ buckets.forEach((bucket, index) => {
212
+ const modelId = bucket.modelId;
213
+ const window = parseWindow(bucket.resetTime);
214
+ const amount = buildAmount(bucket.remainingFraction);
215
+ const tier = modelId ? getModelTier(modelId) : undefined;
216
+ const label = modelId ? `Gemini ${modelId}` : "Gemini quota";
217
+ const id = `${modelId ?? "unknown"}:${window?.id ?? index}`;
218
+
219
+ limits.push({
220
+ id,
221
+ label,
222
+ scope: {
223
+ provider: params.provider,
224
+ accountId: credential.accountId,
225
+ projectId,
226
+ modelId,
227
+ tier,
228
+ windowId: window?.id,
229
+ },
230
+ window,
231
+ amount,
232
+ });
233
+ });
234
+
235
+ const report: UsageReport = {
236
+ provider: params.provider,
237
+ fetchedAt: Date.now(),
238
+ limits,
239
+ metadata: {
240
+ currentTierId: loadResponse?.currentTier?.id,
241
+ currentTierName: loadResponse?.currentTier?.name,
242
+ },
243
+ raw: quotaResponse,
244
+ };
245
+
246
+ return report;
247
+ },
248
+ };