@narumitw/pi-usage 0.58.0 → 0.60.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/README.md +86 -7
- package/dist/index.ts +948 -235
- package/dist/index.ts.map +4 -4
- package/package.json +7 -1
- package/src/format.ts +176 -15
- package/src/index.ts +14 -1
- package/src/providers/baseten.ts +55 -0
- package/src/providers/minimax.ts +264 -0
- package/src/providers/moonshot.ts +64 -0
- package/src/providers/vercel-ai-gateway.ts +43 -0
- package/src/providers/zai.ts +109 -5
- package/src/query.ts +276 -32
- package/src/types.ts +41 -0
- package/src/usage.ts +21 -13
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@narumitw/pi-usage",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.60.0",
|
|
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",
|
|
@@ -14,9 +14,15 @@
|
|
|
14
14
|
"balance",
|
|
15
15
|
"deepseek",
|
|
16
16
|
"fireworks",
|
|
17
|
+
"baseten",
|
|
18
|
+
"vercel-ai-gateway",
|
|
17
19
|
"codex",
|
|
18
20
|
"kimi",
|
|
19
21
|
"kimi-coding",
|
|
22
|
+
"moonshot",
|
|
23
|
+
"moonshotai",
|
|
24
|
+
"minimax",
|
|
25
|
+
"token-plan",
|
|
20
26
|
"copilot",
|
|
21
27
|
"openrouter",
|
|
22
28
|
"opencode",
|
package/src/format.ts
CHANGED
|
@@ -12,23 +12,39 @@ const VALUE_COLUMN = 29;
|
|
|
12
12
|
export function formatUsageReport(report: UsageReport, displayState: UsageDisplayState): string {
|
|
13
13
|
const stateLabel = displayState === "current" ? "Current" : "Configured";
|
|
14
14
|
const title =
|
|
15
|
-
report.providerId === "
|
|
16
|
-
? "
|
|
17
|
-
: report.providerId === "
|
|
18
|
-
? "
|
|
19
|
-
:
|
|
15
|
+
report.providerId === "baseten"
|
|
16
|
+
? "Baseten Model APIs Spend"
|
|
17
|
+
: report.providerId === "deepseek"
|
|
18
|
+
? "DeepSeek API Balance"
|
|
19
|
+
: report.providerId === "fireworks"
|
|
20
|
+
? "Fireworks API Spend"
|
|
21
|
+
: report.providerId === "vercel-ai-gateway"
|
|
22
|
+
? "Vercel AI Gateway Credits"
|
|
23
|
+
: report.providerId === "moonshotai" || report.providerId === "moonshotai-cn"
|
|
24
|
+
? `${report.providerName} Balance`
|
|
25
|
+
: report.providerId === "minimax" || report.providerId === "minimax-cn"
|
|
26
|
+
? report.source === "minimax-account-balance"
|
|
27
|
+
? `${report.providerName} API Balance`
|
|
28
|
+
: `${report.providerName} Token Plan`
|
|
29
|
+
: `${report.providerName} Usage`;
|
|
20
30
|
const lines = [`${title} · ${stateLabel}`];
|
|
21
31
|
if (report.accountLabel) lines.push(`Account: ${report.accountLabel}`);
|
|
22
32
|
lines.push(`Semantics: ${report.semantics.label}`, "");
|
|
23
33
|
|
|
24
|
-
if (report.providerId === "
|
|
34
|
+
if (report.providerId === "baseten") formatBasetenReport(lines, report);
|
|
35
|
+
else if (report.providerId === "openai-codex") formatCodexReport(lines, report);
|
|
25
36
|
else if (report.providerId === "deepseek") formatDeepSeekReport(lines, report);
|
|
26
37
|
else if (report.providerId === "fireworks") formatFireworksReport(lines, report);
|
|
38
|
+
else if (report.providerId === "vercel-ai-gateway") formatVercelAIGatewayReport(lines, report);
|
|
27
39
|
else if (report.providerId === "github-copilot") formatGitHubCopilotReport(lines, report);
|
|
28
40
|
else if (report.providerId === "openrouter") formatOpenRouterReport(lines, report);
|
|
29
41
|
else if (report.providerId === "opencode-go") formatOpenCodeZenReport(lines, report);
|
|
30
42
|
else if (report.providerId === "kimi-coding") formatKimiCodingReport(lines, report);
|
|
31
|
-
else if (report.providerId === "
|
|
43
|
+
else if (report.providerId === "moonshotai" || report.providerId === "moonshotai-cn") {
|
|
44
|
+
formatMoonshotReport(lines, report);
|
|
45
|
+
} else if (report.providerId === "minimax" || report.providerId === "minimax-cn") {
|
|
46
|
+
formatMiniMaxReport(lines, report);
|
|
47
|
+
} else if (report.providerId === "xai") formatXaiReport(lines, report);
|
|
32
48
|
else if (report.providerId === "zai" || report.providerId === "zai-coding-cn") {
|
|
33
49
|
formatZaiReport(lines, report);
|
|
34
50
|
} else formatGenericReport(lines, report);
|
|
@@ -45,11 +61,13 @@ export function formatUsageStatusline(
|
|
|
45
61
|
now = Date.now(),
|
|
46
62
|
showCodexResetCountdown = true,
|
|
47
63
|
): string | undefined {
|
|
64
|
+
if (report.providerId === "baseten") return formatBasetenStatusline(report);
|
|
48
65
|
if (report.providerId === "openai-codex") {
|
|
49
66
|
return formatCodexStatusline(report, model, now, showCodexResetCountdown);
|
|
50
67
|
}
|
|
51
68
|
if (report.providerId === "deepseek") return formatDeepSeekStatusline(report);
|
|
52
69
|
if (report.providerId === "fireworks") return formatFireworksStatusline(report);
|
|
70
|
+
if (report.providerId === "vercel-ai-gateway") return formatVercelAIGatewayStatusline(report);
|
|
53
71
|
if (report.providerId === "github-copilot") return formatGitHubCopilotStatusline(report);
|
|
54
72
|
if (report.providerId === "openrouter") {
|
|
55
73
|
const limit = report.buckets.find((bucket) => bucket.id === "key-limit");
|
|
@@ -59,6 +77,12 @@ export function formatUsageStatusline(
|
|
|
59
77
|
}
|
|
60
78
|
if (report.providerId === "opencode-go") return formatOpenCodeZenStatusline(report);
|
|
61
79
|
if (report.providerId === "kimi-coding") return formatKimiCodingStatusline(report);
|
|
80
|
+
if (report.providerId === "moonshotai" || report.providerId === "moonshotai-cn") {
|
|
81
|
+
return formatMoonshotStatusline(report);
|
|
82
|
+
}
|
|
83
|
+
if (report.providerId === "minimax" || report.providerId === "minimax-cn") {
|
|
84
|
+
return formatMiniMaxStatusline(report, model);
|
|
85
|
+
}
|
|
62
86
|
if (report.providerId === "zai" || report.providerId === "zai-coding-cn") {
|
|
63
87
|
return formatZaiStatusline(report);
|
|
64
88
|
}
|
|
@@ -81,6 +105,18 @@ export function formatProviderStates(states: readonly ProviderUsageState[]): str
|
|
|
81
105
|
.join("\n\n");
|
|
82
106
|
}
|
|
83
107
|
|
|
108
|
+
function formatBasetenReport(lines: string[], report: UsageReport): void {
|
|
109
|
+
lines.push(`${"Spend window:".padEnd(VALUE_COLUMN)}Last 30 days`);
|
|
110
|
+
for (const metric of report.metrics) {
|
|
111
|
+
lines.push(`${`${metric.label}:`.padEnd(VALUE_COLUMN)}USD ${metric.value}`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function formatBasetenStatusline(report: UsageReport): string {
|
|
116
|
+
const subtotal = report.metrics.find((metric) => metric.id === "net-subtotal");
|
|
117
|
+
return subtotal ? `baseten USD ${subtotal.value} net` : "baseten no Model APIs usage";
|
|
118
|
+
}
|
|
119
|
+
|
|
84
120
|
function formatCodexReport(lines: string[], report: UsageReport): void {
|
|
85
121
|
let previousGroup: string | undefined;
|
|
86
122
|
for (const bucket of report.buckets) {
|
|
@@ -157,6 +193,17 @@ function fireworksCurrencies(report: UsageReport): string[] {
|
|
|
157
193
|
return currencies;
|
|
158
194
|
}
|
|
159
195
|
|
|
196
|
+
function formatVercelAIGatewayReport(lines: string[], report: UsageReport): void {
|
|
197
|
+
for (const metric of report.metrics) {
|
|
198
|
+
lines.push(`${`${metric.label}:`.padEnd(VALUE_COLUMN)}USD ${metric.value}`);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function formatVercelAIGatewayStatusline(report: UsageReport): string {
|
|
203
|
+
const balance = report.metrics.find((metric) => metric.id === "credit-balance");
|
|
204
|
+
return balance ? `vercel USD ${balance.value} left` : "vercel credits unavailable";
|
|
205
|
+
}
|
|
206
|
+
|
|
160
207
|
function formatGitHubCopilotReport(lines: string[], report: UsageReport): void {
|
|
161
208
|
const quota = findGitHubCopilotQuota(report);
|
|
162
209
|
if (!quota || quota.limit === undefined || quota.remaining === undefined) {
|
|
@@ -222,6 +269,10 @@ function formatOpenRouterReport(lines: string[], report: UsageReport): void {
|
|
|
222
269
|
|
|
223
270
|
function formatOpenCodeZenReport(lines: string[], report: UsageReport): void {
|
|
224
271
|
for (const bucket of report.buckets) {
|
|
272
|
+
if (bucket.unit === "percent" && bucket.used !== undefined) {
|
|
273
|
+
lines.push(`${`${bucket.label}:`.padEnd(VALUE_COLUMN)}${formatPercentBucket(bucket)}`);
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
225
276
|
const reset = bucket.resetsAt ? ` (resets ${formatReset(bucket.resetsAt)})` : "";
|
|
226
277
|
const used = bucket.used ?? "unavailable";
|
|
227
278
|
lines.push(`${`${bucket.label}:`.padEnd(VALUE_COLUMN)}${used}% used${reset}`);
|
|
@@ -286,6 +337,113 @@ function formatKimiCodingStatusline(report: UsageReport): string | undefined {
|
|
|
286
337
|
return parts.length > 1 ? parts.join(" ") : undefined;
|
|
287
338
|
}
|
|
288
339
|
|
|
340
|
+
function formatMoonshotReport(lines: string[], report: UsageReport): void {
|
|
341
|
+
for (const metric of report.metrics) {
|
|
342
|
+
lines.push(`${`${metric.label}:`.padEnd(VALUE_COLUMN)}${metric.currency} ${metric.value}`);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function formatMoonshotStatusline(report: UsageReport): string {
|
|
347
|
+
const available = report.metrics.find((metric) => metric.id === "available-balance");
|
|
348
|
+
if (!available) return "moonshot balance unavailable";
|
|
349
|
+
return `moonshot ${available.currency ?? ""} ${available.value}`.replace(/\s+/gu, " ");
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function formatMiniMaxReport(lines: string[], report: UsageReport): void {
|
|
353
|
+
if (report.source === "minimax-account-balance") {
|
|
354
|
+
for (const metric of report.metrics) {
|
|
355
|
+
lines.push(`${`${metric.label}:`.padEnd(VALUE_COLUMN)}${metric.currency} ${metric.value}`);
|
|
356
|
+
}
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
let previousGroup: string | undefined;
|
|
360
|
+
for (const bucket of report.buckets) {
|
|
361
|
+
if (bucket.groupId !== previousGroup) lines.push(`${bucket.groupLabel ?? "Token Plan"}:`);
|
|
362
|
+
previousGroup = bucket.groupId;
|
|
363
|
+
const reset = bucket.resetsAt ? ` (resets ${formatReset(bucket.resetsAt)})` : "";
|
|
364
|
+
const value =
|
|
365
|
+
bucket.period === "unlimited"
|
|
366
|
+
? "unlimited"
|
|
367
|
+
: bucket.limit && bucket.remaining !== undefined
|
|
368
|
+
? `${bucket.remaining} of ${bucket.limit} left · ${percentRemaining(bucket)}%${reset}`
|
|
369
|
+
: "unavailable";
|
|
370
|
+
lines.push(`${`${bucket.label}:`.padEnd(VALUE_COLUMN)}${value}`);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function formatMiniMaxStatusline(report: UsageReport, model?: UsageModel): string | undefined {
|
|
375
|
+
const prefix = report.providerId === "minimax-cn" ? "minimax cn" : "minimax";
|
|
376
|
+
if (report.source === "minimax-account-balance") {
|
|
377
|
+
const available = report.metrics.find((metric) => metric.id === "available-balance");
|
|
378
|
+
return available ? `${prefix} ${available.currency} ${available.value}` : undefined;
|
|
379
|
+
}
|
|
380
|
+
const selectedGroup = selectMiniMaxGroup(report, model);
|
|
381
|
+
if (!selectedGroup) return undefined;
|
|
382
|
+
const selected = report.buckets.filter((bucket) => bucket.groupId === selectedGroup);
|
|
383
|
+
const parts = [prefix];
|
|
384
|
+
for (const bucket of selected) {
|
|
385
|
+
const fallback = bucket.id.endsWith(":weekly") ? "weekly" : "5h";
|
|
386
|
+
const window = formatWindowLabel(bucket.windowMinutes, fallback, true);
|
|
387
|
+
if (bucket.period === "unlimited") {
|
|
388
|
+
parts.push(`unlimited ${window}`);
|
|
389
|
+
continue;
|
|
390
|
+
}
|
|
391
|
+
if (!bucket.limit || bucket.remaining === undefined) continue;
|
|
392
|
+
parts.push(`${percentRemaining(bucket)}% ${window}`);
|
|
393
|
+
}
|
|
394
|
+
return parts.length > 1 ? parts.join(" ") : undefined;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function selectMiniMaxGroup(report: UsageReport, model?: UsageModel): string | undefined {
|
|
398
|
+
const groups = [
|
|
399
|
+
...new Set(
|
|
400
|
+
report.buckets
|
|
401
|
+
.map((bucket) => bucket.groupId)
|
|
402
|
+
.filter((group): group is string => group !== undefined),
|
|
403
|
+
),
|
|
404
|
+
];
|
|
405
|
+
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]
|
|
413
|
+
.map(normalizeMiniMaxModelKey)
|
|
414
|
+
.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;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function normalizeMiniMaxModelKey(value: string | undefined): string | undefined {
|
|
430
|
+
const key = value?.toLowerCase().replace(/[^a-z0-9*]+/gu, "");
|
|
431
|
+
return key && /[a-z0-9]/u.test(key) ? key : undefined;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
function wildcardKeyMatches(pattern: string, value: string): boolean {
|
|
435
|
+
if (!pattern.includes("*")) return pattern === value;
|
|
436
|
+
const segments = pattern.split("*").filter(Boolean);
|
|
437
|
+
let offset = 0;
|
|
438
|
+
for (const [index, segment] of segments.entries()) {
|
|
439
|
+
const found = value.indexOf(segment, offset);
|
|
440
|
+
if (found < 0 || (index === 0 && !pattern.startsWith("*") && found !== 0)) return false;
|
|
441
|
+
offset = found + segment.length;
|
|
442
|
+
}
|
|
443
|
+
const last = segments.at(-1);
|
|
444
|
+
return pattern.endsWith("*") || (last !== undefined && value.endsWith(last));
|
|
445
|
+
}
|
|
446
|
+
|
|
289
447
|
function formatZaiStatusline(report: UsageReport): string | undefined {
|
|
290
448
|
const selected = [
|
|
291
449
|
report.buckets.find((bucket) => bucket.id === "five-hour"),
|
|
@@ -315,8 +473,7 @@ function formatXaiReport(lines: string[], report: UsageReport): void {
|
|
|
315
473
|
if (included) {
|
|
316
474
|
let value = "unavailable";
|
|
317
475
|
if (included.unit === "percent" && included.used !== undefined) {
|
|
318
|
-
value =
|
|
319
|
-
if (included.remaining !== undefined) value += ` · ${included.remaining}% left`;
|
|
476
|
+
value = formatPercentBar(included);
|
|
320
477
|
} else if (included.used !== undefined) {
|
|
321
478
|
value = `${formatUsd(included.used)} used`;
|
|
322
479
|
if (included.limit !== undefined) value += ` of ${formatUsd(included.limit)}`;
|
|
@@ -343,12 +500,13 @@ function formatXaiReport(lines: string[], report: UsageReport): void {
|
|
|
343
500
|
|
|
344
501
|
function formatZaiReport(lines: string[], report: UsageReport): void {
|
|
345
502
|
for (const bucket of report.buckets) {
|
|
503
|
+
if (bucket.unit === "percent" && bucket.used !== undefined) {
|
|
504
|
+
lines.push(`${`${bucket.label}:`.padEnd(VALUE_COLUMN)}${formatPercentBucket(bucket)}`);
|
|
505
|
+
continue;
|
|
506
|
+
}
|
|
346
507
|
const reset = bucket.resetsAt ? ` (resets ${formatReset(bucket.resetsAt)})` : "";
|
|
347
508
|
let value = "unavailable";
|
|
348
|
-
if (bucket.
|
|
349
|
-
value = `${bucket.used}% used`;
|
|
350
|
-
if (bucket.remaining !== undefined) value += ` · ${bucket.remaining}% left`;
|
|
351
|
-
} else if (bucket.used !== undefined && bucket.limit !== undefined) {
|
|
509
|
+
if (bucket.used !== undefined && bucket.limit !== undefined) {
|
|
352
510
|
value = `${bucket.used} of ${bucket.limit} used`;
|
|
353
511
|
if (bucket.remaining !== undefined) value += ` · ${bucket.remaining} left`;
|
|
354
512
|
} else if (bucket.used !== undefined) {
|
|
@@ -483,10 +641,13 @@ function compactLimitLabel(label: string): string {
|
|
|
483
641
|
}
|
|
484
642
|
|
|
485
643
|
function formatPercentBucket(bucket: UsageBucket): string {
|
|
644
|
+
return `${formatPercentBar(bucket)}${bucket.resetsAt ? ` (resets ${formatReset(bucket.resetsAt)})` : ""}`;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
function formatPercentBar(bucket: UsageBucket): string {
|
|
486
648
|
const remaining = clampPercent(bucket.remaining ?? 0);
|
|
487
649
|
const filled = Math.round((remaining / 100) * BAR_SEGMENTS);
|
|
488
|
-
|
|
489
|
-
return `[${"█".repeat(filled)}${"░".repeat(BAR_SEGMENTS - filled)}] ${remaining.toFixed(0)}% left${reset}`;
|
|
650
|
+
return `[${"█".repeat(filled)}${"░".repeat(BAR_SEGMENTS - filled)}] ${remaining.toFixed(0)}% left`;
|
|
490
651
|
}
|
|
491
652
|
|
|
492
653
|
function formatWindowLabel(
|
package/src/index.ts
CHANGED
|
@@ -32,6 +32,7 @@ export {
|
|
|
32
32
|
UsageCache,
|
|
33
33
|
} from "./core.js";
|
|
34
34
|
export { formatProviderStates, formatUsageReport, formatUsageStatusline } from "./format.js";
|
|
35
|
+
export { normalizeBasetenBillingUsagePayload } from "./providers/baseten.js";
|
|
35
36
|
export { normalizeCodexBackendPayload } from "./providers/codex.js";
|
|
36
37
|
export { normalizeDeepSeekBalancePayload } from "./providers/deepseek.js";
|
|
37
38
|
export {
|
|
@@ -40,10 +41,18 @@ export {
|
|
|
40
41
|
} from "./providers/fireworks.js";
|
|
41
42
|
export { normalizeGitHubCopilotUsagePayload } from "./providers/github-copilot.js";
|
|
42
43
|
export { normalizeKimiCodingUsagePayload } from "./providers/kimi-coding.js";
|
|
44
|
+
export type { MiniMaxProviderId, MiniMaxUsageKind } from "./providers/minimax.js";
|
|
45
|
+
export {
|
|
46
|
+
miniMaxUsageKind,
|
|
47
|
+
normalizeMiniMaxUsagePayload,
|
|
48
|
+
} from "./providers/minimax.js";
|
|
49
|
+
export type { MoonshotProviderId } from "./providers/moonshot.js";
|
|
50
|
+
export { normalizeMoonshotBalancePayload } from "./providers/moonshot.js";
|
|
43
51
|
export { normalizeOpenCodeZenPayload } from "./providers/opencode-zen.js";
|
|
44
52
|
export { normalizeOpenRouterKeyPayload } from "./providers/openrouter.js";
|
|
53
|
+
export { normalizeVercelAIGatewayCreditsPayload } from "./providers/vercel-ai-gateway.js";
|
|
45
54
|
export { normalizeXaiBillingPayload } from "./providers/xai.js";
|
|
46
|
-
export { normalizeZaiQuotaPayload } from "./providers/zai.js";
|
|
55
|
+
export { normalizeZaiQuotaPayload, normalizeZaiSubscriptionPayload } from "./providers/zai.js";
|
|
47
56
|
export {
|
|
48
57
|
adapterForProvider,
|
|
49
58
|
isStaleExtensionContextError,
|
|
@@ -67,10 +76,13 @@ export {
|
|
|
67
76
|
usageSettingsPath,
|
|
68
77
|
} from "./settings.js";
|
|
69
78
|
export type {
|
|
79
|
+
BasetenBillingUsagePayload,
|
|
70
80
|
DeepSeekBalancePayload,
|
|
71
81
|
FireworksAccountsPayload,
|
|
72
82
|
FireworksBillingSummaryPayload,
|
|
73
83
|
KimiCodingUsagePayload,
|
|
84
|
+
MiniMaxUsagePayload,
|
|
85
|
+
MoonshotBalancePayload,
|
|
74
86
|
ProviderUsageState,
|
|
75
87
|
ResolvedUsageAuth,
|
|
76
88
|
UsageBucket,
|
|
@@ -83,6 +95,7 @@ export type {
|
|
|
83
95
|
UsageSemantics,
|
|
84
96
|
UsageSemanticsKind,
|
|
85
97
|
UsageUnit,
|
|
98
|
+
VercelAIGatewayCreditsPayload,
|
|
86
99
|
XaiBillingPayload,
|
|
87
100
|
XaiUserPayload,
|
|
88
101
|
} from "./types.js";
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { BasetenBillingUsagePayload, UsageMetric, UsageReport } from "../types.js";
|
|
2
|
+
|
|
3
|
+
const DECIMAL_AMOUNT = /^(?:0|[1-9]\d*)(?:\.\d+)?$/u;
|
|
4
|
+
|
|
5
|
+
export function normalizeBasetenBillingUsagePayload(
|
|
6
|
+
payload: BasetenBillingUsagePayload,
|
|
7
|
+
capturedAt: number,
|
|
8
|
+
): UsageReport {
|
|
9
|
+
if (payload.model_apis_usage === undefined || payload.model_apis_usage === null) {
|
|
10
|
+
return report(capturedAt, [], ["Baseten returned no Model APIs usage for the last 30 days."]);
|
|
11
|
+
}
|
|
12
|
+
const usage = asObject(payload.model_apis_usage);
|
|
13
|
+
if (!usage) throw new Error("Baseten Model APIs usage was not an object.");
|
|
14
|
+
const metrics: UsageMetric[] = [
|
|
15
|
+
metric("gross-usage", "Gross usage", usage.total),
|
|
16
|
+
metric("credits-used", "Credits used", usage.credits_used),
|
|
17
|
+
metric("net-subtotal", "Net subtotal", usage.subtotal),
|
|
18
|
+
];
|
|
19
|
+
return report(capturedAt, metrics);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function report(capturedAt: number, metrics: UsageMetric[], notes?: string[]): UsageReport {
|
|
23
|
+
return {
|
|
24
|
+
providerId: "baseten",
|
|
25
|
+
providerName: "Baseten",
|
|
26
|
+
capturedAt,
|
|
27
|
+
source: "baseten-billing-usage-summary",
|
|
28
|
+
semantics: { kind: "api-key", label: "Organization Model APIs spend" },
|
|
29
|
+
buckets: [],
|
|
30
|
+
metrics,
|
|
31
|
+
...(notes ? { notes } : {}),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function metric(id: string, label: string, value: unknown): UsageMetric {
|
|
36
|
+
const amount = decimalAmount(value, label);
|
|
37
|
+
return { id, label, value: amount, unit: "currency", currency: "USD" };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function decimalAmount(value: unknown, label: string): string {
|
|
41
|
+
const normalized = typeof value === "number" && Number.isFinite(value) ? String(value) : value;
|
|
42
|
+
if (
|
|
43
|
+
typeof normalized !== "string" ||
|
|
44
|
+
normalized.length > 64 ||
|
|
45
|
+
!DECIMAL_AMOUNT.test(normalized)
|
|
46
|
+
) {
|
|
47
|
+
throw new Error(`Baseten ${label.toLowerCase()} was not a valid nonnegative amount.`);
|
|
48
|
+
}
|
|
49
|
+
return normalized;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function asObject(value: unknown): Record<string, unknown> | undefined {
|
|
53
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
54
|
+
return value as Record<string, unknown>;
|
|
55
|
+
}
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
import { sanitizeDisplayText } from "../core.js";
|
|
2
|
+
import type { MiniMaxUsagePayload, UsageBucket, UsageMetric, UsageReport } from "../types.js";
|
|
3
|
+
|
|
4
|
+
export type MiniMaxProviderId = "minimax" | "minimax-cn";
|
|
5
|
+
export type MiniMaxUsageKind = "token-plan" | "account-balance";
|
|
6
|
+
|
|
7
|
+
const PROVIDERS = {
|
|
8
|
+
minimax: { name: "MiniMax", currency: "USD" },
|
|
9
|
+
"minimax-cn": { name: "MiniMax CN", currency: "CNY" },
|
|
10
|
+
} as const;
|
|
11
|
+
const DECIMAL_AMOUNT = /^-?(?:0|[1-9]\d*)(?:\.\d+)?$/u;
|
|
12
|
+
const PERCENT_TOLERANCE = 1;
|
|
13
|
+
|
|
14
|
+
export function miniMaxUsageKind(apiKey: string): MiniMaxUsageKind {
|
|
15
|
+
return apiKey.startsWith("sk-api-") ? "account-balance" : "token-plan";
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function normalizeMiniMaxUsagePayload(
|
|
19
|
+
providerId: MiniMaxProviderId,
|
|
20
|
+
kind: MiniMaxUsageKind,
|
|
21
|
+
payload: MiniMaxUsagePayload,
|
|
22
|
+
capturedAt: number,
|
|
23
|
+
): UsageReport {
|
|
24
|
+
return kind === "account-balance"
|
|
25
|
+
? normalizeBalance(providerId, payload, capturedAt)
|
|
26
|
+
: normalizeTokenPlan(providerId, payload, capturedAt);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function normalizeBalance(
|
|
30
|
+
providerId: MiniMaxProviderId,
|
|
31
|
+
payload: MiniMaxUsagePayload,
|
|
32
|
+
capturedAt: number,
|
|
33
|
+
): UsageReport {
|
|
34
|
+
assertSuccess(payload);
|
|
35
|
+
const provider = PROVIDERS[providerId];
|
|
36
|
+
const metrics: UsageMetric[] = [
|
|
37
|
+
balanceMetric(
|
|
38
|
+
"available-balance",
|
|
39
|
+
"Available balance",
|
|
40
|
+
payload.available_amount,
|
|
41
|
+
provider.currency,
|
|
42
|
+
),
|
|
43
|
+
balanceMetric("cash-balance", "Cash balance", payload.cash_balance, provider.currency, true),
|
|
44
|
+
balanceMetric("voucher-balance", "Voucher balance", payload.voucher_balance, provider.currency),
|
|
45
|
+
balanceMetric("credit-balance", "Credit balance", payload.credit_balance, provider.currency),
|
|
46
|
+
balanceMetric("owed-amount", "Owed amount", payload.owed_amount, provider.currency),
|
|
47
|
+
];
|
|
48
|
+
return {
|
|
49
|
+
providerId,
|
|
50
|
+
providerName: provider.name,
|
|
51
|
+
capturedAt,
|
|
52
|
+
source: "minimax-account-balance",
|
|
53
|
+
semantics: { kind: "api-key", label: "MiniMax pay-as-you-go account balance" },
|
|
54
|
+
buckets: [],
|
|
55
|
+
metrics,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function normalizeTokenPlan(
|
|
60
|
+
providerId: MiniMaxProviderId,
|
|
61
|
+
payload: MiniMaxUsagePayload,
|
|
62
|
+
capturedAt: number,
|
|
63
|
+
): UsageReport {
|
|
64
|
+
assertSuccess(payload);
|
|
65
|
+
if (!Array.isArray(payload.model_remains) || payload.model_remains.length === 0) {
|
|
66
|
+
throw new Error("MiniMax Token Plan returned no quota rows.");
|
|
67
|
+
}
|
|
68
|
+
const provider = PROVIDERS[providerId];
|
|
69
|
+
const buckets: UsageBucket[] = [];
|
|
70
|
+
const groups = new Set<string>();
|
|
71
|
+
for (const [index, raw] of payload.model_remains.entries()) {
|
|
72
|
+
const row = asObject(raw);
|
|
73
|
+
if (!row) throw new Error("MiniMax Token Plan quota row was not an object.");
|
|
74
|
+
const groupLabel = safeLabel(row.model_name, `Quota ${index + 1}`);
|
|
75
|
+
const groupId = uniqueGroupId(groupLabel, index, groups);
|
|
76
|
+
buckets.push(
|
|
77
|
+
normalizeWindow(row, {
|
|
78
|
+
id: `${groupId}:interval`,
|
|
79
|
+
label: "Rolling window",
|
|
80
|
+
groupId,
|
|
81
|
+
groupLabel,
|
|
82
|
+
countField: "current_interval_usage_count",
|
|
83
|
+
totalField: "current_interval_total_count",
|
|
84
|
+
percentField: "current_interval_remaining_percent",
|
|
85
|
+
statusField: "current_interval_status",
|
|
86
|
+
startField: "start_time",
|
|
87
|
+
endField: "end_time",
|
|
88
|
+
}),
|
|
89
|
+
normalizeWindow(row, {
|
|
90
|
+
id: `${groupId}:weekly`,
|
|
91
|
+
label: "Weekly window",
|
|
92
|
+
groupId,
|
|
93
|
+
groupLabel,
|
|
94
|
+
countField: "current_weekly_usage_count",
|
|
95
|
+
totalField: "current_weekly_total_count",
|
|
96
|
+
percentField: "current_weekly_remaining_percent",
|
|
97
|
+
statusField: "current_weekly_status",
|
|
98
|
+
startField: "weekly_start_time",
|
|
99
|
+
endField: "weekly_end_time",
|
|
100
|
+
boostPermille: row.weekly_boost_permille,
|
|
101
|
+
}),
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
return {
|
|
105
|
+
providerId,
|
|
106
|
+
providerName: provider.name,
|
|
107
|
+
capturedAt,
|
|
108
|
+
source: "minimax-token-plan",
|
|
109
|
+
semantics: { kind: "consumer-subscription", label: "MiniMax Token Plan quota" },
|
|
110
|
+
buckets,
|
|
111
|
+
metrics: [],
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
type WindowFields = {
|
|
116
|
+
id: string;
|
|
117
|
+
label: string;
|
|
118
|
+
groupId: string;
|
|
119
|
+
groupLabel: string;
|
|
120
|
+
countField: string;
|
|
121
|
+
totalField: string;
|
|
122
|
+
percentField: string;
|
|
123
|
+
statusField: string;
|
|
124
|
+
startField: string;
|
|
125
|
+
endField: string;
|
|
126
|
+
boostPermille?: unknown;
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
function normalizeWindow(row: Record<string, unknown>, fields: WindowFields): UsageBucket {
|
|
130
|
+
const status = optionalInteger(row[fields.statusField], fields.statusField);
|
|
131
|
+
if (status !== undefined && ![1, 2, 3].includes(status)) {
|
|
132
|
+
throw new Error(`MiniMax Token Plan ${fields.label} status was unsupported.`);
|
|
133
|
+
}
|
|
134
|
+
const percent = optionalPercent(row[fields.percentField], fields.percentField);
|
|
135
|
+
validateBoost(fields.boostPermille);
|
|
136
|
+
const start = timestamp(row[fields.startField], fields.startField);
|
|
137
|
+
const end = timestamp(row[fields.endField], fields.endField);
|
|
138
|
+
if (end < start) throw new Error(`MiniMax Token Plan ${fields.label} timestamps were reversed.`);
|
|
139
|
+
const resetsAt = Math.floor(end / 1_000);
|
|
140
|
+
const windowMinutes = Math.max(1, Math.round((end - start) / 60_000));
|
|
141
|
+
if (status === 3) {
|
|
142
|
+
return {
|
|
143
|
+
id: fields.id,
|
|
144
|
+
label: fields.label,
|
|
145
|
+
groupId: fields.groupId,
|
|
146
|
+
groupLabel: fields.groupLabel,
|
|
147
|
+
remaining: 100,
|
|
148
|
+
unit: "percent",
|
|
149
|
+
period: "unlimited",
|
|
150
|
+
windowMinutes,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
const total = nonnegativeInteger(row[fields.totalField], fields.totalField);
|
|
154
|
+
const count = nonnegativeInteger(row[fields.countField], fields.countField);
|
|
155
|
+
const resolved = resolveQuotaCounts(count, total, percent);
|
|
156
|
+
if (!resolved) throw new Error(`MiniMax Token Plan ${fields.label} counts were inconsistent.`);
|
|
157
|
+
return {
|
|
158
|
+
id: fields.id,
|
|
159
|
+
label: fields.label,
|
|
160
|
+
groupId: fields.groupId,
|
|
161
|
+
groupLabel: fields.groupLabel,
|
|
162
|
+
...resolved,
|
|
163
|
+
unit: "count",
|
|
164
|
+
windowMinutes,
|
|
165
|
+
resetsAt,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function resolveQuotaCounts(
|
|
170
|
+
reportedCount: number,
|
|
171
|
+
total: number,
|
|
172
|
+
remainingPercent: number | undefined,
|
|
173
|
+
): Pick<UsageBucket, "used" | "remaining" | "limit"> | undefined {
|
|
174
|
+
if (total <= 0 || reportedCount > total) return undefined;
|
|
175
|
+
let remaining = reportedCount;
|
|
176
|
+
if (remainingPercent !== undefined) {
|
|
177
|
+
const asRemaining = (reportedCount / total) * 100;
|
|
178
|
+
const asUsed = ((total - reportedCount) / total) * 100;
|
|
179
|
+
const remainingDistance = Math.abs(asRemaining - remainingPercent);
|
|
180
|
+
const usedDistance = Math.abs(asUsed - remainingPercent);
|
|
181
|
+
if (Math.min(remainingDistance, usedDistance) > PERCENT_TOLERANCE) return undefined;
|
|
182
|
+
if (usedDistance < remainingDistance) remaining = total - reportedCount;
|
|
183
|
+
}
|
|
184
|
+
return { used: total - remaining, remaining, limit: total };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function assertSuccess(payload: MiniMaxUsagePayload): void {
|
|
188
|
+
const base = asObject(payload.base_resp);
|
|
189
|
+
if (base?.status_code !== 0) {
|
|
190
|
+
throw new Error("MiniMax usage response did not report success.");
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function balanceMetric(
|
|
195
|
+
id: string,
|
|
196
|
+
label: string,
|
|
197
|
+
value: unknown,
|
|
198
|
+
currency: "CNY" | "USD",
|
|
199
|
+
allowNegative = false,
|
|
200
|
+
): UsageMetric {
|
|
201
|
+
if (
|
|
202
|
+
typeof value !== "string" ||
|
|
203
|
+
value.length > 64 ||
|
|
204
|
+
!DECIMAL_AMOUNT.test(value) ||
|
|
205
|
+
(!allowNegative && value.startsWith("-"))
|
|
206
|
+
) {
|
|
207
|
+
throw new Error(`MiniMax ${label.toLowerCase()} was not a valid amount.`);
|
|
208
|
+
}
|
|
209
|
+
return { id, label, value, unit: "currency", currency };
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function asObject(value: unknown): Record<string, unknown> | undefined {
|
|
213
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
214
|
+
return value as Record<string, unknown>;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function safeLabel(value: unknown, fallback: string): string {
|
|
218
|
+
if (typeof value !== "string") throw new Error("MiniMax Token Plan model name was not a string.");
|
|
219
|
+
return sanitizeDisplayText(value, 80) || fallback;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function uniqueGroupId(label: string, index: number, groups: Set<string>): string {
|
|
223
|
+
const base =
|
|
224
|
+
label
|
|
225
|
+
.toLowerCase()
|
|
226
|
+
.replace(/[^a-z0-9]+/gu, "-")
|
|
227
|
+
.replace(/^-|-$/gu, "") || "quota";
|
|
228
|
+
const id = groups.has(base) ? `${base}-${index + 1}` : base;
|
|
229
|
+
groups.add(id);
|
|
230
|
+
return id;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function nonnegativeInteger(value: unknown, field: string): number {
|
|
234
|
+
if (!Number.isSafeInteger(value) || (value as number) < 0) {
|
|
235
|
+
throw new Error(`MiniMax Token Plan ${field} was not a nonnegative safe integer.`);
|
|
236
|
+
}
|
|
237
|
+
return value as number;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function optionalInteger(value: unknown, field: string): number | undefined {
|
|
241
|
+
if (value === undefined || value === null) return undefined;
|
|
242
|
+
return nonnegativeInteger(value, field);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function optionalPercent(value: unknown, field: string): number | undefined {
|
|
246
|
+
if (value === undefined || value === null) return undefined;
|
|
247
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 0 || value > 100) {
|
|
248
|
+
throw new Error(`MiniMax Token Plan ${field} was not a percentage.`);
|
|
249
|
+
}
|
|
250
|
+
return value;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function validateBoost(boost: unknown): void {
|
|
254
|
+
if (boost === undefined || boost === null) return;
|
|
255
|
+
const permille = nonnegativeInteger(boost, "weekly_boost_permille");
|
|
256
|
+
if (permille > 10_000) throw new Error("MiniMax Token Plan weekly boost was unreasonable.");
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function timestamp(value: unknown, field: string): number {
|
|
260
|
+
if (!Number.isSafeInteger(value) || (value as number) <= 0) {
|
|
261
|
+
throw new Error(`MiniMax Token Plan ${field} was not a valid timestamp.`);
|
|
262
|
+
}
|
|
263
|
+
return value as number;
|
|
264
|
+
}
|