@narumitw/pi-usage 0.57.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.
package/src/query.ts CHANGED
@@ -5,36 +5,71 @@ 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";
11
+ import {
12
+ isFireworksAccountId,
13
+ normalizeFireworksAccountsPayload,
14
+ normalizeFireworksBillingSummaryPayload,
15
+ } from "./providers/fireworks.js";
10
16
  import { normalizeGitHubCopilotUsagePayload } from "./providers/github-copilot.js";
11
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";
12
24
  import { normalizeOpenCodeZenPayload } from "./providers/opencode-zen.js";
13
25
  import { normalizeOpenRouterKeyPayload } from "./providers/openrouter.js";
26
+ import { normalizeVercelAIGatewayCreditsPayload } from "./providers/vercel-ai-gateway.js";
14
27
  import { normalizeXaiBillingPayload } from "./providers/xai.js";
15
28
  import { normalizeZaiQuotaPayload } from "./providers/zai.js";
16
29
  import type {
30
+ BasetenBillingUsagePayload,
17
31
  CodexBackendPayload,
18
32
  DeepSeekBalancePayload,
33
+ FireworksAccountsPayload,
34
+ FireworksBillingSummaryPayload,
19
35
  GitHubCopilotUsagePayload,
20
36
  KimiCodingUsagePayload,
37
+ MiniMaxUsagePayload,
38
+ MoonshotBalancePayload,
21
39
  OpenCodeZenPayload,
22
40
  OpenRouterKeyPayload,
23
41
  PiModel,
24
42
  ResolvedUsageAuth,
25
43
  UsageProviderAdapter,
44
+ UsageQuerySettings,
26
45
  UsageReport,
46
+ VercelAIGatewayCreditsPayload,
27
47
  XaiBillingPayload,
28
48
  XaiUserPayload,
29
49
  ZaiQuotaPayload,
30
50
  } from "./types.js";
31
51
 
52
+ const BASETEN_BILLING_USAGE_URL = "https://api.baseten.co/v1/billing/usage_summary";
53
+ const BASETEN_USAGE_WINDOW_DAYS = 30;
32
54
  const CODEX_USAGE_URL = "https://chatgpt.com/backend-api/wham/usage";
33
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;
34
59
  const GITHUB_COPILOT_USAGE_URL = "https://api.github.com/copilot_internal/user";
35
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";
36
62
  const OPENCODE_GO_USAGE_URL = "https://opencode.ai/zen/go/v1/usage";
37
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";
38
73
  const XAI_USER_URL = "https://cli-chat-proxy.grok.com/v1/user?include=subscription";
39
74
  const XAI_BILLING_URL = "https://cli-chat-proxy.grok.com/v1/billing?format=credits";
40
75
  const XAI_CLIENT_HEADERS = Object.freeze({
@@ -50,6 +85,27 @@ export const AUTH_FINGERPRINT_SALT = randomBytes(32);
50
85
  export type UsageRequestGuard = () => Promise<void>;
51
86
 
52
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
+ },
53
109
  {
54
110
  id: "openai-codex",
55
111
  displayName: "OpenAI Codex",
@@ -122,6 +178,56 @@ export const SUPPORTED_ADAPTERS: readonly UsageProviderAdapter[] = [
122
178
  return normalizeOpenRouterKeyPayload(payload as OpenRouterKeyPayload, Date.now());
123
179
  },
124
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
+ },
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
+ },
125
231
  {
126
232
  id: "opencode-go",
127
233
  displayName: "OpenCode Go",
@@ -153,6 +259,38 @@ export const SUPPORTED_ADAPTERS: readonly UsageProviderAdapter[] = [
153
259
  return normalizeKimiCodingUsagePayload(payload as KimiCodingUsagePayload, Date.now());
154
260
  },
155
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
+ },
156
294
  {
157
295
  id: "zai",
158
296
  displayName: "Z.AI",
@@ -289,11 +427,14 @@ export async function resolveUsageAuth(
289
427
  if (!result.ok) throw new Error(redactUsageError(result.error));
290
428
  return authorizationFrom(result) ? result : undefined;
291
429
  };
292
- if (adapter.id !== "deepseek") modelAuth = await resolveCurrentModelAuth();
430
+ const resolveSelectedAuthLast = ["deepseek", "minimax", "minimax-cn"].includes(adapter.id);
431
+ if (!resolveSelectedAuthLast) modelAuth = await resolveCurrentModelAuth();
293
432
  if (typeof registry.getProviderAuth !== "function") {
294
433
  throw new Error("pi-usage requires Pi 0.81.0 or newer to validate resolved provider auth.");
295
434
  }
435
+ if (!moonshotProviderAuthIsAllowed(ctx, adapter.id)) return undefined;
296
436
  const providerResult = await registry.getProviderAuth(adapter.id);
437
+ if (!moonshotProviderAuthIsAllowed(ctx, adapter.id)) return undefined;
297
438
  if (
298
439
  providerResult?.auth.baseUrl &&
299
440
  !hasOfficialUrlOrigin(providerResult.auth.baseUrl, adapter.id)
@@ -302,9 +443,9 @@ export async function resolveUsageAuth(
302
443
  `${adapter.displayName} usage cannot send a proxy-resolved credential to the official usage endpoint.`,
303
444
  );
304
445
  }
305
- // DeepSeek reads selected-model auth last so a rotation during provider-origin validation
306
- // cannot leave the earlier credential queued for the balance request.
307
- 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();
308
449
  const auth = modelAuth ?? providerResult?.auth;
309
450
  if (!auth) return undefined;
310
451
  if (adapter.id === "github-copilot") {
@@ -370,9 +511,10 @@ export async function queryProviderUsage(
370
511
  signal: AbortSignal,
371
512
  timeoutMs: number,
372
513
  guard?: UsageRequestGuard,
514
+ settings?: Readonly<UsageQuerySettings>,
373
515
  ): Promise<UsageReport> {
374
516
  try {
375
- return await adapter.query(auth, signal, timeoutMs, guard);
517
+ return await adapter.query(auth, signal, timeoutMs, guard, settings);
376
518
  } catch (error) {
377
519
  if (isStaleExtensionContextError(error) || isAbortError(error)) throw error;
378
520
  throw new Error(redactUsageError(errorMessage(error), auth.secrets));
@@ -381,12 +523,53 @@ export async function queryProviderUsage(
381
523
 
382
524
  export function providerIsConfigured(ctx: ExtensionContext, providerId: string): boolean {
383
525
  try {
384
- 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);
385
543
  } catch {
386
- return candidateModels(ctx, providerId).length > 0;
544
+ return false;
387
545
  }
388
546
  }
389
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
+
390
573
  function candidateModels(ctx: ExtensionContext, providerId: string): PiModel[] {
391
574
  const candidates: PiModel[] = [];
392
575
  const seen = new Set<string>();
@@ -731,11 +914,20 @@ function hasOfficialOrigin(model: PiModel, providerId: string): boolean {
731
914
  function hasOfficialUrlOrigin(value: string, providerId: string): boolean {
732
915
  try {
733
916
  const url = new URL(value);
917
+ if (providerId === "baseten") {
918
+ return ["https://inference.baseten.co", "https://api.baseten.co"].includes(url.origin);
919
+ }
734
920
  if (providerId === "openai-codex") return url.origin === "https://chatgpt.com";
735
921
  if (providerId === "deepseek") return url.origin === "https://api.deepseek.com";
922
+ if (providerId === "fireworks") return url.origin === FIREWORKS_BILLING_SUMMARY_ORIGIN;
736
923
  if (providerId === "openrouter") return url.origin === "https://openrouter.ai";
924
+ if (providerId === "vercel-ai-gateway") return url.origin === "https://ai-gateway.vercel.sh";
737
925
  if (providerId === "opencode-go") return url.origin === "https://opencode.ai";
738
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";
739
931
  if (providerId === "xai") return url.origin === "https://api.x.ai";
740
932
  if (providerId === "zai") return url.origin === "https://api.z.ai";
741
933
  if (providerId === "zai-coding-cn") return url.origin === "https://open.bigmodel.cn";
@@ -771,12 +963,166 @@ function validatedXaiUserId(value: unknown): string {
771
963
  return value;
772
964
  }
773
965
 
774
- function remainingTimeout(timeoutMs: number, startedAt: number): number {
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
+
1024
+ function remainingTimeout(
1025
+ timeoutMs: number,
1026
+ startedAt: number,
1027
+ description = "fetching xAI consumer usage",
1028
+ ): number {
775
1029
  const remaining = timeoutMs - (Date.now() - startedAt);
776
- if (remaining <= 0) throw new Error("Timed out while fetching xAI consumer usage.");
1030
+ if (remaining <= 0) throw new Error(`Timed out while ${description}.`);
777
1031
  return remaining;
778
1032
  }
779
1033
 
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();
1124
+ }
1125
+
780
1126
  function zaiMonitorUrl(baseUrl: string | undefined): string {
781
1127
  const base = baseUrl?.trim();
782
1128
  if (!base) throw new Error("Z.AI model base URL is unavailable.");
package/src/settings.ts CHANGED
@@ -3,6 +3,7 @@ import { constants } from "node:fs";
3
3
  import { chmod, mkdir, open, rename, rm, writeFile } from "node:fs/promises";
4
4
  import { basename, dirname, join } from "node:path";
5
5
  import { getAgentDir } from "@earendil-works/pi-coding-agent";
6
+ import { isFireworksAccountId } from "./providers/fireworks.js";
6
7
 
7
8
  export const USAGE_SETTINGS_FILE = "pi-usage.json";
8
9
  export const MAX_USAGE_SETTINGS_BYTES = 64 * 1024;
@@ -10,6 +11,7 @@ export const MAX_USAGE_SETTINGS_BYTES = 64 * 1024;
10
11
  export interface UsageSettings {
11
12
  codexFastMode: boolean;
12
13
  codexStatusResetCountdown: boolean;
14
+ fireworksAccountId?: string;
13
15
  }
14
16
 
15
17
  export const DEFAULT_USAGE_SETTINGS: Readonly<UsageSettings> = Object.freeze({
@@ -60,6 +62,12 @@ export function normalizeUsageSettings(value: unknown): UsageSettings | undefine
60
62
  ) {
61
63
  return undefined;
62
64
  }
65
+ if (
66
+ Object.hasOwn(value, "fireworksAccountId") &&
67
+ !isFireworksAccountId(value.fireworksAccountId)
68
+ ) {
69
+ return undefined;
70
+ }
63
71
  return {
64
72
  codexFastMode:
65
73
  typeof value.codexFastMode === "boolean"
@@ -69,6 +77,9 @@ export function normalizeUsageSettings(value: unknown): UsageSettings | undefine
69
77
  typeof value.codexStatusResetCountdown === "boolean"
70
78
  ? value.codexStatusResetCountdown
71
79
  : DEFAULT_USAGE_SETTINGS.codexStatusResetCountdown,
80
+ ...(isFireworksAccountId(value.fireworksAccountId)
81
+ ? { fireworksAccountId: value.fireworksAccountId }
82
+ : {}),
72
83
  };
73
84
  }
74
85
 
@@ -172,7 +183,11 @@ async function saveUsageSettingsPatch(
172
183
  if (latest.kind === "invalid") {
173
184
  throw new Error("Cannot overwrite an invalid pi-usage.json; repair it and reload first");
174
185
  }
175
- const document = { ...latest.document, ...patch };
186
+ const document = { ...latest.document };
187
+ for (const [key, value] of Object.entries(patch)) {
188
+ if (value === undefined) delete document[key];
189
+ else document[key] = value;
190
+ }
176
191
  const settings = normalizeUsageSettings(document);
177
192
  if (!settings) throw new Error("Refusing to save invalid pi-usage settings");
178
193
  const directory = dirname(path);
package/src/types.ts CHANGED
@@ -55,6 +55,10 @@ export interface ResolvedUsageAuth {
55
55
  model: PiModel;
56
56
  }
57
57
 
58
+ export interface UsageQuerySettings {
59
+ fireworksAccountId?: string;
60
+ }
61
+
58
62
  export interface UsageProviderAdapter {
59
63
  id: string;
60
64
  displayName: string;
@@ -65,6 +69,7 @@ export interface UsageProviderAdapter {
65
69
  signal: AbortSignal,
66
70
  timeoutMs: number,
67
71
  guard?: () => Promise<void>,
72
+ settings?: Readonly<UsageQuerySettings>,
68
73
  ): Promise<UsageReport>;
69
74
  }
70
75
 
@@ -84,11 +89,26 @@ export type ProviderUsageState =
84
89
  message: string;
85
90
  };
86
91
 
92
+ export type BasetenBillingUsagePayload = {
93
+ dedicated_usage?: unknown;
94
+ model_apis_usage?: unknown;
95
+ training_usage?: unknown;
96
+ };
97
+
87
98
  export type DeepSeekBalancePayload = {
88
99
  is_available?: unknown;
89
100
  balance_infos?: unknown;
90
101
  };
91
102
 
103
+ export type FireworksAccountsPayload = {
104
+ accounts?: unknown;
105
+ nextPageToken?: unknown;
106
+ };
107
+
108
+ export type FireworksBillingSummaryPayload = {
109
+ lineItems?: unknown;
110
+ };
111
+
92
112
  export type GitHubCopilotUsagePayload = {
93
113
  login?: unknown;
94
114
  copilot_plan?: unknown;
@@ -105,6 +125,30 @@ export type OpenRouterKeyPayload = {
105
125
  data?: unknown;
106
126
  };
107
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
+
108
152
  export type OpenCodeZenPayload = {
109
153
  usage?: unknown;
110
154
  };