@narumitw/pi-usage 0.59.0 → 0.60.1

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 CHANGED
@@ -1,3 +1,5 @@
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.
1
3
  import { randomBytes } from "node:crypto";
2
4
  import { type ExtensionContext, readStoredCredential } from "@earendil-works/pi-coding-agent";
3
5
  import { errorMessage, fingerprintResolvedAuth, redactUsageError } from "./core.js";
@@ -8,11 +10,7 @@ import {
8
10
  import { normalizeBasetenBillingUsagePayload } from "./providers/baseten.js";
9
11
  import { normalizeCodexBackendPayload } from "./providers/codex.js";
10
12
  import { normalizeDeepSeekBalancePayload } from "./providers/deepseek.js";
11
- import {
12
- isFireworksAccountId,
13
- normalizeFireworksAccountsPayload,
14
- normalizeFireworksBillingSummaryPayload,
15
- } from "./providers/fireworks.js";
13
+ import { createFireworksAdapter } from "./providers/fireworks.js";
16
14
  import { normalizeGitHubCopilotUsagePayload } from "./providers/github-copilot.js";
17
15
  import { normalizeKimiCodingUsagePayload } from "./providers/kimi-coding.js";
18
16
  import {
@@ -25,13 +23,11 @@ import { normalizeOpenCodeZenPayload } from "./providers/opencode-zen.js";
25
23
  import { normalizeOpenRouterKeyPayload } from "./providers/openrouter.js";
26
24
  import { normalizeVercelAIGatewayCreditsPayload } from "./providers/vercel-ai-gateway.js";
27
25
  import { normalizeXaiBillingPayload } from "./providers/xai.js";
28
- import { normalizeZaiQuotaPayload } from "./providers/zai.js";
26
+ import { normalizeZaiQuotaPayload, normalizeZaiSubscriptionPayload } from "./providers/zai.js";
29
27
  import type {
30
28
  BasetenBillingUsagePayload,
31
29
  CodexBackendPayload,
32
30
  DeepSeekBalancePayload,
33
- FireworksAccountsPayload,
34
- FireworksBillingSummaryPayload,
35
31
  GitHubCopilotUsagePayload,
36
32
  KimiCodingUsagePayload,
37
33
  MiniMaxUsagePayload,
@@ -43,19 +39,20 @@ import type {
43
39
  UsageProviderAdapter,
44
40
  UsageQuerySettings,
45
41
  UsageReport,
42
+ UsageRequestGuard,
46
43
  VercelAIGatewayCreditsPayload,
47
44
  XaiBillingPayload,
48
45
  XaiUserPayload,
46
+ ZaiPlanInfo,
49
47
  ZaiQuotaPayload,
48
+ ZaiSubscriptionPayload,
50
49
  } from "./types.js";
50
+ import { resolveUsageTarget } from "./usage-targets.js";
51
51
 
52
52
  const BASETEN_BILLING_USAGE_URL = "https://api.baseten.co/v1/billing/usage_summary";
53
53
  const BASETEN_USAGE_WINDOW_DAYS = 30;
54
54
  const CODEX_USAGE_URL = "https://chatgpt.com/backend-api/wham/usage";
55
55
  const DEEPSEEK_BALANCE_URL = "https://api.deepseek.com/user/balance";
56
- const FIREWORKS_BILLING_SUMMARY_ORIGIN = "https://api.fireworks.ai";
57
- const FIREWORKS_SPEND_WINDOW_DAYS = 30;
58
- const FIREWORKS_MAX_ACCOUNT_PAGES = 5;
59
56
  const GITHUB_COPILOT_USAGE_URL = "https://api.github.com/copilot_internal/user";
60
57
  const OPENROUTER_KEY_URL = "https://openrouter.ai/api/v1/key";
61
58
  const VERCEL_AI_GATEWAY_CREDITS_URL = "https://ai-gateway.vercel.sh/v1/credits";
@@ -82,8 +79,6 @@ const MAX_ERROR_BODY_BYTES = 4 * 1024;
82
79
 
83
80
  export const AUTH_FINGERPRINT_SALT = randomBytes(32);
84
81
 
85
- export type UsageRequestGuard = () => Promise<void>;
86
-
87
82
  export const SUPPORTED_ADAPTERS: readonly UsageProviderAdapter[] = [
88
83
  {
89
84
  id: "baseten",
@@ -199,35 +194,7 @@ export const SUPPORTED_ADAPTERS: readonly UsageProviderAdapter[] = [
199
194
  return normalizeVercelAIGatewayCreditsPayload(payload, Date.now());
200
195
  },
201
196
  },
202
- {
203
- id: "fireworks",
204
- displayName: "Fireworks",
205
- semantics: { kind: "api-key", label: "Fireworks API spend" },
206
- async query(auth, signal, timeoutMs, guard, settings) {
207
- if (!guard) throw new Error("Fireworks API spend requires request-boundary revalidation.");
208
- const startedAt = Date.now();
209
- await guard();
210
- const accountId = await resolveFireworksAccountId(
211
- auth,
212
- signal,
213
- remainingTimeout(timeoutMs, startedAt, "resolving the Fireworks account"),
214
- guard,
215
- settings?.fireworksAccountId,
216
- );
217
- await guard();
218
- const billingWindowAt = Date.now();
219
- const payload = (await fetchProviderJson(
220
- fireworksBillingSummaryUrl(accountId, billingWindowAt),
221
- auth,
222
- signal,
223
- remainingTimeout(timeoutMs, startedAt, "fetching Fireworks rated spend"),
224
- "Fireworks billing summary endpoint",
225
- { redirect: "error" },
226
- )) as FireworksBillingSummaryPayload;
227
- await guard();
228
- return normalizeFireworksBillingSummaryPayload(payload, accountId, Date.now());
229
- },
230
- },
197
+ createFireworksAdapter(fetchProviderJson),
231
198
  {
232
199
  id: "opencode-go",
233
200
  displayName: "OpenCode Go",
@@ -295,35 +262,16 @@ export const SUPPORTED_ADAPTERS: readonly UsageProviderAdapter[] = [
295
262
  id: "zai",
296
263
  displayName: "Z.AI",
297
264
  semantics: { kind: "consumer-subscription", label: "GLM Coding Plan usage" },
298
- async query(auth, signal, timeoutMs) {
299
- const payload = await fetchProviderJson(
300
- zaiMonitorUrl(auth.model.baseUrl),
301
- zaiMonitorAuth(auth),
302
- signal,
303
- timeoutMs,
304
- "Z.AI quota endpoint",
305
- );
306
- return normalizeZaiQuotaPayload("zai", "Z.AI", payload as ZaiQuotaPayload, Date.now());
265
+ async query(auth, signal, timeoutMs, guard) {
266
+ return queryZaiUsage("zai", "Z.AI", auth, signal, timeoutMs, guard);
307
267
  },
308
268
  },
309
269
  {
310
270
  id: "zai-coding-cn",
311
271
  displayName: "Z.AI Coding CN",
312
272
  semantics: { kind: "consumer-subscription", label: "GLM Coding Plan usage" },
313
- async query(auth, signal, timeoutMs) {
314
- const payload = await fetchProviderJson(
315
- zaiMonitorUrl(auth.model.baseUrl),
316
- zaiMonitorAuth(auth),
317
- signal,
318
- timeoutMs,
319
- "Z.AI Coding CN quota endpoint",
320
- );
321
- return normalizeZaiQuotaPayload(
322
- "zai-coding-cn",
323
- "Z.AI Coding CN",
324
- payload as ZaiQuotaPayload,
325
- Date.now(),
326
- );
273
+ async query(auth, signal, timeoutMs, guard) {
274
+ return queryZaiUsage("zai-coding-cn", "Z.AI Coding CN", auth, signal, timeoutMs, guard);
327
275
  },
328
276
  },
329
277
  ];
@@ -419,6 +367,12 @@ export async function resolveUsageAuth(
419
367
  if (!model) return undefined;
420
368
  // SAFETY: Pi exposes the required auth methods at runtime, and checks below narrow them before use.
421
369
  const registry = ctx.modelRegistry as unknown as UsageAuthRegistry;
370
+ const provider = registry.getProvider?.(adapter.id);
371
+ if (provider?.baseUrl && !hasOfficialUrlOrigin(provider.baseUrl, adapter.id)) {
372
+ throw new Error(
373
+ `${adapter.displayName} usage cannot send an overridden provider credential to the official usage endpoint.`,
374
+ );
375
+ }
422
376
  let modelAuth: RequestAuth | undefined;
423
377
  const currentModel = ctx.model?.provider === adapter.id ? ctx.model : undefined;
424
378
  const resolveCurrentModelAuth = async (): Promise<RequestAuth | undefined> => {
@@ -446,8 +400,46 @@ export async function resolveUsageAuth(
446
400
  // Providers with credential-change retries read selected-model auth last so a rotation during
447
401
  // provider-origin validation cannot leave the earlier credential queued for the usage request.
448
402
  if (resolveSelectedAuthLast) modelAuth = await resolveCurrentModelAuth();
403
+ if (modelAuth?.baseUrl && !hasOfficialUrlOrigin(modelAuth.baseUrl, adapter.id)) {
404
+ throw new Error(
405
+ `${adapter.displayName} usage cannot send model-resolved proxy credentials to the official usage endpoint.`,
406
+ );
407
+ }
449
408
  const auth = modelAuth ?? providerResult?.auth;
450
409
  if (!auth) return undefined;
410
+ const finalize = (resolved: ResolvedUsageAuth): ResolvedUsageAuth => {
411
+ const preservedAuth = { ...(providerResult?.auth ?? auth) };
412
+ const env = providerResult?.env ?? modelAuth?.env;
413
+ const source = providerResult?.source;
414
+ const effectiveBaseUrl =
415
+ modelAuth?.baseUrl ?? providerResult?.auth.baseUrl ?? provider?.baseUrl ?? model.baseUrl;
416
+ const redactionInputs = [
417
+ preservedAuth.apiKey,
418
+ ...Object.values(preservedAuth.headers ?? {}),
419
+ ...Object.values(env ?? {}),
420
+ modelAuth?.apiKey,
421
+ ...Object.values(modelAuth?.headers ?? {}),
422
+ ].filter((value): value is string => typeof value === "string" && value.length > 0);
423
+ return {
424
+ ...resolved,
425
+ auth: preservedAuth,
426
+ ...(env ? { env: { ...env } } : {}),
427
+ ...(source ? { source } : {}),
428
+ effectiveBaseUrl,
429
+ secrets: [...new Set([...resolved.secrets, ...redactionInputs])],
430
+ fingerprint: fingerprintResolvedAuth(
431
+ {
432
+ apiKey: resolved.apiKey,
433
+ headers: resolved.headers,
434
+ baseUrl: effectiveBaseUrl,
435
+ env,
436
+ source,
437
+ providerAuth: preservedAuth,
438
+ },
439
+ salt,
440
+ ),
441
+ };
442
+ };
451
443
  if (adapter.id === "github-copilot") {
452
444
  const offered = candidateReader
453
445
  ? candidateReader(ctx, adapter.id)
@@ -455,12 +447,14 @@ export async function resolveUsageAuth(
455
447
  if (!offered.ok) {
456
448
  throw new Error("GitHub Copilot OAuth credential discovery failed closed.");
457
449
  }
458
- return resolveGitHubCopilotUsageAuth(
459
- auth,
460
- model,
461
- salt,
462
- offered.candidates,
463
- offered.offeredCount === 0,
450
+ return finalize(
451
+ resolveGitHubCopilotUsageAuth(
452
+ auth,
453
+ model,
454
+ salt,
455
+ offered.candidates,
456
+ offered.offeredCount === 0,
457
+ ),
464
458
  );
465
459
  }
466
460
  if (adapter.id === "xai") {
@@ -468,7 +462,7 @@ export async function resolveUsageAuth(
468
462
  ? candidateReader(ctx, adapter.id)
469
463
  : fallbackOAuthCredentialCandidates(adapter.id, credentialReader);
470
464
  if (!offered.ok) throw new Error("xAI OAuth credential discovery failed closed.");
471
- return resolveXaiUsageAuth(auth, model, salt, offered.candidates);
465
+ return finalize(resolveXaiUsageAuth(auth, model, salt, offered.candidates));
472
466
  }
473
467
  if (adapter.id === "deepseek") {
474
468
  const resolvedAuthorization = authorizationFrom(auth);
@@ -476,10 +470,10 @@ export async function resolveUsageAuth(
476
470
  if (!access) throw new Error("DeepSeek API balance requires Bearer authentication.");
477
471
  const authorization = `Bearer ${access}`;
478
472
  const headers = { Authorization: authorization };
479
- return {
473
+ return finalize({
480
474
  apiKey: access,
481
475
  headers,
482
- fingerprint: fingerprintResolvedAuth({ headers }, salt),
476
+ fingerprint: "",
483
477
  secrets: [
484
478
  access,
485
479
  auth.apiKey,
@@ -488,7 +482,7 @@ export async function resolveUsageAuth(
488
482
  authorization,
489
483
  ].filter((value): value is string => Boolean(value)),
490
484
  model,
491
- };
485
+ });
492
486
  }
493
487
  const authorization = authorizationFrom(auth);
494
488
  if (!authorization) return undefined;
@@ -496,13 +490,13 @@ export async function resolveUsageAuth(
496
490
  const secrets = [auth.apiKey, headerValue(auth.headers, "Authorization"), authorization].filter(
497
491
  (value): value is string => Boolean(value),
498
492
  );
499
- return {
493
+ return finalize({
500
494
  apiKey: auth.apiKey,
501
495
  headers,
502
- fingerprint: fingerprintResolvedAuth({ headers }, salt),
496
+ fingerprint: "",
503
497
  secrets,
504
498
  model,
505
- };
499
+ });
506
500
  }
507
501
 
508
502
  export async function queryProviderUsage(
@@ -511,10 +505,46 @@ export async function queryProviderUsage(
511
505
  signal: AbortSignal,
512
506
  timeoutMs: number,
513
507
  guard?: UsageRequestGuard,
514
- settings?: Readonly<UsageQuerySettings>,
508
+ targetOrSettings?: string | Readonly<UsageQuerySettings>,
515
509
  ): Promise<UsageReport> {
510
+ const startedAt = Date.now();
511
+ let targetId =
512
+ typeof targetOrSettings === "string"
513
+ ? targetOrSettings
514
+ : adapter.id === "fireworks"
515
+ ? targetOrSettings?.fireworksAccountId
516
+ : undefined;
517
+ let resolvedLegacyFireworksTarget = false;
516
518
  try {
517
- return await adapter.query(auth, signal, timeoutMs, guard, settings);
519
+ if (
520
+ adapter.id === "fireworks" &&
521
+ typeof targetOrSettings !== "string" &&
522
+ adapter.targets &&
523
+ guard
524
+ ) {
525
+ const target = await resolveUsageTarget(
526
+ adapter,
527
+ auth,
528
+ targetId,
529
+ signal,
530
+ remainingTimeout(timeoutMs, startedAt, "resolving the Fireworks account"),
531
+ guard,
532
+ );
533
+ if (target.kind === "selection-required") {
534
+ throw new Error("Fireworks account selection is required.");
535
+ }
536
+ targetId = target.targetId;
537
+ resolvedLegacyFireworksTarget = true;
538
+ }
539
+ return await adapter.query(
540
+ auth,
541
+ signal,
542
+ resolvedLegacyFireworksTarget
543
+ ? remainingTimeout(timeoutMs, startedAt, `querying ${adapter.displayName} usage`)
544
+ : timeoutMs,
545
+ guard,
546
+ targetId,
547
+ );
518
548
  } catch (error) {
519
549
  if (isStaleExtensionContextError(error) || isAbortError(error)) throw error;
520
550
  throw new Error(redactUsageError(errorMessage(error), auth.secrets));
@@ -713,17 +743,22 @@ async function readBoundedResponse(
713
743
  type RequestAuth = {
714
744
  apiKey?: string;
715
745
  headers?: Record<string, string | null>;
746
+ baseUrl?: string;
747
+ env?: Record<string, string>;
716
748
  };
717
749
 
718
750
  type StoredCredentialReader = (providerId: string) => unknown;
719
751
 
720
752
  type UsageAuthRegistry = {
753
+ getProvider?(providerId: string): { baseUrl?: string } | undefined;
721
754
  getApiKeyAndHeaders?(
722
755
  model: PiModel,
723
756
  ): Promise<({ ok: true } & RequestAuth) | { ok: false; error: string }>;
724
757
  getProviderAuth?(providerId: string): Promise<
725
758
  | {
726
- auth: RequestAuth & { baseUrl?: string };
759
+ auth: RequestAuth;
760
+ env?: Record<string, string>;
761
+ source?: string;
727
762
  }
728
763
  | undefined
729
764
  >;
@@ -919,7 +954,7 @@ function hasOfficialUrlOrigin(value: string, providerId: string): boolean {
919
954
  }
920
955
  if (providerId === "openai-codex") return url.origin === "https://chatgpt.com";
921
956
  if (providerId === "deepseek") return url.origin === "https://api.deepseek.com";
922
- if (providerId === "fireworks") return url.origin === FIREWORKS_BILLING_SUMMARY_ORIGIN;
957
+ if (providerId === "fireworks") return url.origin === "https://api.fireworks.ai";
923
958
  if (providerId === "openrouter") return url.origin === "https://openrouter.ai";
924
959
  if (providerId === "vercel-ai-gateway") return url.origin === "https://ai-gateway.vercel.sh";
925
960
  if (providerId === "opencode-go") return url.origin === "https://opencode.ai";
@@ -1031,102 +1066,14 @@ function remainingTimeout(
1031
1066
  return remaining;
1032
1067
  }
1033
1068
 
1034
- // Fireworks requires an account slug for its billing endpoints; discover it through the
1035
- // documented account listing, requiring an explicit slug when a key can see several accounts.
1036
- async function resolveFireworksAccountId(
1037
- auth: ResolvedUsageAuth,
1038
- signal: AbortSignal,
1039
- timeoutMs: number,
1040
- guard: () => Promise<void>,
1041
- configuredAccountId: string | undefined,
1042
- ): Promise<string> {
1043
- if (configuredAccountId !== undefined && !isFireworksAccountId(configuredAccountId)) {
1044
- throw new Error("The Fireworks account setting was not a safe account slug.");
1045
- }
1046
- const startedAt = Date.now();
1047
- const accounts: string[] = [];
1048
- let pageToken: string | undefined;
1049
- for (let page = 0; page < FIREWORKS_MAX_ACCOUNT_PAGES; page += 1) {
1050
- await guard();
1051
- const payload = (await fetchProviderJson(
1052
- fireworksAccountsUrl(pageToken),
1053
- auth,
1054
- signal,
1055
- remainingTimeout(timeoutMs, startedAt, "fetching Fireworks accounts"),
1056
- "Fireworks accounts endpoint",
1057
- { redirect: "error" },
1058
- )) as FireworksAccountsPayload;
1059
- for (const accountId of normalizeFireworksAccountsPayload(
1060
- payload as FireworksAccountsPayload,
1061
- )) {
1062
- if (accounts.includes(accountId)) {
1063
- throw new Error(`Fireworks accounts listing repeated ${accountId}.`);
1064
- }
1065
- accounts.push(accountId);
1066
- if (configuredAccountId === accountId) return accountId;
1067
- }
1068
- pageToken = fireworksNextPageToken(payload.nextPageToken);
1069
- if (!pageToken) break;
1070
- }
1071
- if (pageToken) {
1072
- throw new Error(
1073
- configuredAccountId
1074
- ? `The configured Fireworks account was not found within the first ${FIREWORKS_MAX_ACCOUNT_PAGES} listing pages.`
1075
- : `Fireworks account listing exceeded ${FIREWORKS_MAX_ACCOUNT_PAGES} pages; set fireworksAccountId in pi-usage.json to an account returned in those pages.`,
1076
- );
1077
- }
1078
- if (accounts.length === 0) {
1079
- throw new Error("Fireworks account discovery returned no accounts for this API key.");
1080
- }
1081
- if (configuredAccountId) {
1082
- throw new Error(
1083
- "The configured Fireworks account does not match an account visible to this API key.",
1084
- );
1085
- }
1086
- if (accounts.length === 1) return accounts[0] as string;
1087
- const preview = accounts.slice(0, 8).join(", ");
1088
- const suffix = accounts.length > 8 ? ` …and ${accounts.length - 8} more` : "";
1089
- throw new Error(
1090
- `The Fireworks key can see ${accounts.length} accounts (${preview}${suffix}); set fireworksAccountId in pi-usage.json to one of them.`,
1091
- );
1092
- }
1093
-
1094
- function fireworksAccountsUrl(pageToken: string | undefined): string {
1095
- const url = new URL("/v1/accounts", FIREWORKS_BILLING_SUMMARY_ORIGIN);
1096
- url.searchParams.set("pageSize", "200");
1097
- if (pageToken !== undefined) url.searchParams.set("pageToken", pageToken);
1098
- return url.toString();
1099
- }
1100
-
1101
- function fireworksNextPageToken(value: unknown): string | undefined {
1102
- if (value === undefined || value === null) return undefined;
1103
- if (typeof value !== "string" || !value || value.length > 512) {
1104
- throw new Error("Fireworks accounts listing returned an invalid page token.");
1105
- }
1106
- return value;
1107
- }
1108
-
1109
- function fireworksBillingSummaryUrl(accountId: string, startedAt: number): string {
1110
- const dayMs = 24 * 60 * 60 * 1000;
1111
- const dayFloor = (time: number) => `${new Date(time).toISOString().slice(0, 10)}T00:00:00Z`;
1112
- const url = new URL(
1113
- `/v1/accounts/${accountId}/billing/summary`,
1114
- FIREWORKS_BILLING_SUMMARY_ORIGIN,
1115
- );
1116
- // The endpoint aggregates by UTC date; endTime is exclusive, so the window includes today
1117
- // plus the preceding 29 dates.
1118
- url.searchParams.set(
1119
- "startTime",
1120
- dayFloor(startedAt - (FIREWORKS_SPEND_WINDOW_DAYS - 1) * dayMs),
1121
- );
1122
- url.searchParams.set("endTime", dayFloor(startedAt + dayMs));
1123
- return url.toString();
1069
+ function zaiOrigin(baseUrl: string | undefined): string {
1070
+ const base = baseUrl?.trim();
1071
+ if (!base) throw new Error("Z.AI model base URL is unavailable.");
1072
+ return new URL(base).origin;
1124
1073
  }
1125
1074
 
1126
1075
  function zaiMonitorUrl(baseUrl: string | undefined): string {
1127
- const base = baseUrl?.trim();
1128
- if (!base) throw new Error("Z.AI model base URL is unavailable.");
1129
- return `${new URL(base).origin}/api/monitor/usage/quota/limit`;
1076
+ return `${zaiOrigin(baseUrl)}/api/monitor/usage/quota/limit`;
1130
1077
  }
1131
1078
 
1132
1079
  function zaiMonitorAuth(auth: ResolvedUsageAuth): ResolvedUsageAuth {
@@ -1137,6 +1084,55 @@ function zaiMonitorAuth(auth: ResolvedUsageAuth): ResolvedUsageAuth {
1137
1084
  return { ...auth, headers: { ...auth.headers, Authorization: token } };
1138
1085
  }
1139
1086
 
1087
+ async function queryZaiUsage(
1088
+ providerId: "zai" | "zai-coding-cn",
1089
+ providerName: string,
1090
+ auth: ResolvedUsageAuth,
1091
+ signal: AbortSignal,
1092
+ timeoutMs: number,
1093
+ guard: UsageRequestGuard | undefined,
1094
+ ): Promise<UsageReport> {
1095
+ if (!guard) throw new Error("Z.AI usage requires request-boundary revalidation.");
1096
+ const startedAt = Date.now();
1097
+ await guard();
1098
+ const payload = (await fetchProviderJson(
1099
+ zaiMonitorUrl(auth.model.baseUrl),
1100
+ zaiMonitorAuth(auth),
1101
+ signal,
1102
+ remainingTimeout(timeoutMs, startedAt, `fetching ${providerName} quota`),
1103
+ `${providerName} quota endpoint`,
1104
+ )) as ZaiQuotaPayload;
1105
+ await guard();
1106
+ const planTimeoutMs = timeoutMs - (Date.now() - startedAt);
1107
+ const plan = await fetchZaiPlan(providerName, auth, signal, planTimeoutMs);
1108
+ return normalizeZaiQuotaPayload(providerId, providerName, payload, Date.now(), plan);
1109
+ }
1110
+
1111
+ // The subscription endpoint is undocumented and may not exist on every official origin. It only
1112
+ // contributes the plan name and renewal date, so any non-abort failure is swallowed instead of
1113
+ // blanking the required quota report.
1114
+ async function fetchZaiPlan(
1115
+ providerName: string,
1116
+ auth: ResolvedUsageAuth,
1117
+ signal: AbortSignal,
1118
+ timeoutMs: number,
1119
+ ): Promise<ZaiPlanInfo | undefined> {
1120
+ if (timeoutMs <= 0 || signal.aborted) return undefined;
1121
+ try {
1122
+ const payload = (await fetchProviderJson(
1123
+ `${zaiOrigin(auth.model.baseUrl)}/api/biz/subscription/list`,
1124
+ zaiMonitorAuth(auth),
1125
+ signal,
1126
+ timeoutMs,
1127
+ `${providerName} plan endpoint`,
1128
+ )) as ZaiSubscriptionPayload;
1129
+ return normalizeZaiSubscriptionPayload(payload);
1130
+ } catch (error) {
1131
+ if (isAbortError(error)) throw error;
1132
+ return undefined;
1133
+ }
1134
+ }
1135
+
1140
1136
  function isAbortError(error: unknown): boolean {
1141
1137
  return error instanceof Error && error.name === "AbortError";
1142
1138
  }