@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/README.md +27 -19
- package/dist/index.ts +804 -322
- package/dist/index.ts.map +4 -4
- package/package.json +1 -1
- package/src/core.ts +28 -2
- package/src/format.ts +37 -23
- package/src/index.ts +13 -0
- package/src/providers/fireworks.ts +112 -0
- package/src/providers/minimax.ts +21 -0
- package/src/query.ts +110 -150
- package/src/settings.ts +155 -8
- package/src/types.ts +39 -2
- package/src/usage-settings-ui.ts +88 -183
- package/src/usage-targets.ts +143 -0
- package/src/usage.ts +419 -107
package/package.json
CHANGED
package/src/core.ts
CHANGED
|
@@ -54,13 +54,39 @@ export class UsageCache {
|
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
export function fingerprintResolvedAuth(
|
|
57
|
-
auth: {
|
|
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
|
|
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.
|
|
368
|
-
? `${bucket.remaining}
|
|
369
|
-
:
|
|
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 (
|
|
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
|
|
407
|
-
|
|
408
|
-
.
|
|
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
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
(pattern) =>
|
|
424
|
-
|
|
425
|
-
)
|
|
426
|
-
|
|
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>;
|
package/src/providers/minimax.ts
CHANGED
|
@@ -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 {
|