@narumitw/pi-usage 0.58.0 → 0.59.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,43 @@
1
+ import type { UsageMetric, UsageReport, VercelAIGatewayCreditsPayload } from "../types.js";
2
+
3
+ const DECIMAL_AMOUNT = /^(?:0|[1-9]\d*)(?:\.\d+)?$/u;
4
+
5
+ export function normalizeVercelAIGatewayCreditsPayload(
6
+ payload: VercelAIGatewayCreditsPayload,
7
+ capturedAt: number,
8
+ ): UsageReport {
9
+ const balance = decimalAmount(payload.balance, "balance");
10
+ const totalUsed = decimalAmount(payload.total_used, "total used");
11
+ const metrics: UsageMetric[] = [
12
+ {
13
+ id: "credit-balance",
14
+ label: "Credit balance",
15
+ value: balance,
16
+ unit: "currency",
17
+ currency: "USD",
18
+ },
19
+ {
20
+ id: "lifetime-spend",
21
+ label: "Lifetime spend",
22
+ value: totalUsed,
23
+ unit: "currency",
24
+ currency: "USD",
25
+ },
26
+ ];
27
+ return {
28
+ providerId: "vercel-ai-gateway",
29
+ providerName: "Vercel AI Gateway",
30
+ capturedAt,
31
+ source: "vercel-ai-gateway-credits",
32
+ semantics: { kind: "api-key", label: "AI Gateway credits and lifetime spend" },
33
+ buckets: [],
34
+ metrics,
35
+ };
36
+ }
37
+
38
+ function decimalAmount(value: unknown, label: string): string {
39
+ if (typeof value !== "string" || value.length > 64 || !DECIMAL_AMOUNT.test(value)) {
40
+ throw new Error(`Vercel AI Gateway ${label} was not a valid nonnegative amount.`);
41
+ }
42
+ return value;
43
+ }
package/src/query.ts CHANGED
@@ -5,6 +5,7 @@ import {
5
5
  fallbackOAuthCredentialCandidates,
6
6
  type OAuthCredentialCandidateReader,
7
7
  } from "./oauth-credential-source.js";
8
+ import { normalizeBasetenBillingUsagePayload } from "./providers/baseten.js";
8
9
  import { normalizeCodexBackendPayload } from "./providers/codex.js";
9
10
  import { normalizeDeepSeekBalancePayload } from "./providers/deepseek.js";
10
11
  import {
@@ -14,17 +15,27 @@ import {
14
15
  } from "./providers/fireworks.js";
15
16
  import { normalizeGitHubCopilotUsagePayload } from "./providers/github-copilot.js";
16
17
  import { normalizeKimiCodingUsagePayload } from "./providers/kimi-coding.js";
18
+ import {
19
+ type MiniMaxProviderId,
20
+ miniMaxUsageKind,
21
+ normalizeMiniMaxUsagePayload,
22
+ } from "./providers/minimax.js";
23
+ import { normalizeMoonshotBalancePayload } from "./providers/moonshot.js";
17
24
  import { normalizeOpenCodeZenPayload } from "./providers/opencode-zen.js";
18
25
  import { normalizeOpenRouterKeyPayload } from "./providers/openrouter.js";
26
+ import { normalizeVercelAIGatewayCreditsPayload } from "./providers/vercel-ai-gateway.js";
19
27
  import { normalizeXaiBillingPayload } from "./providers/xai.js";
20
28
  import { normalizeZaiQuotaPayload } from "./providers/zai.js";
21
29
  import type {
30
+ BasetenBillingUsagePayload,
22
31
  CodexBackendPayload,
23
32
  DeepSeekBalancePayload,
24
33
  FireworksAccountsPayload,
25
34
  FireworksBillingSummaryPayload,
26
35
  GitHubCopilotUsagePayload,
27
36
  KimiCodingUsagePayload,
37
+ MiniMaxUsagePayload,
38
+ MoonshotBalancePayload,
28
39
  OpenCodeZenPayload,
29
40
  OpenRouterKeyPayload,
30
41
  PiModel,
@@ -32,11 +43,14 @@ import type {
32
43
  UsageProviderAdapter,
33
44
  UsageQuerySettings,
34
45
  UsageReport,
46
+ VercelAIGatewayCreditsPayload,
35
47
  XaiBillingPayload,
36
48
  XaiUserPayload,
37
49
  ZaiQuotaPayload,
38
50
  } from "./types.js";
39
51
 
52
+ const BASETEN_BILLING_USAGE_URL = "https://api.baseten.co/v1/billing/usage_summary";
53
+ const BASETEN_USAGE_WINDOW_DAYS = 30;
40
54
  const CODEX_USAGE_URL = "https://chatgpt.com/backend-api/wham/usage";
41
55
  const DEEPSEEK_BALANCE_URL = "https://api.deepseek.com/user/balance";
42
56
  const FIREWORKS_BILLING_SUMMARY_ORIGIN = "https://api.fireworks.ai";
@@ -44,8 +58,18 @@ const FIREWORKS_SPEND_WINDOW_DAYS = 30;
44
58
  const FIREWORKS_MAX_ACCOUNT_PAGES = 5;
45
59
  const GITHUB_COPILOT_USAGE_URL = "https://api.github.com/copilot_internal/user";
46
60
  const OPENROUTER_KEY_URL = "https://openrouter.ai/api/v1/key";
61
+ const VERCEL_AI_GATEWAY_CREDITS_URL = "https://ai-gateway.vercel.sh/v1/credits";
47
62
  const OPENCODE_GO_USAGE_URL = "https://opencode.ai/zen/go/v1/usage";
48
63
  const KIMI_CODING_USAGE_URL = "https://api.kimi.com/coding/v1/usages";
64
+ const MINIMAX_API_ROOTS = Object.freeze({
65
+ minimax: "https://api.minimax.io",
66
+ "minimax-cn": "https://api.minimaxi.com",
67
+ });
68
+ const MOONSHOT_BALANCE_URLS = Object.freeze({
69
+ moonshotai: "https://api.moonshot.ai/v1/users/me/balance",
70
+ "moonshotai-cn": "https://api.moonshot.cn/v1/users/me/balance",
71
+ });
72
+ const SHARED_MOONSHOT_ENV_VAR = "MOONSHOT_API_KEY";
49
73
  const XAI_USER_URL = "https://cli-chat-proxy.grok.com/v1/user?include=subscription";
50
74
  const XAI_BILLING_URL = "https://cli-chat-proxy.grok.com/v1/billing?format=credits";
51
75
  const XAI_CLIENT_HEADERS = Object.freeze({
@@ -61,6 +85,27 @@ export const AUTH_FINGERPRINT_SALT = randomBytes(32);
61
85
  export type UsageRequestGuard = () => Promise<void>;
62
86
 
63
87
  export const SUPPORTED_ADAPTERS: readonly UsageProviderAdapter[] = [
88
+ {
89
+ id: "baseten",
90
+ displayName: "Baseten",
91
+ semantics: { kind: "api-key", label: "Organization Model APIs spend" },
92
+ async query(auth, signal, timeoutMs, guard) {
93
+ if (!guard) throw new Error("Baseten billing usage requires request-boundary revalidation.");
94
+ const startedAt = Date.now();
95
+ await guard();
96
+ const windowAt = Date.now();
97
+ const payload = (await fetchProviderJson(
98
+ basetenBillingUsageUrl(windowAt),
99
+ auth,
100
+ signal,
101
+ remainingTimeout(timeoutMs, startedAt, "fetching Baseten billing usage"),
102
+ "Baseten billing usage endpoint",
103
+ { redirect: "error" },
104
+ )) as BasetenBillingUsagePayload;
105
+ await guard();
106
+ return normalizeBasetenBillingUsagePayload(payload, Date.now());
107
+ },
108
+ },
64
109
  {
65
110
  id: "openai-codex",
66
111
  displayName: "OpenAI Codex",
@@ -133,6 +178,27 @@ export const SUPPORTED_ADAPTERS: readonly UsageProviderAdapter[] = [
133
178
  return normalizeOpenRouterKeyPayload(payload as OpenRouterKeyPayload, Date.now());
134
179
  },
135
180
  },
181
+ {
182
+ id: "vercel-ai-gateway",
183
+ displayName: "Vercel AI Gateway",
184
+ semantics: { kind: "api-key", label: "AI Gateway credits and lifetime spend" },
185
+ async query(auth, signal, timeoutMs, guard) {
186
+ if (!guard)
187
+ throw new Error("Vercel AI Gateway usage requires request-boundary revalidation.");
188
+ const startedAt = Date.now();
189
+ await guard();
190
+ const payload = (await fetchProviderJson(
191
+ VERCEL_AI_GATEWAY_CREDITS_URL,
192
+ auth,
193
+ signal,
194
+ remainingTimeout(timeoutMs, startedAt, "fetching Vercel AI Gateway credits"),
195
+ "Vercel AI Gateway credits endpoint",
196
+ { redirect: "error" },
197
+ )) as VercelAIGatewayCreditsPayload;
198
+ await guard();
199
+ return normalizeVercelAIGatewayCreditsPayload(payload, Date.now());
200
+ },
201
+ },
136
202
  {
137
203
  id: "fireworks",
138
204
  displayName: "Fireworks",
@@ -193,6 +259,38 @@ export const SUPPORTED_ADAPTERS: readonly UsageProviderAdapter[] = [
193
259
  return normalizeKimiCodingUsagePayload(payload as KimiCodingUsagePayload, Date.now());
194
260
  },
195
261
  },
262
+ {
263
+ id: "minimax",
264
+ displayName: "MiniMax",
265
+ semantics: { kind: "consumer-subscription", label: "MiniMax usage" },
266
+ async query(auth, signal, timeoutMs, guard) {
267
+ return queryMiniMaxUsage("minimax", auth, signal, timeoutMs, guard);
268
+ },
269
+ },
270
+ {
271
+ id: "minimax-cn",
272
+ displayName: "MiniMax CN",
273
+ semantics: { kind: "consumer-subscription", label: "MiniMax usage" },
274
+ async query(auth, signal, timeoutMs, guard) {
275
+ return queryMiniMaxUsage("minimax-cn", auth, signal, timeoutMs, guard);
276
+ },
277
+ },
278
+ {
279
+ id: "moonshotai",
280
+ displayName: "Moonshot AI",
281
+ semantics: { kind: "api-key", label: "Moonshot API account balance" },
282
+ async query(auth, signal, timeoutMs, guard) {
283
+ return queryMoonshotBalance("moonshotai", auth, signal, timeoutMs, guard);
284
+ },
285
+ },
286
+ {
287
+ id: "moonshotai-cn",
288
+ displayName: "Moonshot AI CN",
289
+ semantics: { kind: "api-key", label: "Moonshot API account balance" },
290
+ async query(auth, signal, timeoutMs, guard) {
291
+ return queryMoonshotBalance("moonshotai-cn", auth, signal, timeoutMs, guard);
292
+ },
293
+ },
196
294
  {
197
295
  id: "zai",
198
296
  displayName: "Z.AI",
@@ -329,11 +427,14 @@ export async function resolveUsageAuth(
329
427
  if (!result.ok) throw new Error(redactUsageError(result.error));
330
428
  return authorizationFrom(result) ? result : undefined;
331
429
  };
332
- if (adapter.id !== "deepseek") modelAuth = await resolveCurrentModelAuth();
430
+ const resolveSelectedAuthLast = ["deepseek", "minimax", "minimax-cn"].includes(adapter.id);
431
+ if (!resolveSelectedAuthLast) modelAuth = await resolveCurrentModelAuth();
333
432
  if (typeof registry.getProviderAuth !== "function") {
334
433
  throw new Error("pi-usage requires Pi 0.81.0 or newer to validate resolved provider auth.");
335
434
  }
435
+ if (!moonshotProviderAuthIsAllowed(ctx, adapter.id)) return undefined;
336
436
  const providerResult = await registry.getProviderAuth(adapter.id);
437
+ if (!moonshotProviderAuthIsAllowed(ctx, adapter.id)) return undefined;
337
438
  if (
338
439
  providerResult?.auth.baseUrl &&
339
440
  !hasOfficialUrlOrigin(providerResult.auth.baseUrl, adapter.id)
@@ -342,9 +443,9 @@ export async function resolveUsageAuth(
342
443
  `${adapter.displayName} usage cannot send a proxy-resolved credential to the official usage endpoint.`,
343
444
  );
344
445
  }
345
- // DeepSeek reads selected-model auth last so a rotation during provider-origin validation
346
- // cannot leave the earlier credential queued for the balance request.
347
- if (adapter.id === "deepseek") modelAuth = await resolveCurrentModelAuth();
446
+ // Providers with credential-change retries read selected-model auth last so a rotation during
447
+ // provider-origin validation cannot leave the earlier credential queued for the usage request.
448
+ if (resolveSelectedAuthLast) modelAuth = await resolveCurrentModelAuth();
348
449
  const auth = modelAuth ?? providerResult?.auth;
349
450
  if (!auth) return undefined;
350
451
  if (adapter.id === "github-copilot") {
@@ -422,12 +523,53 @@ export async function queryProviderUsage(
422
523
 
423
524
  export function providerIsConfigured(ctx: ExtensionContext, providerId: string): boolean {
424
525
  try {
425
- return ctx.modelRegistry.getProviderAuthStatus(providerId).configured;
526
+ const status = ctx.modelRegistry.getProviderAuthStatus(providerId);
527
+ return (
528
+ status.configured &&
529
+ moonshotProviderAuthSourceIsAllowed(ctx, providerId, status.source, status.label)
530
+ );
531
+ } catch {
532
+ return (
533
+ !isMoonshotSiblingProvider(ctx, providerId) && candidateModels(ctx, providerId).length > 0
534
+ );
535
+ }
536
+ }
537
+
538
+ function moonshotProviderAuthIsAllowed(ctx: ExtensionContext, providerId: string): boolean {
539
+ if (!isMoonshotSiblingProvider(ctx, providerId)) return true;
540
+ try {
541
+ const status = ctx.modelRegistry.getProviderAuthStatus(providerId);
542
+ return moonshotProviderAuthSourceIsAllowed(ctx, providerId, status.source, status.label);
426
543
  } catch {
427
- return candidateModels(ctx, providerId).length > 0;
544
+ return false;
428
545
  }
429
546
  }
430
547
 
548
+ function moonshotProviderAuthSourceIsAllowed(
549
+ ctx: ExtensionContext,
550
+ providerId: string,
551
+ source: string | undefined,
552
+ label: string | undefined,
553
+ ): boolean {
554
+ if (!isMoonshotSiblingProvider(ctx, providerId)) return true;
555
+ if (source === undefined) return false;
556
+ if (source !== "environment") return true;
557
+ return (
558
+ label !== undefined &&
559
+ !label
560
+ .split(",")
561
+ .map((name) => name.trim())
562
+ .includes(SHARED_MOONSHOT_ENV_VAR)
563
+ );
564
+ }
565
+
566
+ function isMoonshotSiblingProvider(ctx: ExtensionContext, providerId: string): boolean {
567
+ return (
568
+ (providerId === "moonshotai" || providerId === "moonshotai-cn") &&
569
+ ctx.model?.provider !== providerId
570
+ );
571
+ }
572
+
431
573
  function candidateModels(ctx: ExtensionContext, providerId: string): PiModel[] {
432
574
  const candidates: PiModel[] = [];
433
575
  const seen = new Set<string>();
@@ -772,12 +914,20 @@ function hasOfficialOrigin(model: PiModel, providerId: string): boolean {
772
914
  function hasOfficialUrlOrigin(value: string, providerId: string): boolean {
773
915
  try {
774
916
  const url = new URL(value);
917
+ if (providerId === "baseten") {
918
+ return ["https://inference.baseten.co", "https://api.baseten.co"].includes(url.origin);
919
+ }
775
920
  if (providerId === "openai-codex") return url.origin === "https://chatgpt.com";
776
921
  if (providerId === "deepseek") return url.origin === "https://api.deepseek.com";
777
922
  if (providerId === "fireworks") return url.origin === FIREWORKS_BILLING_SUMMARY_ORIGIN;
778
923
  if (providerId === "openrouter") return url.origin === "https://openrouter.ai";
924
+ if (providerId === "vercel-ai-gateway") return url.origin === "https://ai-gateway.vercel.sh";
779
925
  if (providerId === "opencode-go") return url.origin === "https://opencode.ai";
780
926
  if (providerId === "kimi-coding") return url.origin === "https://api.kimi.com";
927
+ if (providerId === "minimax") return url.origin === "https://api.minimax.io";
928
+ if (providerId === "minimax-cn") return url.origin === "https://api.minimaxi.com";
929
+ if (providerId === "moonshotai") return url.origin === "https://api.moonshot.ai";
930
+ if (providerId === "moonshotai-cn") return url.origin === "https://api.moonshot.cn";
781
931
  if (providerId === "xai") return url.origin === "https://api.x.ai";
782
932
  if (providerId === "zai") return url.origin === "https://api.z.ai";
783
933
  if (providerId === "zai-coding-cn") return url.origin === "https://open.bigmodel.cn";
@@ -813,6 +963,64 @@ function validatedXaiUserId(value: unknown): string {
813
963
  return value;
814
964
  }
815
965
 
966
+ function basetenBillingUsageUrl(windowAt: number): string {
967
+ const url = new URL(BASETEN_BILLING_USAGE_URL);
968
+ url.searchParams.set(
969
+ "start_date",
970
+ new Date(windowAt - BASETEN_USAGE_WINDOW_DAYS * 24 * 60 * 60 * 1_000).toISOString(),
971
+ );
972
+ url.searchParams.set("end_date", new Date(windowAt).toISOString());
973
+ return url.toString();
974
+ }
975
+
976
+ async function queryMiniMaxUsage(
977
+ providerId: MiniMaxProviderId,
978
+ auth: ResolvedUsageAuth,
979
+ signal: AbortSignal,
980
+ timeoutMs: number,
981
+ guard: UsageRequestGuard | undefined,
982
+ ): Promise<UsageReport> {
983
+ if (!guard) throw new Error("MiniMax usage requires request-boundary revalidation.");
984
+ const apiKey = bearerToken(headerValue(auth.headers, "Authorization")) ?? auth.apiKey;
985
+ if (!apiKey) throw new Error("MiniMax runtime API key was unavailable.");
986
+ const kind = miniMaxUsageKind(apiKey);
987
+ const path = kind === "account-balance" ? "/account/query_balance" : "/v1/token_plan/remains";
988
+ const startedAt = Date.now();
989
+ await guard();
990
+ const payload = (await fetchProviderJson(
991
+ `${MINIMAX_API_ROOTS[providerId]}${path}`,
992
+ auth,
993
+ signal,
994
+ remainingTimeout(timeoutMs, startedAt, "fetching MiniMax usage"),
995
+ "MiniMax usage endpoint",
996
+ { redirect: "error" },
997
+ )) as MiniMaxUsagePayload;
998
+ await guard();
999
+ return normalizeMiniMaxUsagePayload(providerId, kind, payload, Date.now());
1000
+ }
1001
+
1002
+ async function queryMoonshotBalance(
1003
+ providerId: "moonshotai" | "moonshotai-cn",
1004
+ auth: ResolvedUsageAuth,
1005
+ signal: AbortSignal,
1006
+ timeoutMs: number,
1007
+ guard: UsageRequestGuard | undefined,
1008
+ ): Promise<UsageReport> {
1009
+ if (!guard) throw new Error("Moonshot AI balance requires request-boundary revalidation.");
1010
+ const startedAt = Date.now();
1011
+ await guard();
1012
+ const payload = (await fetchProviderJson(
1013
+ MOONSHOT_BALANCE_URLS[providerId],
1014
+ auth,
1015
+ signal,
1016
+ remainingTimeout(timeoutMs, startedAt, "fetching Moonshot AI balance"),
1017
+ "Moonshot AI balance endpoint",
1018
+ { redirect: "error" },
1019
+ )) as MoonshotBalancePayload;
1020
+ await guard();
1021
+ return normalizeMoonshotBalancePayload(providerId, payload, Date.now());
1022
+ }
1023
+
816
1024
  function remainingTimeout(
817
1025
  timeoutMs: number,
818
1026
  startedAt: number,
package/src/types.ts CHANGED
@@ -89,6 +89,12 @@ export type ProviderUsageState =
89
89
  message: string;
90
90
  };
91
91
 
92
+ export type BasetenBillingUsagePayload = {
93
+ dedicated_usage?: unknown;
94
+ model_apis_usage?: unknown;
95
+ training_usage?: unknown;
96
+ };
97
+
92
98
  export type DeepSeekBalancePayload = {
93
99
  is_available?: unknown;
94
100
  balance_infos?: unknown;
@@ -119,6 +125,30 @@ export type OpenRouterKeyPayload = {
119
125
  data?: unknown;
120
126
  };
121
127
 
128
+ export type MiniMaxUsagePayload = {
129
+ available_amount?: unknown;
130
+ balance_alert_switch?: unknown;
131
+ balance_alert_threshold?: unknown;
132
+ base_resp?: unknown;
133
+ cash_balance?: unknown;
134
+ credit_balance?: unknown;
135
+ model_remains?: unknown;
136
+ owed_amount?: unknown;
137
+ voucher_balance?: unknown;
138
+ };
139
+
140
+ export type MoonshotBalancePayload = {
141
+ code?: unknown;
142
+ data?: unknown;
143
+ scode?: unknown;
144
+ status?: unknown;
145
+ };
146
+
147
+ export type VercelAIGatewayCreditsPayload = {
148
+ balance?: unknown;
149
+ total_used?: unknown;
150
+ };
151
+
122
152
  export type OpenCodeZenPayload = {
123
153
  usage?: unknown;
124
154
  };
package/src/usage.ts CHANGED
@@ -207,13 +207,7 @@ export default function usageExtension(
207
207
  const generation = statusGeneration;
208
208
  statusCountdownTimer = setTimeout(() => {
209
209
  statusCountdownTimer = undefined;
210
- if (
211
- !sessionActive ||
212
- generation !== statusGeneration ||
213
- modelIdentity(ctx.model) !== modelIdentity(model)
214
- ) {
215
- return;
216
- }
210
+ if (!sessionActive || generation !== statusGeneration) return;
217
211
  publishStatus(ctx, outcome, model, false);
218
212
  }, STATUS_COUNTDOWN_REFRESH_MS);
219
213
  statusCountdownTimer.unref?.();
@@ -281,7 +275,17 @@ export default function usageExtension(
281
275
  },
282
276
  };
283
277
  }
284
- const requiresRequestBoundaryGuard = ["deepseek", "fireworks", "xai"].includes(adapter.id);
278
+ const requiresRequestBoundaryGuard = [
279
+ "baseten",
280
+ "deepseek",
281
+ "fireworks",
282
+ "minimax",
283
+ "minimax-cn",
284
+ "moonshotai",
285
+ "moonshotai-cn",
286
+ "vercel-ai-gateway",
287
+ "xai",
288
+ ].includes(adapter.id);
285
289
  const requestContextChanged = () =>
286
290
  expectedSessionGeneration !== sessionGeneration ||
287
291
  ctx.sessionManager.getSessionId() !== expectedSessionId ||
@@ -345,7 +349,7 @@ export default function usageExtension(
345
349
  const queryId = querySequence;
346
350
  setBoundedMap(latestQueries, failureKey, queryId, MAX_ACCOUNT_STATES);
347
351
 
348
- let deepSeekAuthChanged = false;
352
+ let retryableAuthChanged = false;
349
353
  try {
350
354
  const remainingMs = Math.max(1, deadlineAt - Date.now());
351
355
  const guard = requiresRequestBoundaryGuard
@@ -359,9 +363,11 @@ export default function usageExtension(
359
363
  );
360
364
  if (signal.aborted || requestContextChanged()) throw abortError();
361
365
  if (revalidated?.fingerprint !== auth.fingerprint) {
362
- if (adapter.id === "deepseek") {
363
- deepSeekAuthChanged = true;
364
- throw new Error("DeepSeek runtime credential changed during the balance query.");
366
+ if (["deepseek", "minimax", "minimax-cn"].includes(adapter.id)) {
367
+ retryableAuthChanged = true;
368
+ throw new Error(
369
+ `${adapter.displayName} runtime credential changed during the usage query.`,
370
+ );
365
371
  }
366
372
  throw abortError();
367
373
  }
@@ -393,7 +399,7 @@ export default function usageExtension(
393
399
  } catch (error) {
394
400
  if (isStaleExtensionContextError(error) || isAbortError(error)) throw error;
395
401
  if (
396
- deepSeekAuthChanged &&
402
+ retryableAuthChanged &&
397
403
  authRetry === 0 &&
398
404
  !signal.aborted &&
399
405
  !requestContextChanged() &&