@diousk/pi-usage-fast 0.1.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.
package/src/query.ts ADDED
@@ -0,0 +1,1073 @@
1
+ // This module intentionally keeps adapter transport beside resolved-auth and origin validation;
2
+ // separating that security boundary would duplicate request and redaction policy across providers.
3
+ import { randomBytes } from "node:crypto";
4
+ import { type ExtensionContext, readStoredCredential } from "@earendil-works/pi-coding-agent";
5
+ import { errorMessage, fingerprintResolvedAuth, redactUsageError } from "./core.js";
6
+ import { fallbackOAuthCredentialCandidates, type OAuthCredentialCandidateReader } from "./oauth-credential-source.js";
7
+ import { normalizeBasetenBillingUsagePayload } from "./providers/baseten.js";
8
+ import { normalizeCodexBackendPayload } from "./providers/codex.js";
9
+ import { normalizeDeepSeekBalancePayload } from "./providers/deepseek.js";
10
+ import { createFireworksAdapter } from "./providers/fireworks.js";
11
+ import { normalizeGitHubCopilotUsagePayload } from "./providers/github-copilot.js";
12
+ import { normalizeKimiCodingUsagePayload } from "./providers/kimi-coding.js";
13
+ import { type MiniMaxProviderId, miniMaxUsageKind, normalizeMiniMaxUsagePayload } from "./providers/minimax.js";
14
+ import { normalizeMoonshotBalancePayload } from "./providers/moonshot.js";
15
+ import { normalizeOpenCodeZenPayload } from "./providers/opencode-zen.js";
16
+ import { normalizeOpenRouterKeyPayload } from "./providers/openrouter.js";
17
+ import { normalizeVercelAIGatewayCreditsPayload } from "./providers/vercel-ai-gateway.js";
18
+ import { normalizeXaiBillingPayload } from "./providers/xai.js";
19
+ import { normalizeZaiQuotaPayload, normalizeZaiSubscriptionPayload } from "./providers/zai.js";
20
+ import { zaiResponseError } from "./providers/zai-errors.js";
21
+ import type {
22
+ BasetenBillingUsagePayload,
23
+ CodexBackendPayload,
24
+ DeepSeekBalancePayload,
25
+ GitHubCopilotUsagePayload,
26
+ KimiCodingUsagePayload,
27
+ MiniMaxUsagePayload,
28
+ MoonshotBalancePayload,
29
+ OpenCodeZenPayload,
30
+ OpenRouterKeyPayload,
31
+ PiModel,
32
+ ResolvedUsageAuth,
33
+ UsageProviderAdapter,
34
+ UsageQuerySettings,
35
+ UsageReport,
36
+ UsageRequestGuard,
37
+ VercelAIGatewayCreditsPayload,
38
+ XaiBillingPayload,
39
+ XaiUserPayload,
40
+ ZaiPlanInfo,
41
+ ZaiQuotaPayload,
42
+ ZaiSubscriptionPayload,
43
+ } from "./types.js";
44
+ import { resolveUsageTarget } from "./usage-targets.js";
45
+
46
+ const BASETEN_BILLING_USAGE_URL = "https://api.baseten.co/v1/billing/usage_summary";
47
+ const BASETEN_USAGE_WINDOW_DAYS = 30;
48
+ const CODEX_USAGE_URL = "https://chatgpt.com/backend-api/wham/usage";
49
+ const DEEPSEEK_BALANCE_URL = "https://api.deepseek.com/user/balance";
50
+ const GITHUB_COPILOT_USAGE_URL = "https://api.github.com/copilot_internal/user";
51
+ const OPENROUTER_KEY_URL = "https://openrouter.ai/api/v1/key";
52
+ const VERCEL_AI_GATEWAY_CREDITS_URL = "https://ai-gateway.vercel.sh/v1/credits";
53
+ const OPENCODE_GO_USAGE_URL = "https://opencode.ai/zen/go/v1/usage";
54
+ const KIMI_CODING_USAGE_URL = "https://api.kimi.com/coding/v1/usages";
55
+ const MINIMAX_API_ROOTS = Object.freeze({
56
+ minimax: "https://api.minimax.io",
57
+ "minimax-cn": "https://api.minimaxi.com",
58
+ });
59
+ const MOONSHOT_BALANCE_URLS = Object.freeze({
60
+ moonshotai: "https://api.moonshot.ai/v1/users/me/balance",
61
+ "moonshotai-cn": "https://api.moonshot.cn/v1/users/me/balance",
62
+ });
63
+ const SHARED_MOONSHOT_ENV_VAR = "MOONSHOT_API_KEY";
64
+ const XAI_USER_URL = "https://cli-chat-proxy.grok.com/v1/user?include=subscription";
65
+ const XAI_BILLING_URL = "https://cli-chat-proxy.grok.com/v1/billing?format=credits";
66
+ const XAI_CLIENT_HEADERS = Object.freeze({
67
+ "X-XAI-Token-Auth": "xai-grok-cli",
68
+ "x-grok-client-version": "1.0.10",
69
+ "x-grok-client-mode": "interactive",
70
+ });
71
+ const MAX_SUCCESS_BODY_BYTES = 64 * 1024;
72
+ const MAX_ERROR_BODY_BYTES = 4 * 1024;
73
+
74
+ export const AUTH_FINGERPRINT_SALT = randomBytes(32);
75
+
76
+ export const SUPPORTED_ADAPTERS: readonly UsageProviderAdapter[] = [
77
+ {
78
+ id: "baseten",
79
+ displayName: "Baseten",
80
+ semantics: { kind: "api-key", label: "Organization Model APIs spend" },
81
+ async query(auth, signal, timeoutMs, guard) {
82
+ if (!guard) throw new Error("Baseten billing usage requires request-boundary revalidation.");
83
+ const startedAt = Date.now();
84
+ await guard();
85
+ const windowAt = Date.now();
86
+ const payload = (await fetchProviderJson(
87
+ basetenBillingUsageUrl(windowAt),
88
+ auth,
89
+ signal,
90
+ remainingTimeout(timeoutMs, startedAt, "fetching Baseten billing usage"),
91
+ "Baseten billing usage endpoint",
92
+ { redirect: "error" },
93
+ )) as BasetenBillingUsagePayload;
94
+ await guard();
95
+ return normalizeBasetenBillingUsagePayload(payload, Date.now());
96
+ },
97
+ },
98
+ {
99
+ id: "openai-codex",
100
+ displayName: "OpenAI Codex",
101
+ semantics: {
102
+ kind: "consumer-subscription",
103
+ label: "ChatGPT subscription limits",
104
+ },
105
+ async query(auth, signal, timeoutMs) {
106
+ const payload = await fetchProviderJson(CODEX_USAGE_URL, auth, signal, timeoutMs, "Codex usage endpoint");
107
+ return normalizeCodexBackendPayload(payload as CodexBackendPayload, Date.now());
108
+ },
109
+ },
110
+ {
111
+ id: "deepseek",
112
+ displayName: "DeepSeek",
113
+ semantics: { kind: "api-key", label: "DeepSeek API balance" },
114
+ async query(auth, signal, timeoutMs, guard) {
115
+ if (!guard) throw new Error("DeepSeek API balance requires request-boundary revalidation.");
116
+ const startedAt = Date.now();
117
+ await guard();
118
+ const remainingMs = timeoutMs - (Date.now() - startedAt);
119
+ if (remainingMs <= 0) throw new Error("Timed out while revalidating DeepSeek runtime auth.");
120
+ const payload = await fetchProviderJson(
121
+ DEEPSEEK_BALANCE_URL,
122
+ auth,
123
+ signal,
124
+ remainingMs,
125
+ "DeepSeek API balance endpoint",
126
+ { redirect: "error" },
127
+ );
128
+ return normalizeDeepSeekBalancePayload(payload as DeepSeekBalancePayload, Date.now());
129
+ },
130
+ },
131
+ {
132
+ id: "github-copilot",
133
+ displayName: "GitHub Copilot",
134
+ semantics: {
135
+ kind: "consumer-subscription",
136
+ label: "GitHub Copilot account allowance",
137
+ },
138
+ async query(auth, signal, timeoutMs) {
139
+ const payload = await fetchProviderJson(
140
+ GITHUB_COPILOT_USAGE_URL,
141
+ auth,
142
+ signal,
143
+ timeoutMs,
144
+ "GitHub Copilot usage endpoint",
145
+ );
146
+ return normalizeGitHubCopilotUsagePayload(payload as GitHubCopilotUsagePayload, Date.now());
147
+ },
148
+ },
149
+ {
150
+ id: "openrouter",
151
+ displayName: "OpenRouter",
152
+ semantics: { kind: "api-key", label: "API-key spend limits" },
153
+ async query(auth, signal, timeoutMs) {
154
+ const payload = await fetchProviderJson(OPENROUTER_KEY_URL, auth, signal, timeoutMs, "OpenRouter key endpoint");
155
+ return normalizeOpenRouterKeyPayload(payload as OpenRouterKeyPayload, Date.now());
156
+ },
157
+ },
158
+ {
159
+ id: "vercel-ai-gateway",
160
+ displayName: "Vercel AI Gateway",
161
+ semantics: { kind: "api-key", label: "AI Gateway credits and lifetime spend" },
162
+ async query(auth, signal, timeoutMs, guard) {
163
+ if (!guard) throw new Error("Vercel AI Gateway usage requires request-boundary revalidation.");
164
+ const startedAt = Date.now();
165
+ await guard();
166
+ const payload = (await fetchProviderJson(
167
+ VERCEL_AI_GATEWAY_CREDITS_URL,
168
+ auth,
169
+ signal,
170
+ remainingTimeout(timeoutMs, startedAt, "fetching Vercel AI Gateway credits"),
171
+ "Vercel AI Gateway credits endpoint",
172
+ { redirect: "error" },
173
+ )) as VercelAIGatewayCreditsPayload;
174
+ await guard();
175
+ return normalizeVercelAIGatewayCreditsPayload(payload, Date.now());
176
+ },
177
+ },
178
+ createFireworksAdapter(fetchProviderJson),
179
+ {
180
+ id: "opencode-go",
181
+ displayName: "OpenCode Go",
182
+ semantics: { kind: "consumer-subscription", label: "OpenCode Zen plan usage" },
183
+ async query(auth, signal, timeoutMs) {
184
+ const payload = await fetchProviderJson(
185
+ OPENCODE_GO_USAGE_URL,
186
+ auth,
187
+ signal,
188
+ timeoutMs,
189
+ "OpenCode Zen usage endpoint",
190
+ );
191
+ return normalizeOpenCodeZenPayload(payload as OpenCodeZenPayload, Date.now());
192
+ },
193
+ },
194
+ {
195
+ id: "kimi-coding",
196
+ displayName: "Kimi For Coding",
197
+ semantics: { kind: "consumer-subscription", label: "Kimi Coding Plan usage" },
198
+ async query(auth, signal, timeoutMs) {
199
+ const payload = await fetchProviderJson(
200
+ KIMI_CODING_USAGE_URL,
201
+ auth,
202
+ signal,
203
+ timeoutMs,
204
+ "Kimi Coding usage endpoint",
205
+ { redirect: "error" },
206
+ );
207
+ return normalizeKimiCodingUsagePayload(payload as KimiCodingUsagePayload, Date.now());
208
+ },
209
+ },
210
+ {
211
+ id: "minimax",
212
+ displayName: "MiniMax",
213
+ semantics: { kind: "consumer-subscription", label: "MiniMax usage" },
214
+ async query(auth, signal, timeoutMs, guard) {
215
+ return queryMiniMaxUsage("minimax", auth, signal, timeoutMs, guard);
216
+ },
217
+ },
218
+ {
219
+ id: "minimax-cn",
220
+ displayName: "MiniMax CN",
221
+ semantics: { kind: "consumer-subscription", label: "MiniMax usage" },
222
+ async query(auth, signal, timeoutMs, guard) {
223
+ return queryMiniMaxUsage("minimax-cn", auth, signal, timeoutMs, guard);
224
+ },
225
+ },
226
+ {
227
+ id: "moonshotai",
228
+ displayName: "Moonshot AI",
229
+ semantics: { kind: "api-key", label: "Moonshot API account balance" },
230
+ async query(auth, signal, timeoutMs, guard) {
231
+ return queryMoonshotBalance("moonshotai", auth, signal, timeoutMs, guard);
232
+ },
233
+ },
234
+ {
235
+ id: "moonshotai-cn",
236
+ displayName: "Moonshot AI CN",
237
+ semantics: { kind: "api-key", label: "Moonshot API account balance" },
238
+ async query(auth, signal, timeoutMs, guard) {
239
+ return queryMoonshotBalance("moonshotai-cn", auth, signal, timeoutMs, guard);
240
+ },
241
+ },
242
+ {
243
+ id: "zai",
244
+ displayName: "Z.AI",
245
+ invalidateCacheOnFailure: true,
246
+ semantics: { kind: "consumer-subscription", label: "GLM Coding Plan usage" },
247
+ async query(auth, signal, timeoutMs, guard) {
248
+ return queryZaiUsage("zai", "Z.AI", auth, signal, timeoutMs, guard);
249
+ },
250
+ },
251
+ {
252
+ id: "zai-coding-cn",
253
+ displayName: "Z.AI Coding CN",
254
+ invalidateCacheOnFailure: true,
255
+ semantics: { kind: "consumer-subscription", label: "GLM Coding Plan usage" },
256
+ async query(auth, signal, timeoutMs, guard) {
257
+ return queryZaiUsage("zai-coding-cn", "Z.AI Coding CN", auth, signal, timeoutMs, guard);
258
+ },
259
+ },
260
+ ];
261
+
262
+ // Reviewed contract pins:
263
+ // - Pi xAI provider at https://api.x.ai and OAuth scope
264
+ // "openid profile email offline_access grok-cli:access api:access" at
265
+ // e86823096c5bad39e1ca282ec24bc5eb9bec745b, unchanged at
266
+ // ccfe79ed238674f760c986e3a61493aab794000a.
267
+ // - Grok Build identity, credits routes/structs, required token-auth and version headers, and
268
+ // client-mode telemetry at 9684fa3cdbf2995e30ea8b9b637f1db008f144fc (client version 1.0.10).
269
+ // - xAI Management API's separate team billing boundary at
270
+ // 723dd2aa22d17be35617463837dc47cda008d90e.
271
+ // x-userid remains attached only to billing to bind the proxy-canonical identity as Grok Build does.
272
+ export const XAI_ADAPTER: UsageProviderAdapter = {
273
+ id: "xai",
274
+ displayName: "xAI",
275
+ semantics: {
276
+ kind: "consumer-subscription",
277
+ label: "xAI consumer subscription usage",
278
+ },
279
+ publishesStatusline: false,
280
+ async query(auth, signal, timeoutMs, guard) {
281
+ if (!guard) throw new Error("xAI usage requires request-boundary revalidation.");
282
+ const startedAt = Date.now();
283
+ const clientAuth = {
284
+ ...auth,
285
+ headers: { ...auth.headers, ...XAI_CLIENT_HEADERS },
286
+ };
287
+ await guard();
288
+ const userPayload = (await fetchProviderJson(
289
+ XAI_USER_URL,
290
+ clientAuth,
291
+ signal,
292
+ remainingTimeout(timeoutMs, startedAt),
293
+ "xAI consumer identity endpoint",
294
+ { redirect: "error", userAgent: false },
295
+ )) as XaiUserPayload;
296
+ await guard();
297
+ const userId = validatedXaiUserId(userPayload.userId);
298
+ const billingAuth = {
299
+ ...clientAuth,
300
+ headers: { ...clientAuth.headers, "x-userid": userId },
301
+ secrets: [...clientAuth.secrets, userId],
302
+ };
303
+ await guard();
304
+ const billingPayload = (await fetchProviderJson(
305
+ XAI_BILLING_URL,
306
+ billingAuth,
307
+ signal,
308
+ remainingTimeout(timeoutMs, startedAt),
309
+ "xAI consumer billing endpoint",
310
+ { redirect: "error", userAgent: false },
311
+ )) as XaiBillingPayload;
312
+ await guard();
313
+ return normalizeXaiBillingPayload(billingPayload, userPayload.subscriptionTier, Date.now());
314
+ },
315
+ };
316
+
317
+ export function usageAdapters(): readonly UsageProviderAdapter[] {
318
+ return [...SUPPORTED_ADAPTERS, XAI_ADAPTER];
319
+ }
320
+
321
+ export function adapterForProvider(providerId: string | undefined): UsageProviderAdapter | undefined {
322
+ return usageAdapters().find((adapter) => adapter.id === providerId);
323
+ }
324
+
325
+ export function isStaleExtensionContextError(error: unknown): boolean {
326
+ return (
327
+ error instanceof Error && error.message.includes("This extension ctx is stale after session replacement or reload")
328
+ );
329
+ }
330
+
331
+ export async function resolveUsageAuth(
332
+ ctx: ExtensionContext,
333
+ adapter: UsageProviderAdapter,
334
+ salt: Uint8Array = AUTH_FINGERPRINT_SALT,
335
+ credentialReader: StoredCredentialReader = readStoredCredential,
336
+ candidateReader?: OAuthCredentialCandidateReader,
337
+ ): Promise<ResolvedUsageAuth | undefined> {
338
+ if (ctx.model?.provider === adapter.id && !hasOfficialOrigin(ctx.model, adapter.id)) {
339
+ throw new Error(
340
+ `${adapter.displayName} usage cannot send a custom provider base URL credential to the official usage endpoint.`,
341
+ );
342
+ }
343
+
344
+ const model = candidateModels(ctx, adapter.id).find((candidate) => hasOfficialOrigin(candidate, adapter.id));
345
+ if (!model) return undefined;
346
+ if (
347
+ ["github-copilot", "xai"].includes(adapter.id) &&
348
+ candidateReader?.waitUntilReady &&
349
+ !(await candidateReader.waitUntilReady(ctx, adapter.id))
350
+ ) {
351
+ throw new Error(`${adapter.displayName} OAuth credential readiness failed closed.`);
352
+ }
353
+ // SAFETY: Pi exposes the required auth methods at runtime, and checks below narrow them before use.
354
+ const registry = ctx.modelRegistry as unknown as UsageAuthRegistry;
355
+ const provider = registry.getProvider?.(adapter.id);
356
+ if (provider?.baseUrl && !hasOfficialUrlOrigin(provider.baseUrl, adapter.id)) {
357
+ throw new Error(
358
+ `${adapter.displayName} usage cannot send an overridden provider credential to the official usage endpoint.`,
359
+ );
360
+ }
361
+ let modelAuth: RequestAuth | undefined;
362
+ const currentModel = ctx.model?.provider === adapter.id ? ctx.model : undefined;
363
+ const resolveCurrentModelAuth = async (): Promise<RequestAuth | undefined> => {
364
+ if (!currentModel || typeof registry.getApiKeyAndHeaders !== "function") return undefined;
365
+ const result = await registry.getApiKeyAndHeaders(currentModel);
366
+ if (!result.ok) throw new Error(redactUsageError(result.error));
367
+ return authorizationFrom(result) ? result : undefined;
368
+ };
369
+ const resolveSelectedAuthLast = ["deepseek", "minimax", "minimax-cn"].includes(adapter.id);
370
+ if (!resolveSelectedAuthLast) modelAuth = await resolveCurrentModelAuth();
371
+ if (typeof registry.getProviderAuth !== "function") {
372
+ throw new Error("pi-usage requires Pi 0.81.0 or newer to validate resolved provider auth.");
373
+ }
374
+ if (!moonshotProviderAuthIsAllowed(ctx, adapter.id)) return undefined;
375
+ const providerResult = await registry.getProviderAuth(adapter.id);
376
+ if (!moonshotProviderAuthIsAllowed(ctx, adapter.id)) return undefined;
377
+ if (providerResult?.auth.baseUrl && !hasOfficialUrlOrigin(providerResult.auth.baseUrl, adapter.id)) {
378
+ throw new Error(
379
+ `${adapter.displayName} usage cannot send a proxy-resolved credential to the official usage endpoint.`,
380
+ );
381
+ }
382
+ // Providers with credential-change retries read selected-model auth last so a rotation during
383
+ // provider-origin validation cannot leave the earlier credential queued for the usage request.
384
+ if (resolveSelectedAuthLast) modelAuth = await resolveCurrentModelAuth();
385
+ if (modelAuth?.baseUrl && !hasOfficialUrlOrigin(modelAuth.baseUrl, adapter.id)) {
386
+ throw new Error(
387
+ `${adapter.displayName} usage cannot send model-resolved proxy credentials to the official usage endpoint.`,
388
+ );
389
+ }
390
+ const auth = modelAuth ?? providerResult?.auth;
391
+ if (!auth) return undefined;
392
+ const finalize = (resolved: ResolvedUsageAuth): ResolvedUsageAuth => {
393
+ const preservedAuth = { ...(providerResult?.auth ?? auth) };
394
+ const env = providerResult?.env ?? modelAuth?.env;
395
+ const source = providerResult?.source;
396
+ const effectiveBaseUrl = modelAuth?.baseUrl ?? providerResult?.auth.baseUrl ?? provider?.baseUrl ?? model.baseUrl;
397
+ const redactionInputs = [
398
+ preservedAuth.apiKey,
399
+ ...Object.values(preservedAuth.headers ?? {}),
400
+ ...Object.values(env ?? {}),
401
+ modelAuth?.apiKey,
402
+ ...Object.values(modelAuth?.headers ?? {}),
403
+ ].filter((value): value is string => typeof value === "string" && value.length > 0);
404
+ return {
405
+ ...resolved,
406
+ auth: preservedAuth,
407
+ ...(env ? { env: { ...env } } : {}),
408
+ ...(source ? { source } : {}),
409
+ effectiveBaseUrl,
410
+ secrets: [...new Set([...resolved.secrets, ...redactionInputs])],
411
+ fingerprint: fingerprintResolvedAuth(
412
+ {
413
+ apiKey: resolved.apiKey,
414
+ headers: resolved.headers,
415
+ baseUrl: effectiveBaseUrl,
416
+ env,
417
+ source,
418
+ providerAuth: preservedAuth,
419
+ },
420
+ salt,
421
+ ),
422
+ };
423
+ };
424
+ if (adapter.id === "github-copilot") {
425
+ const offered = candidateReader
426
+ ? await candidateReader(ctx, adapter.id)
427
+ : fallbackOAuthCredentialCandidates(adapter.id, credentialReader);
428
+ if (!offered.ok) {
429
+ throw new Error("GitHub Copilot OAuth credential discovery failed closed.");
430
+ }
431
+ return finalize(resolveGitHubCopilotUsageAuth(auth, model, salt, offered.candidates, offered.offeredCount === 0));
432
+ }
433
+ if (adapter.id === "xai") {
434
+ const offered = candidateReader
435
+ ? await candidateReader(ctx, adapter.id)
436
+ : fallbackOAuthCredentialCandidates(adapter.id, credentialReader);
437
+ if (!offered.ok) throw new Error("xAI OAuth credential discovery failed closed.");
438
+ return finalize(resolveXaiUsageAuth(auth, model, salt, offered.candidates));
439
+ }
440
+ if (adapter.id === "deepseek") {
441
+ const resolvedAuthorization = authorizationFrom(auth);
442
+ const access = bearerToken(resolvedAuthorization);
443
+ if (!access) throw new Error("DeepSeek API balance requires Bearer authentication.");
444
+ const authorization = `Bearer ${access}`;
445
+ const headers = { Authorization: authorization };
446
+ return finalize({
447
+ apiKey: access,
448
+ headers,
449
+ fingerprint: "",
450
+ secrets: [
451
+ access,
452
+ auth.apiKey,
453
+ headerValue(auth.headers, "Authorization"),
454
+ resolvedAuthorization,
455
+ authorization,
456
+ ].filter((value): value is string => Boolean(value)),
457
+ model,
458
+ });
459
+ }
460
+ const authorization = authorizationFrom(auth);
461
+ if (!authorization) return undefined;
462
+ const headers = { Authorization: authorization };
463
+ const secrets = [auth.apiKey, headerValue(auth.headers, "Authorization"), authorization].filter(
464
+ (value): value is string => Boolean(value),
465
+ );
466
+ return finalize({
467
+ apiKey: auth.apiKey,
468
+ headers,
469
+ fingerprint: "",
470
+ secrets,
471
+ model,
472
+ });
473
+ }
474
+
475
+ export async function queryProviderUsage(
476
+ adapter: UsageProviderAdapter,
477
+ auth: ResolvedUsageAuth,
478
+ signal: AbortSignal,
479
+ timeoutMs: number,
480
+ guard?: UsageRequestGuard,
481
+ targetOrSettings?: string | Readonly<UsageQuerySettings>,
482
+ ): Promise<UsageReport> {
483
+ const startedAt = Date.now();
484
+ let targetId =
485
+ typeof targetOrSettings === "string"
486
+ ? targetOrSettings
487
+ : adapter.id === "fireworks"
488
+ ? targetOrSettings?.fireworksAccountId
489
+ : undefined;
490
+ let resolvedLegacyFireworksTarget = false;
491
+ try {
492
+ if (adapter.id === "fireworks" && typeof targetOrSettings !== "string" && adapter.targets && guard) {
493
+ const target = await resolveUsageTarget(
494
+ adapter,
495
+ auth,
496
+ targetId,
497
+ signal,
498
+ remainingTimeout(timeoutMs, startedAt, "resolving the Fireworks account"),
499
+ guard,
500
+ );
501
+ if (target.kind === "selection-required") {
502
+ throw new Error("Fireworks account selection is required.");
503
+ }
504
+ targetId = target.targetId;
505
+ resolvedLegacyFireworksTarget = true;
506
+ }
507
+ return await adapter.query(
508
+ auth,
509
+ signal,
510
+ resolvedLegacyFireworksTarget
511
+ ? remainingTimeout(timeoutMs, startedAt, `querying ${adapter.displayName} usage`)
512
+ : timeoutMs,
513
+ guard,
514
+ targetId,
515
+ );
516
+ } catch (error) {
517
+ if (isStaleExtensionContextError(error) || isAbortError(error)) throw error;
518
+ throw new Error(redactUsageError(errorMessage(error), auth.secrets));
519
+ }
520
+ }
521
+
522
+ export function providerIsConfigured(ctx: ExtensionContext, providerId: string): boolean {
523
+ try {
524
+ const status = ctx.modelRegistry.getProviderAuthStatus(providerId);
525
+ return status.configured && moonshotProviderAuthSourceIsAllowed(ctx, providerId, status.source, status.label);
526
+ } catch {
527
+ return !isMoonshotSiblingProvider(ctx, providerId) && candidateModels(ctx, providerId).length > 0;
528
+ }
529
+ }
530
+
531
+ function moonshotProviderAuthIsAllowed(ctx: ExtensionContext, providerId: string): boolean {
532
+ if (!isMoonshotSiblingProvider(ctx, providerId)) return true;
533
+ try {
534
+ const status = ctx.modelRegistry.getProviderAuthStatus(providerId);
535
+ return moonshotProviderAuthSourceIsAllowed(ctx, providerId, status.source, status.label);
536
+ } catch {
537
+ return false;
538
+ }
539
+ }
540
+
541
+ function moonshotProviderAuthSourceIsAllowed(
542
+ ctx: ExtensionContext,
543
+ providerId: string,
544
+ source: string | undefined,
545
+ label: string | undefined,
546
+ ): boolean {
547
+ if (!isMoonshotSiblingProvider(ctx, providerId)) return true;
548
+ if (source === undefined) return false;
549
+ if (source !== "environment") return true;
550
+ return (
551
+ label !== undefined &&
552
+ !label
553
+ .split(",")
554
+ .map((name) => name.trim())
555
+ .includes(SHARED_MOONSHOT_ENV_VAR)
556
+ );
557
+ }
558
+
559
+ function isMoonshotSiblingProvider(ctx: ExtensionContext, providerId: string): boolean {
560
+ return (providerId === "moonshotai" || providerId === "moonshotai-cn") && ctx.model?.provider !== providerId;
561
+ }
562
+
563
+ function candidateModels(ctx: ExtensionContext, providerId: string): PiModel[] {
564
+ const candidates: PiModel[] = [];
565
+ const seen = new Set<string>();
566
+ const add = (model: PiModel | undefined) => {
567
+ if (!model || model.provider !== providerId) return;
568
+ const key = `${model.provider}/${model.id}`;
569
+ if (seen.has(key)) return;
570
+ seen.add(key);
571
+ candidates.push(model);
572
+ };
573
+ add(ctx.model);
574
+ for (const model of ctx.modelRegistry.getAvailable()) add(model);
575
+ for (const model of ctx.modelRegistry.getAll()) add(model);
576
+ return candidates;
577
+ }
578
+
579
+ export async function fetchProviderJson(
580
+ url: string,
581
+ auth: ResolvedUsageAuth,
582
+ signal: AbortSignal,
583
+ timeoutMs: number,
584
+ description: string,
585
+ request: {
586
+ method?: "GET" | "POST";
587
+ body?: Record<string, unknown>;
588
+ redirect?: RequestRedirect;
589
+ userAgent?: boolean;
590
+ responseError?: (status: number, text: string) => string | undefined;
591
+ } = {},
592
+ ): Promise<Record<string, unknown>> {
593
+ const controller = new AbortController();
594
+ let timedOut = false;
595
+ const abortFromCaller = () => controller.abort();
596
+ if (signal.aborted) controller.abort();
597
+ else signal.addEventListener("abort", abortFromCaller, { once: true });
598
+ const timeout = setTimeout(() => {
599
+ timedOut = true;
600
+ controller.abort();
601
+ }, timeoutMs);
602
+ try {
603
+ const headers = { ...auth.headers };
604
+ if (request.userAgent !== false && !hasHeader(headers, "User-Agent")) {
605
+ headers["User-Agent"] = "pi-usage";
606
+ }
607
+ if (request.body && !hasHeader(headers, "Content-Type")) {
608
+ headers["Content-Type"] = "application/json";
609
+ }
610
+ const response = await fetch(url, {
611
+ method: request.method ?? "GET",
612
+ headers,
613
+ ...(request.body ? { body: JSON.stringify(request.body) } : {}),
614
+ ...(request.redirect ? { redirect: request.redirect } : {}),
615
+ signal: controller.signal,
616
+ });
617
+ if (response.redirected) throw new Error(`${description} refused a redirected response.`);
618
+ if (controller.signal.aborted) throw Object.assign(new Error("Usage query aborted."), { name: "AbortError" });
619
+ const text = await readBoundedResponse(
620
+ response,
621
+ response.ok ? MAX_SUCCESS_BODY_BYTES : MAX_ERROR_BODY_BYTES,
622
+ !response.ok,
623
+ description,
624
+ controller.signal,
625
+ );
626
+ if (controller.signal.aborted) throw Object.assign(new Error("Usage query aborted."), { name: "AbortError" });
627
+ const responseError = request.responseError?.(response.status, text);
628
+ if (responseError) throw new Error(responseError);
629
+ if (!response.ok) {
630
+ throw new Error(
631
+ `${description} returned ${response.status} ${response.statusText}: ${redactUsageError(text, auth.secrets)}`,
632
+ );
633
+ }
634
+ let parsed: unknown;
635
+ try {
636
+ parsed = JSON.parse(text) as unknown;
637
+ } catch (error) {
638
+ throw new Error(`${description} returned invalid JSON: ${errorMessage(error)}`);
639
+ }
640
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
641
+ throw new Error(`${description} response was not an object.`);
642
+ }
643
+ return parsed as Record<string, unknown>;
644
+ } catch (error) {
645
+ if (timedOut) {
646
+ throw new Error(`Timed out after ${Math.round(timeoutMs / 1000)}s while fetching usage.`);
647
+ }
648
+ if (signal.aborted) throw Object.assign(new Error("Usage query aborted."), { name: "AbortError" });
649
+ throw error;
650
+ } finally {
651
+ clearTimeout(timeout);
652
+ signal.removeEventListener("abort", abortFromCaller);
653
+ }
654
+ }
655
+
656
+ async function readBoundedResponse(
657
+ response: Response,
658
+ maxBytes: number,
659
+ truncateOverflow: boolean,
660
+ description: string,
661
+ signal: AbortSignal,
662
+ ): Promise<string> {
663
+ if (!response.body) return "";
664
+ const reader = response.body.getReader();
665
+ const chunks: Uint8Array[] = [];
666
+ let total = 0;
667
+ let truncated = false;
668
+ const abort = () => void reader.cancel().catch(() => undefined);
669
+ if (signal.aborted) abort();
670
+ else signal.addEventListener("abort", abort, { once: true });
671
+ try {
672
+ while (true) {
673
+ const { done, value } = await reader.read();
674
+ if (done) break;
675
+ const remaining = maxBytes - total;
676
+ if (value.byteLength > remaining) {
677
+ if (remaining > 0) chunks.push(value.subarray(0, remaining));
678
+ total = maxBytes;
679
+ truncated = true;
680
+ await reader.cancel();
681
+ break;
682
+ }
683
+ chunks.push(value);
684
+ total += value.byteLength;
685
+ }
686
+ } finally {
687
+ signal.removeEventListener("abort", abort);
688
+ reader.releaseLock();
689
+ }
690
+ if (truncated && !truncateOverflow) {
691
+ throw new Error(`${description} response exceeded ${maxBytes} bytes.`);
692
+ }
693
+ const body = new Uint8Array(total);
694
+ let offset = 0;
695
+ for (const chunk of chunks) {
696
+ body.set(chunk, offset);
697
+ offset += chunk.byteLength;
698
+ }
699
+ const text = new TextDecoder().decode(body);
700
+ return truncated ? `${text}…` : text;
701
+ }
702
+
703
+ type RequestAuth = {
704
+ apiKey?: string;
705
+ headers?: Record<string, string | null>;
706
+ baseUrl?: string;
707
+ env?: Record<string, string>;
708
+ };
709
+
710
+ type StoredCredentialReader = (providerId: string) => unknown;
711
+
712
+ type UsageAuthRegistry = {
713
+ getProvider?(providerId: string): { baseUrl?: string } | undefined;
714
+ getApiKeyAndHeaders?(model: PiModel): Promise<({ ok: true } & RequestAuth) | { ok: false; error: string }>;
715
+ getProviderAuth?(providerId: string): Promise<
716
+ | {
717
+ auth: RequestAuth;
718
+ env?: Record<string, string>;
719
+ source?: string;
720
+ }
721
+ | undefined
722
+ >;
723
+ };
724
+
725
+ function resolveXaiUsageAuth(
726
+ auth: RequestAuth,
727
+ model: PiModel,
728
+ salt: Uint8Array,
729
+ candidates: readonly unknown[],
730
+ ): ResolvedUsageAuth {
731
+ const resolvedAccess = bearerToken(headerValue(auth.headers, "Authorization")) ?? auth.apiKey;
732
+ if (!resolvedAccess) throw new Error("xAI runtime authentication was incomplete.");
733
+ let sawOAuth = false;
734
+ let sawMatchingAccess = false;
735
+ let sawIncompleteMatch = false;
736
+ const matches: Array<{ access: string; refresh: string }> = [];
737
+ for (const candidate of candidates) {
738
+ try {
739
+ const credential = asObject(candidate);
740
+ if (credential?.type !== "oauth") continue;
741
+ sawOAuth = true;
742
+ if (credential.access !== resolvedAccess) continue;
743
+ sawMatchingAccess = true;
744
+ if (
745
+ typeof credential.access !== "string" ||
746
+ !credential.access ||
747
+ typeof credential.refresh !== "string" ||
748
+ !credential.refresh ||
749
+ typeof credential.expires !== "number" ||
750
+ !Number.isFinite(credential.expires)
751
+ ) {
752
+ sawIncompleteMatch = true;
753
+ continue;
754
+ }
755
+ matches.push({ access: credential.access, refresh: credential.refresh });
756
+ } catch {
757
+ // Malformed candidates never authorize a consumer-proxy request.
758
+ }
759
+ }
760
+ if (sawIncompleteMatch) throw new Error("The matching xAI OAuth credential was incomplete.");
761
+ if (matches.length > 1) {
762
+ throw new Error("Multiple OAuth credentials match the active xAI runtime account.");
763
+ }
764
+ const match = matches[0];
765
+ if (!match) {
766
+ if (!sawOAuth) {
767
+ throw new Error(
768
+ "xAI consumer usage requires the OAuth subscription account configured through Pi /login; XAI_API_KEY users can review API spend at console.x.ai.",
769
+ );
770
+ }
771
+ if (sawMatchingAccess) throw new Error("The matching xAI OAuth credential was incomplete.");
772
+ throw new Error("The active xAI runtime account does not match Pi's stored OAuth account.");
773
+ }
774
+ const authorization = `Bearer ${match.access}`;
775
+ const headers = { Authorization: authorization };
776
+ return {
777
+ apiKey: match.access,
778
+ headers,
779
+ fingerprint: fingerprintResolvedAuth({ headers }, salt),
780
+ secrets: [
781
+ match.access,
782
+ match.refresh,
783
+ resolvedAccess,
784
+ auth.apiKey,
785
+ headerValue(auth.headers, "Authorization"),
786
+ authorization,
787
+ ].filter((value): value is string => Boolean(value)),
788
+ model,
789
+ };
790
+ }
791
+
792
+ function resolveGitHubCopilotUsageAuth(
793
+ auth: RequestAuth,
794
+ model: PiModel,
795
+ salt: Uint8Array,
796
+ candidates: readonly unknown[],
797
+ standaloneFallback: boolean,
798
+ ): ResolvedUsageAuth {
799
+ const resolvedAccess = bearerToken(headerValue(auth.headers, "Authorization")) ?? auth.apiKey;
800
+ if (!resolvedAccess) throw new Error("GitHub Copilot OAuth credentials were incomplete.");
801
+ let sawOAuth = false;
802
+ let sawMatchingAccess = false;
803
+ let sawIncompleteMatch = false;
804
+ let sawEnterpriseMatch = false;
805
+ const matches = new Map<string, { refresh: string; storedAccess: string }>();
806
+ for (const candidate of candidates) {
807
+ try {
808
+ const credential = asObject(candidate);
809
+ if (credential?.type !== "oauth") continue;
810
+ sawOAuth = true;
811
+ const storedAccess = typeof credential.access === "string" && credential.access ? credential.access : undefined;
812
+ if (storedAccess !== resolvedAccess) continue;
813
+ sawMatchingAccess = true;
814
+ const enterpriseUrl = credential.enterpriseUrl;
815
+ if (typeof enterpriseUrl === "string" && enterpriseUrl && !isPublicGitHubDomain(enterpriseUrl)) {
816
+ sawEnterpriseMatch = true;
817
+ continue;
818
+ }
819
+ const refresh = typeof credential.refresh === "string" && credential.refresh ? credential.refresh : undefined;
820
+ if (!refresh) {
821
+ sawIncompleteMatch = true;
822
+ continue;
823
+ }
824
+ matches.set(`${storedAccess.length}:${storedAccess}${refresh}`, { refresh, storedAccess });
825
+ } catch {
826
+ // Malformed candidates never authorize a provider request.
827
+ }
828
+ }
829
+ if (sawEnterpriseMatch) {
830
+ throw new Error("GitHub Copilot usage does not yet support GitHub Enterprise accounts.");
831
+ }
832
+ if (sawIncompleteMatch) throw new Error("GitHub Copilot OAuth credentials were incomplete.");
833
+ if (matches.size > 1) {
834
+ throw new Error("Conflicting OAuth credentials match the active GitHub Copilot runtime account.");
835
+ }
836
+ const match = matches.values().next().value;
837
+ if (!match) {
838
+ if (!sawOAuth) {
839
+ throw new Error(
840
+ standaloneFallback
841
+ ? "GitHub Copilot usage requires the OAuth account configured through Pi /login."
842
+ : "GitHub Copilot usage requires an OAuth account configured through Pi /login or a compatible credential source.",
843
+ );
844
+ }
845
+ if (sawMatchingAccess) throw new Error("GitHub Copilot OAuth credentials were incomplete.");
846
+ throw new Error(
847
+ standaloneFallback
848
+ ? "The active GitHub Copilot runtime account does not match Pi's stored OAuth account."
849
+ : "The active GitHub Copilot runtime account does not match any available OAuth account.",
850
+ );
851
+ }
852
+ const { refresh, storedAccess } = match;
853
+ const authorization = `Bearer ${refresh}`;
854
+ const headers = {
855
+ Authorization: authorization,
856
+ "X-GitHub-Api-Version": "2025-05-01",
857
+ };
858
+ return {
859
+ apiKey: refresh,
860
+ headers,
861
+ fingerprint: fingerprintResolvedAuth({ headers }, salt),
862
+ secrets: [refresh, storedAccess, resolvedAccess, authorization],
863
+ model,
864
+ };
865
+ }
866
+
867
+ function authorizationFrom(auth: RequestAuth): string | undefined {
868
+ return headerValue(auth.headers, "Authorization") ?? (auth.apiKey ? `Bearer ${auth.apiKey}` : undefined);
869
+ }
870
+
871
+ function bearerToken(authorization: string | undefined): string | undefined {
872
+ const match = /^Bearer\s+(.+)$/iu.exec(authorization ?? "");
873
+ return match?.[1];
874
+ }
875
+
876
+ function asObject(value: unknown): Record<string, unknown> | undefined {
877
+ if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
878
+ return value as Record<string, unknown>;
879
+ }
880
+
881
+ function isPublicGitHubDomain(value: string): boolean {
882
+ try {
883
+ const url = new URL(value.includes("://") ? value : `https://${value}`);
884
+ return url.hostname.toLowerCase() === "github.com";
885
+ } catch {
886
+ return false;
887
+ }
888
+ }
889
+
890
+ function hasOfficialOrigin(model: PiModel, providerId: string): boolean {
891
+ return hasOfficialUrlOrigin(model.baseUrl, providerId);
892
+ }
893
+
894
+ function hasOfficialUrlOrigin(value: string, providerId: string): boolean {
895
+ try {
896
+ const url = new URL(value);
897
+ if (providerId === "baseten") {
898
+ return ["https://inference.baseten.co", "https://api.baseten.co"].includes(url.origin);
899
+ }
900
+ if (providerId === "openai-codex") return url.origin === "https://chatgpt.com";
901
+ if (providerId === "deepseek") return url.origin === "https://api.deepseek.com";
902
+ if (providerId === "fireworks") return url.origin === "https://api.fireworks.ai";
903
+ if (providerId === "openrouter") return url.origin === "https://openrouter.ai";
904
+ if (providerId === "vercel-ai-gateway") return url.origin === "https://ai-gateway.vercel.sh";
905
+ if (providerId === "opencode-go") return url.origin === "https://opencode.ai";
906
+ if (providerId === "kimi-coding") return url.origin === "https://api.kimi.com";
907
+ if (providerId === "minimax") return url.origin === "https://api.minimax.io";
908
+ if (providerId === "minimax-cn") return url.origin === "https://api.minimaxi.com";
909
+ if (providerId === "moonshotai") return url.origin === "https://api.moonshot.ai";
910
+ if (providerId === "moonshotai-cn") return url.origin === "https://api.moonshot.cn";
911
+ if (providerId === "xai") return url.origin === "https://api.x.ai";
912
+ if (providerId === "zai") return url.origin === "https://api.z.ai";
913
+ if (providerId === "zai-coding-cn") return url.origin === "https://open.bigmodel.cn";
914
+ if (providerId === "github-copilot") {
915
+ return url.protocol === "https:" && /^api\.[a-z0-9-]+\.githubcopilot\.com$/u.test(url.hostname);
916
+ }
917
+ return false;
918
+ } catch {
919
+ return false;
920
+ }
921
+ }
922
+
923
+ function headerValue(headers: Record<string, string | null> | undefined, name: string): string | undefined {
924
+ const entry = Object.entries(headers ?? {}).find(([candidate]) => candidate.toLowerCase() === name.toLowerCase());
925
+ return entry?.[1] ?? undefined;
926
+ }
927
+
928
+ function hasHeader(headers: Record<string, string>, name: string): boolean {
929
+ return Object.keys(headers).some((key) => key.toLowerCase() === name.toLowerCase());
930
+ }
931
+
932
+ function validatedXaiUserId(value: unknown): string {
933
+ if (typeof value !== "string" || !/^[A-Za-z0-9._~-]{1,128}$/u.test(value)) {
934
+ throw new Error("xAI consumer identity returned an unsafe canonical user ID.");
935
+ }
936
+ return value;
937
+ }
938
+
939
+ function basetenBillingUsageUrl(windowAt: number): string {
940
+ const url = new URL(BASETEN_BILLING_USAGE_URL);
941
+ url.searchParams.set(
942
+ "start_date",
943
+ new Date(windowAt - BASETEN_USAGE_WINDOW_DAYS * 24 * 60 * 60 * 1_000).toISOString(),
944
+ );
945
+ url.searchParams.set("end_date", new Date(windowAt).toISOString());
946
+ return url.toString();
947
+ }
948
+
949
+ async function queryMiniMaxUsage(
950
+ providerId: MiniMaxProviderId,
951
+ auth: ResolvedUsageAuth,
952
+ signal: AbortSignal,
953
+ timeoutMs: number,
954
+ guard: UsageRequestGuard | undefined,
955
+ ): Promise<UsageReport> {
956
+ if (!guard) throw new Error("MiniMax usage requires request-boundary revalidation.");
957
+ const apiKey = bearerToken(headerValue(auth.headers, "Authorization")) ?? auth.apiKey;
958
+ if (!apiKey) throw new Error("MiniMax runtime API key was unavailable.");
959
+ const kind = miniMaxUsageKind(apiKey);
960
+ const path = kind === "account-balance" ? "/account/query_balance" : "/v1/token_plan/remains";
961
+ const startedAt = Date.now();
962
+ await guard();
963
+ const payload = (await fetchProviderJson(
964
+ `${MINIMAX_API_ROOTS[providerId]}${path}`,
965
+ auth,
966
+ signal,
967
+ remainingTimeout(timeoutMs, startedAt, "fetching MiniMax usage"),
968
+ "MiniMax usage endpoint",
969
+ { redirect: "error" },
970
+ )) as MiniMaxUsagePayload;
971
+ await guard();
972
+ return normalizeMiniMaxUsagePayload(providerId, kind, payload, Date.now());
973
+ }
974
+
975
+ async function queryMoonshotBalance(
976
+ providerId: "moonshotai" | "moonshotai-cn",
977
+ auth: ResolvedUsageAuth,
978
+ signal: AbortSignal,
979
+ timeoutMs: number,
980
+ guard: UsageRequestGuard | undefined,
981
+ ): Promise<UsageReport> {
982
+ if (!guard) throw new Error("Moonshot AI balance requires request-boundary revalidation.");
983
+ const startedAt = Date.now();
984
+ await guard();
985
+ const payload = (await fetchProviderJson(
986
+ MOONSHOT_BALANCE_URLS[providerId],
987
+ auth,
988
+ signal,
989
+ remainingTimeout(timeoutMs, startedAt, "fetching Moonshot AI balance"),
990
+ "Moonshot AI balance endpoint",
991
+ { redirect: "error" },
992
+ )) as MoonshotBalancePayload;
993
+ await guard();
994
+ return normalizeMoonshotBalancePayload(providerId, payload, Date.now());
995
+ }
996
+
997
+ function remainingTimeout(timeoutMs: number, startedAt: number, description = "fetching xAI consumer usage"): number {
998
+ const remaining = timeoutMs - (Date.now() - startedAt);
999
+ if (remaining <= 0) throw new Error(`Timed out while ${description}.`);
1000
+ return remaining;
1001
+ }
1002
+
1003
+ function zaiOrigin(baseUrl: string | undefined): string {
1004
+ const base = baseUrl?.trim();
1005
+ if (!base) throw new Error("Z.AI model base URL is unavailable.");
1006
+ return new URL(base).origin;
1007
+ }
1008
+
1009
+ function zaiMonitorUrl(baseUrl: string | undefined): string {
1010
+ return `${zaiOrigin(baseUrl)}/api/monitor/usage/quota/limit`;
1011
+ }
1012
+
1013
+ function zaiMonitorAuth(auth: ResolvedUsageAuth): ResolvedUsageAuth {
1014
+ const authorization = headerValue(auth.headers, "Authorization");
1015
+ const token = authorization === undefined ? undefined : (bearerToken(authorization) ?? authorization);
1016
+ if (token === undefined || token === authorization) return auth;
1017
+ return { ...auth, headers: { ...auth.headers, Authorization: token } };
1018
+ }
1019
+
1020
+ async function queryZaiUsage(
1021
+ providerId: "zai" | "zai-coding-cn",
1022
+ providerName: string,
1023
+ auth: ResolvedUsageAuth,
1024
+ signal: AbortSignal,
1025
+ timeoutMs: number,
1026
+ guard: UsageRequestGuard | undefined,
1027
+ ): Promise<UsageReport> {
1028
+ if (!guard) throw new Error("Z.AI usage requires request-boundary revalidation.");
1029
+ const startedAt = Date.now();
1030
+ await guard();
1031
+ const payload = (await fetchProviderJson(
1032
+ zaiMonitorUrl(auth.model.baseUrl),
1033
+ zaiMonitorAuth(auth),
1034
+ signal,
1035
+ remainingTimeout(timeoutMs, startedAt, `fetching ${providerName} quota`),
1036
+ `${providerName} quota endpoint`,
1037
+ { responseError: zaiResponseError },
1038
+ )) as ZaiQuotaPayload;
1039
+ await guard();
1040
+ const planTimeoutMs = timeoutMs - (Date.now() - startedAt);
1041
+ const plan = await fetchZaiPlan(providerName, auth, signal, planTimeoutMs);
1042
+ return normalizeZaiQuotaPayload(providerId, providerName, payload, Date.now(), plan);
1043
+ }
1044
+
1045
+ // The subscription endpoint is undocumented and may not exist on every official origin. It only
1046
+ // contributes the plan name and renewal date, so any non-abort failure is swallowed instead of
1047
+ // blanking the required quota report.
1048
+ async function fetchZaiPlan(
1049
+ providerName: string,
1050
+ auth: ResolvedUsageAuth,
1051
+ signal: AbortSignal,
1052
+ timeoutMs: number,
1053
+ ): Promise<ZaiPlanInfo | undefined> {
1054
+ if (timeoutMs <= 0 || signal.aborted) return undefined;
1055
+ try {
1056
+ const payload = (await fetchProviderJson(
1057
+ `${zaiOrigin(auth.model.baseUrl)}/api/biz/subscription/list`,
1058
+ zaiMonitorAuth(auth),
1059
+ signal,
1060
+ timeoutMs,
1061
+ `${providerName} plan endpoint`,
1062
+ { responseError: zaiResponseError },
1063
+ )) as ZaiSubscriptionPayload;
1064
+ return normalizeZaiSubscriptionPayload(payload);
1065
+ } catch (error) {
1066
+ if (isAbortError(error)) throw error;
1067
+ return undefined;
1068
+ }
1069
+ }
1070
+
1071
+ function isAbortError(error: unknown): boolean {
1072
+ return error instanceof Error && error.name === "AbortError";
1073
+ }