@narumitw/pi-usage 0.60.0 → 0.60.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@narumitw/pi-usage",
3
- "version": "0.60.0",
3
+ "version": "0.60.2",
4
4
  "description": "Pi extension that shows current-account usage and DeepSeek API balance for supported providers.",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/core.ts CHANGED
@@ -54,13 +54,39 @@ export class UsageCache {
54
54
  }
55
55
 
56
56
  export function fingerprintResolvedAuth(
57
- auth: { apiKey?: string; headers?: Record<string, string> },
57
+ auth: {
58
+ apiKey?: string;
59
+ headers?: Record<string, string | null>;
60
+ baseUrl?: string;
61
+ env?: Record<string, string>;
62
+ source?: string;
63
+ providerAuth?: {
64
+ apiKey?: string;
65
+ headers?: Record<string, string | null>;
66
+ baseUrl?: string;
67
+ };
68
+ },
58
69
  salt: Uint8Array,
59
70
  ): string {
60
71
  const headers = Object.entries(auth.headers ?? {})
61
72
  .map(([name, value]) => [name.toLowerCase(), value] as const)
62
73
  .sort(([left], [right]) => left.localeCompare(right));
63
- const canonical = JSON.stringify({ apiKey: auth.apiKey ?? "", headers });
74
+ const env = Object.entries(auth.env ?? {}).sort(([left], [right]) => left.localeCompare(right));
75
+ const providerHeaders = Object.entries(auth.providerAuth?.headers ?? {})
76
+ .map(([name, value]) => [name.toLowerCase(), value] as const)
77
+ .sort(([left], [right]) => left.localeCompare(right));
78
+ const canonical = JSON.stringify({
79
+ apiKey: auth.apiKey ?? "",
80
+ headers,
81
+ baseUrl: auth.baseUrl ?? "",
82
+ env,
83
+ source: auth.source ?? "",
84
+ providerAuth: {
85
+ apiKey: auth.providerAuth?.apiKey ?? "",
86
+ headers: providerHeaders,
87
+ baseUrl: auth.providerAuth?.baseUrl ?? "",
88
+ },
89
+ });
64
90
  return createHmac("sha256", salt).update(canonical).digest("hex");
65
91
  }
66
92
 
package/src/format.ts CHANGED
@@ -94,6 +94,9 @@ export function formatProviderStates(states: readonly ProviderUsageState[]): str
94
94
  .map((state) => {
95
95
  if (state.status === "ready") return formatUsageReport(state.report, state.displayState);
96
96
  const label = state.displayState === "current" ? "Current" : "Configured";
97
+ if (state.status === "selection-required") {
98
+ return `${state.providerName} · ${label}\nSelection required: choose this provider's ${state.singularLabel} by viewing it individually.`;
99
+ }
97
100
  const status =
98
101
  state.status === "auth-unavailable"
99
102
  ? "Authentication unavailable"
@@ -364,9 +367,11 @@ function formatMiniMaxReport(lines: string[], report: UsageReport): void {
364
367
  const value =
365
368
  bucket.period === "unlimited"
366
369
  ? "unlimited"
367
- : bucket.limit && bucket.remaining !== undefined
368
- ? `${bucket.remaining} of ${bucket.limit} left · ${percentRemaining(bucket)}%${reset}`
369
- : "unavailable";
370
+ : bucket.unit === "percent" && bucket.remaining !== undefined
371
+ ? `${bucket.remaining}% remaining${reset}`
372
+ : bucket.limit !== undefined && bucket.remaining !== undefined
373
+ ? `${bucket.remaining} of ${bucket.limit} left · ${percentRemaining(bucket)}%${reset}`
374
+ : "unavailable";
370
375
  lines.push(`${`${bucket.label}:`.padEnd(VALUE_COLUMN)}${value}`);
371
376
  }
372
377
  }
@@ -388,7 +393,11 @@ function formatMiniMaxStatusline(report: UsageReport, model?: UsageModel): strin
388
393
  parts.push(`unlimited ${window}`);
389
394
  continue;
390
395
  }
391
- if (!bucket.limit || bucket.remaining === undefined) continue;
396
+ if (bucket.unit === "percent" && bucket.remaining !== undefined) {
397
+ parts.push(`${bucket.remaining}% ${window}`);
398
+ continue;
399
+ }
400
+ if (bucket.limit === undefined || bucket.remaining === undefined) continue;
392
401
  parts.push(`${percentRemaining(bucket)}% ${window}`);
393
402
  }
394
403
  return parts.length > 1 ? parts.join(" ") : undefined;
@@ -403,27 +412,32 @@ function selectMiniMaxGroup(report: UsageReport, model?: UsageModel): string | u
403
412
  ),
404
413
  ];
405
414
  if (groups.length <= 1) return groups[0];
406
- if (model?.provider !== report.providerId) return undefined;
407
- const modelKeys = [model.id, model.name]
408
- .map(normalizeMiniMaxModelKey)
409
- .filter((key): key is string => key !== undefined);
410
- const candidates = groups.map((group) => {
411
- const bucket = report.buckets.find((candidate) => candidate.groupId === group);
412
- const patterns = [bucket?.groupLabel, ...(bucket?.modelKeys ?? []), group]
415
+ if (model && model.provider !== report.providerId) return undefined;
416
+ if (model) {
417
+ const modelKeys = [model.id, model.name]
413
418
  .map(normalizeMiniMaxModelKey)
414
419
  .filter((key): key is string => key !== undefined);
415
- return { group, patterns };
416
- });
417
- const exact = candidates.find(({ patterns }) =>
418
- patterns.some((pattern) => !pattern.includes("*") && modelKeys.includes(pattern)),
419
- );
420
- if (exact) return exact.group;
421
- return candidates.find(({ patterns }) =>
422
- patterns.some(
423
- (pattern) =>
424
- pattern.includes("*") && modelKeys.some((key) => wildcardKeyMatches(pattern, key)),
425
- ),
426
- )?.group;
420
+ const candidates = groups.map((group) => {
421
+ const bucket = report.buckets.find((candidate) => candidate.groupId === group);
422
+ const patterns = [bucket?.groupLabel, ...(bucket?.modelKeys ?? []), group]
423
+ .map(normalizeMiniMaxModelKey)
424
+ .filter((key): key is string => key !== undefined);
425
+ return { group, patterns };
426
+ });
427
+ const exact = candidates.find(({ patterns }) =>
428
+ patterns.some((pattern) => !pattern.includes("*") && modelKeys.includes(pattern)),
429
+ );
430
+ if (exact) return exact.group;
431
+ const wildcard = candidates.find(({ patterns }) =>
432
+ patterns.some(
433
+ (pattern) =>
434
+ pattern.includes("*") && modelKeys.some((key) => wildcardKeyMatches(pattern, key)),
435
+ ),
436
+ );
437
+ if (wildcard) return wildcard.group;
438
+ }
439
+ // Prefer the Coding Plan catch-all over hiding the chip.
440
+ return groups.find((group) => group === "general");
427
441
  }
428
442
 
429
443
  function normalizeMiniMaxModelKey(value: string | undefined): string | undefined {
package/src/index.ts CHANGED
@@ -36,6 +36,7 @@ export { normalizeBasetenBillingUsagePayload } from "./providers/baseten.js";
36
36
  export { normalizeCodexBackendPayload } from "./providers/codex.js";
37
37
  export { normalizeDeepSeekBalancePayload } from "./providers/deepseek.js";
38
38
  export {
39
+ createFireworksAdapter,
39
40
  normalizeFireworksAccountsPayload,
40
41
  normalizeFireworksBillingSummaryPayload,
41
42
  } from "./providers/fireworks.js";
@@ -67,6 +68,7 @@ export type {
67
68
  UsageSettings,
68
69
  UsageSettingsRuntime,
69
70
  UsageSettingsState,
71
+ UsageTargetPublicationCheck,
70
72
  } from "./settings.js";
71
73
  export {
72
74
  createUsageSettingsRuntime,
@@ -90,13 +92,24 @@ export type {
90
92
  UsageMetric,
91
93
  UsageModel,
92
94
  UsageProviderAdapter,
95
+ UsageProviderTarget,
93
96
  UsageQuerySettings,
94
97
  UsageReport,
98
+ UsageRequestGuard,
95
99
  UsageSemantics,
96
100
  UsageSemanticsKind,
101
+ UsageTargetResolver,
97
102
  UsageUnit,
98
103
  VercelAIGatewayCreditsPayload,
99
104
  XaiBillingPayload,
100
105
  XaiUserPayload,
101
106
  } from "./types.js";
102
107
  export { default } from "./usage.js";
108
+ export type { UsageTargetResolution, UsageTargetSelectOptions } from "./usage-targets.js";
109
+ export {
110
+ createUsageTargetSelectOptions,
111
+ isBoundedTargetId,
112
+ listUsageTargets,
113
+ normalizeUsageTargets,
114
+ resolveUsageTarget,
115
+ } from "./usage-targets.js";
@@ -2,7 +2,9 @@ import { sanitizeDisplayText } from "../core.js";
2
2
  import type {
3
3
  FireworksAccountsPayload,
4
4
  FireworksBillingSummaryPayload,
5
+ ResolvedUsageAuth,
5
6
  UsageMetric,
7
+ UsageProviderAdapter,
6
8
  UsageReport,
7
9
  } from "../types.js";
8
10
 
@@ -14,6 +16,9 @@ const INT64_MIN = -(2n ** 63n);
14
16
  const INT64_MAX = 2n ** 63n - 1n;
15
17
  const MAX_UNITS_CHARS = 20;
16
18
  const MAX_NANOS_CHARS = 11;
19
+ const FIREWORKS_BILLING_SUMMARY_ORIGIN = "https://api.fireworks.ai";
20
+ const FIREWORKS_SPEND_WINDOW_DAYS = 30;
21
+ const FIREWORKS_MAX_ACCOUNT_PAGES = 5;
17
22
 
18
23
  const SERIES_KEYS = ["serverless", "dedicated", "training", "other"] as const;
19
24
  type SeriesKey = (typeof SERIES_KEYS)[number];
@@ -53,6 +58,77 @@ export function normalizeFireworksAccountsPayload(payload: FireworksAccountsPayl
53
58
  return accounts;
54
59
  }
55
60
 
61
+ type FetchProviderJson = (
62
+ url: string,
63
+ auth: ResolvedUsageAuth,
64
+ signal: AbortSignal,
65
+ timeoutMs: number,
66
+ description: string,
67
+ request?: { redirect?: RequestRedirect },
68
+ ) => Promise<Record<string, unknown>>;
69
+
70
+ export function createFireworksAdapter(fetchProviderJson: FetchProviderJson): UsageProviderAdapter {
71
+ return {
72
+ id: "fireworks",
73
+ displayName: "Fireworks",
74
+ semantics: { kind: "api-key", label: "Fireworks API spend" },
75
+ targets: {
76
+ singularLabel: "account",
77
+ pluralLabel: "accounts",
78
+ async list(auth, signal, timeoutMs, guard) {
79
+ const startedAt = Date.now();
80
+ const accounts: string[] = [];
81
+ let pageToken: string | undefined;
82
+ for (let page = 0; page < FIREWORKS_MAX_ACCOUNT_PAGES; page += 1) {
83
+ await guard();
84
+ const payload = (await fetchProviderJson(
85
+ fireworksAccountsUrl(pageToken),
86
+ auth,
87
+ signal,
88
+ remainingTimeout(timeoutMs, startedAt, "fetching Fireworks accounts"),
89
+ "Fireworks accounts endpoint",
90
+ { redirect: "error" },
91
+ )) as FireworksAccountsPayload;
92
+ await guard();
93
+ for (const accountId of normalizeFireworksAccountsPayload(payload)) {
94
+ if (accounts.includes(accountId)) {
95
+ throw new Error(`Fireworks accounts listing repeated ${accountId}.`);
96
+ }
97
+ accounts.push(accountId);
98
+ }
99
+ pageToken = fireworksNextPageToken(payload.nextPageToken);
100
+ if (!pageToken) break;
101
+ }
102
+ if (pageToken) {
103
+ throw new Error(
104
+ `Fireworks account listing exceeded ${FIREWORKS_MAX_ACCOUNT_PAGES} pages.`,
105
+ );
106
+ }
107
+ return accounts.map((id) => ({ id, label: id }));
108
+ },
109
+ },
110
+ async query(auth, signal, timeoutMs, guard, targetId) {
111
+ if (!guard) throw new Error("Fireworks API spend requires request-boundary revalidation.");
112
+ if (!isFireworksAccountId(targetId)) {
113
+ throw new Error("Fireworks billing requires a safe selected account slug.");
114
+ }
115
+ const startedAt = Date.now();
116
+ await guard();
117
+ const billingWindowAt = Date.now();
118
+ const payload = (await fetchProviderJson(
119
+ fireworksBillingSummaryUrl(targetId, billingWindowAt),
120
+ auth,
121
+ signal,
122
+ remainingTimeout(timeoutMs, startedAt, "fetching Fireworks rated spend"),
123
+ "Fireworks billing summary endpoint",
124
+ { redirect: "error" },
125
+ )) as FireworksBillingSummaryPayload;
126
+ await guard();
127
+ return normalizeFireworksBillingSummaryPayload(payload, targetId, Date.now());
128
+ },
129
+ };
130
+ }
131
+
56
132
  export function normalizeFireworksBillingSummaryPayload(
57
133
  payload: FireworksBillingSummaryPayload,
58
134
  accountId: string,
@@ -192,6 +268,42 @@ function formatMoneyAmount(amount: bigint): string {
192
268
  return `${negative ? "-" : ""}${units.toString()}${nanos ? `.${nanos}` : ""}`;
193
269
  }
194
270
 
271
+ function fireworksAccountsUrl(pageToken: string | undefined): string {
272
+ const url = new URL("/v1/accounts", FIREWORKS_BILLING_SUMMARY_ORIGIN);
273
+ url.searchParams.set("pageSize", "200");
274
+ if (pageToken !== undefined) url.searchParams.set("pageToken", pageToken);
275
+ return url.toString();
276
+ }
277
+
278
+ function fireworksNextPageToken(value: unknown): string | undefined {
279
+ if (value === undefined || value === null) return undefined;
280
+ if (typeof value !== "string" || !value || value.length > 512) {
281
+ throw new Error("Fireworks accounts listing returned an invalid page token.");
282
+ }
283
+ return value;
284
+ }
285
+
286
+ function fireworksBillingSummaryUrl(accountId: string, startedAt: number): string {
287
+ const dayMs = 24 * 60 * 60 * 1000;
288
+ const dayFloor = (time: number) => `${new Date(time).toISOString().slice(0, 10)}T00:00:00Z`;
289
+ const url = new URL(
290
+ `/v1/accounts/${accountId}/billing/summary`,
291
+ FIREWORKS_BILLING_SUMMARY_ORIGIN,
292
+ );
293
+ url.searchParams.set(
294
+ "startTime",
295
+ dayFloor(startedAt - (FIREWORKS_SPEND_WINDOW_DAYS - 1) * dayMs),
296
+ );
297
+ url.searchParams.set("endTime", dayFloor(startedAt + dayMs));
298
+ return url.toString();
299
+ }
300
+
301
+ function remainingTimeout(timeoutMs: number, startedAt: number, description: string): number {
302
+ const remaining = timeoutMs - (Date.now() - startedAt);
303
+ if (remaining <= 0) throw new Error(`Timed out while ${description}.`);
304
+ return remaining;
305
+ }
306
+
195
307
  function asObject(value: unknown): Record<string, unknown> | undefined {
196
308
  if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
197
309
  return value as Record<string, unknown>;
@@ -152,6 +152,27 @@ function normalizeWindow(row: Record<string, unknown>, fields: WindowFields): Us
152
152
  }
153
153
  const total = nonnegativeInteger(row[fields.totalField], fields.totalField);
154
154
  const count = nonnegativeInteger(row[fields.countField], fields.countField);
155
+ if (total === 0) {
156
+ if (count !== 0) {
157
+ throw new Error(`MiniMax Token Plan ${fields.label} counts were inconsistent.`);
158
+ }
159
+ if (percent === undefined) {
160
+ throw new Error(`MiniMax Token Plan ${fields.label} returned no quota and no percent.`);
161
+ }
162
+ // Token Plan reports remaining_percent as the quota when both counts are 0.
163
+ return {
164
+ id: fields.id,
165
+ label: fields.label,
166
+ groupId: fields.groupId,
167
+ groupLabel: fields.groupLabel,
168
+ remaining: percent,
169
+ used: 100 - percent,
170
+ limit: 0,
171
+ unit: "percent",
172
+ windowMinutes,
173
+ resetsAt,
174
+ };
175
+ }
155
176
  const resolved = resolveQuotaCounts(count, total, percent);
156
177
  if (!resolved) throw new Error(`MiniMax Token Plan ${fields.label} counts were inconsistent.`);
157
178
  return {