@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/README.md +118 -2
- package/dist/index.ts +1247 -240
- package/dist/index.ts.map +4 -4
- package/package.json +8 -1
- package/src/codex-fast-runtime.ts +34 -26
- package/src/format.ts +189 -3
- package/src/index.ts +20 -0
- package/src/providers/baseten.ts +55 -0
- package/src/providers/fireworks.ts +198 -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/query.ts +355 -9
- package/src/settings.ts +16 -1
- package/src/types.ts +44 -0
- package/src/usage-settings-ui.ts +125 -33
- package/src/usage.ts +55 -23
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@narumitw/pi-usage",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.59.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",
|
|
@@ -13,9 +13,16 @@
|
|
|
13
13
|
"quota",
|
|
14
14
|
"balance",
|
|
15
15
|
"deepseek",
|
|
16
|
+
"fireworks",
|
|
17
|
+
"baseten",
|
|
18
|
+
"vercel-ai-gateway",
|
|
16
19
|
"codex",
|
|
17
20
|
"kimi",
|
|
18
21
|
"kimi-coding",
|
|
22
|
+
"moonshot",
|
|
23
|
+
"moonshotai",
|
|
24
|
+
"minimax",
|
|
25
|
+
"token-plan",
|
|
19
26
|
"copilot",
|
|
20
27
|
"openrouter",
|
|
21
28
|
"opencode",
|
|
@@ -24,6 +24,7 @@ export function registerCodexFastMode(
|
|
|
24
24
|
pi: ExtensionAPI,
|
|
25
25
|
settingsRuntime: UsageSettingsRuntime,
|
|
26
26
|
refreshStatus: (ctx: ExtensionContext) => void,
|
|
27
|
+
options: { registerSessionStart?: boolean } = {},
|
|
27
28
|
) {
|
|
28
29
|
let sessionController = new AbortController();
|
|
29
30
|
let generation = 0;
|
|
@@ -95,41 +96,47 @@ export function registerCodexFastMode(
|
|
|
95
96
|
},
|
|
96
97
|
});
|
|
97
98
|
|
|
98
|
-
|
|
99
|
+
const prepareSession = (ctx: ExtensionContext): Promise<void> => {
|
|
100
|
+
const sessionId = ctx.sessionManager.getSessionId();
|
|
99
101
|
generation += 1;
|
|
100
102
|
sessionController.abort();
|
|
101
103
|
pendingFastRequests.clear();
|
|
102
104
|
sessionController = new AbortController();
|
|
103
105
|
const ownerGeneration = generation;
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
106
|
+
return (async () => {
|
|
107
|
+
let state: Readonly<UsageSettingsState>;
|
|
108
|
+
try {
|
|
109
|
+
state = await settingsRuntime.reload(sessionController.signal);
|
|
110
|
+
} catch (error) {
|
|
111
|
+
if (sessionController.signal.aborted || ownerGeneration !== generation) return;
|
|
112
|
+
if (ctx.hasUI) {
|
|
113
|
+
ctx.ui.notify(
|
|
114
|
+
`Could not load pi-usage.json; using defaults. ${errorMessage(error)}`,
|
|
115
|
+
"warning",
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
if (
|
|
121
|
+
sessionController.signal.aborted ||
|
|
122
|
+
ownerGeneration !== generation ||
|
|
123
|
+
ctx.sessionManager.getSessionId() !== sessionId
|
|
124
|
+
) {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
if (ctx.hasUI && state.kind === "invalid") {
|
|
111
128
|
ctx.ui.notify(
|
|
112
|
-
`
|
|
129
|
+
`Invalid pi-usage.json; using defaults without overwriting it. ${state.issue}`,
|
|
113
130
|
"warning",
|
|
114
131
|
);
|
|
115
132
|
}
|
|
116
|
-
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
return;
|
|
124
|
-
}
|
|
125
|
-
if (ctx.hasUI && state.kind === "invalid") {
|
|
126
|
-
ctx.ui.notify(
|
|
127
|
-
`Invalid pi-usage.json; using defaults without overwriting it. ${state.issue}`,
|
|
128
|
-
"warning",
|
|
129
|
-
);
|
|
130
|
-
}
|
|
131
|
-
refreshStatus(ctx);
|
|
132
|
-
});
|
|
133
|
+
refreshStatus(ctx);
|
|
134
|
+
})();
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
if (options.registerSessionStart !== false) {
|
|
138
|
+
pi.on("session_start", async (_event, ctx) => prepareSession(ctx));
|
|
139
|
+
}
|
|
133
140
|
|
|
134
141
|
pi.on("before_provider_request", (event, ctx) => {
|
|
135
142
|
const rewritten = rewriteCodexFastPayload(
|
|
@@ -164,6 +171,7 @@ export function registerCodexFastMode(
|
|
|
164
171
|
});
|
|
165
172
|
|
|
166
173
|
return {
|
|
174
|
+
prepareSession,
|
|
167
175
|
availability(model: PiModel | undefined) {
|
|
168
176
|
return codexFastAvailability(model, settingsRuntime.get().settings.codexFastMode);
|
|
169
177
|
},
|
package/src/format.ts
CHANGED
|
@@ -12,18 +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 === "
|
|
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`;
|
|
16
30
|
const lines = [`${title} · ${stateLabel}`];
|
|
17
31
|
if (report.accountLabel) lines.push(`Account: ${report.accountLabel}`);
|
|
18
32
|
lines.push(`Semantics: ${report.semantics.label}`, "");
|
|
19
33
|
|
|
20
|
-
if (report.providerId === "
|
|
34
|
+
if (report.providerId === "baseten") formatBasetenReport(lines, report);
|
|
35
|
+
else if (report.providerId === "openai-codex") formatCodexReport(lines, report);
|
|
21
36
|
else if (report.providerId === "deepseek") formatDeepSeekReport(lines, report);
|
|
37
|
+
else if (report.providerId === "fireworks") formatFireworksReport(lines, report);
|
|
38
|
+
else if (report.providerId === "vercel-ai-gateway") formatVercelAIGatewayReport(lines, report);
|
|
22
39
|
else if (report.providerId === "github-copilot") formatGitHubCopilotReport(lines, report);
|
|
23
40
|
else if (report.providerId === "openrouter") formatOpenRouterReport(lines, report);
|
|
24
41
|
else if (report.providerId === "opencode-go") formatOpenCodeZenReport(lines, report);
|
|
25
42
|
else if (report.providerId === "kimi-coding") formatKimiCodingReport(lines, report);
|
|
26
|
-
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);
|
|
27
48
|
else if (report.providerId === "zai" || report.providerId === "zai-coding-cn") {
|
|
28
49
|
formatZaiReport(lines, report);
|
|
29
50
|
} else formatGenericReport(lines, report);
|
|
@@ -40,10 +61,13 @@ export function formatUsageStatusline(
|
|
|
40
61
|
now = Date.now(),
|
|
41
62
|
showCodexResetCountdown = true,
|
|
42
63
|
): string | undefined {
|
|
64
|
+
if (report.providerId === "baseten") return formatBasetenStatusline(report);
|
|
43
65
|
if (report.providerId === "openai-codex") {
|
|
44
66
|
return formatCodexStatusline(report, model, now, showCodexResetCountdown);
|
|
45
67
|
}
|
|
46
68
|
if (report.providerId === "deepseek") return formatDeepSeekStatusline(report);
|
|
69
|
+
if (report.providerId === "fireworks") return formatFireworksStatusline(report);
|
|
70
|
+
if (report.providerId === "vercel-ai-gateway") return formatVercelAIGatewayStatusline(report);
|
|
47
71
|
if (report.providerId === "github-copilot") return formatGitHubCopilotStatusline(report);
|
|
48
72
|
if (report.providerId === "openrouter") {
|
|
49
73
|
const limit = report.buckets.find((bucket) => bucket.id === "key-limit");
|
|
@@ -53,6 +77,12 @@ export function formatUsageStatusline(
|
|
|
53
77
|
}
|
|
54
78
|
if (report.providerId === "opencode-go") return formatOpenCodeZenStatusline(report);
|
|
55
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
|
+
}
|
|
56
86
|
if (report.providerId === "zai" || report.providerId === "zai-coding-cn") {
|
|
57
87
|
return formatZaiStatusline(report);
|
|
58
88
|
}
|
|
@@ -75,6 +105,18 @@ export function formatProviderStates(states: readonly ProviderUsageState[]): str
|
|
|
75
105
|
.join("\n\n");
|
|
76
106
|
}
|
|
77
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
|
+
|
|
78
120
|
function formatCodexReport(lines: string[], report: UsageReport): void {
|
|
79
121
|
let previousGroup: string | undefined;
|
|
80
122
|
for (const bucket of report.buckets) {
|
|
@@ -125,6 +167,43 @@ function formatDeepSeekStatusline(report: UsageReport): string {
|
|
|
125
167
|
return totals.length > 0 ? `deepseek ${totals.join(" · ")}` : "deepseek balance unavailable";
|
|
126
168
|
}
|
|
127
169
|
|
|
170
|
+
function formatFireworksReport(lines: string[], report: UsageReport): void {
|
|
171
|
+
lines.push(`${"Spend window:".padEnd(VALUE_COLUMN)}Last 30 days (rated)`);
|
|
172
|
+
for (const currency of fireworksCurrencies(report)) {
|
|
173
|
+
lines.push("", `${currency} rated spend:`);
|
|
174
|
+
for (const metric of report.metrics) {
|
|
175
|
+
if (metric.currency !== currency) continue;
|
|
176
|
+
lines.push(`${`${metric.label}:`.padEnd(VALUE_COLUMN)}${currency} ${metric.value}`);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function formatFireworksStatusline(report: UsageReport): string {
|
|
182
|
+
const totals = report.metrics.filter((metric) => metric.id.endsWith("-total"));
|
|
183
|
+
if (totals.length === 0) return "fireworks no rated usage";
|
|
184
|
+
return `fireworks ${totals.map((metric) => `${metric.currency} ${metric.value}`).join(" · ")}`;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function fireworksCurrencies(report: UsageReport): string[] {
|
|
188
|
+
const currencies: string[] = [];
|
|
189
|
+
for (const metric of report.metrics) {
|
|
190
|
+
if (!metric.currency || currencies.includes(metric.currency)) continue;
|
|
191
|
+
currencies.push(metric.currency);
|
|
192
|
+
}
|
|
193
|
+
return currencies;
|
|
194
|
+
}
|
|
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
|
+
|
|
128
207
|
function formatGitHubCopilotReport(lines: string[], report: UsageReport): void {
|
|
129
208
|
const quota = findGitHubCopilotQuota(report);
|
|
130
209
|
if (!quota || quota.limit === undefined || quota.remaining === undefined) {
|
|
@@ -254,6 +333,113 @@ function formatKimiCodingStatusline(report: UsageReport): string | undefined {
|
|
|
254
333
|
return parts.length > 1 ? parts.join(" ") : undefined;
|
|
255
334
|
}
|
|
256
335
|
|
|
336
|
+
function formatMoonshotReport(lines: string[], report: UsageReport): void {
|
|
337
|
+
for (const metric of report.metrics) {
|
|
338
|
+
lines.push(`${`${metric.label}:`.padEnd(VALUE_COLUMN)}${metric.currency} ${metric.value}`);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function formatMoonshotStatusline(report: UsageReport): string {
|
|
343
|
+
const available = report.metrics.find((metric) => metric.id === "available-balance");
|
|
344
|
+
if (!available) return "moonshot balance unavailable";
|
|
345
|
+
return `moonshot ${available.currency ?? ""} ${available.value}`.replace(/\s+/gu, " ");
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function formatMiniMaxReport(lines: string[], report: UsageReport): void {
|
|
349
|
+
if (report.source === "minimax-account-balance") {
|
|
350
|
+
for (const metric of report.metrics) {
|
|
351
|
+
lines.push(`${`${metric.label}:`.padEnd(VALUE_COLUMN)}${metric.currency} ${metric.value}`);
|
|
352
|
+
}
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
let previousGroup: string | undefined;
|
|
356
|
+
for (const bucket of report.buckets) {
|
|
357
|
+
if (bucket.groupId !== previousGroup) lines.push(`${bucket.groupLabel ?? "Token Plan"}:`);
|
|
358
|
+
previousGroup = bucket.groupId;
|
|
359
|
+
const reset = bucket.resetsAt ? ` (resets ${formatReset(bucket.resetsAt)})` : "";
|
|
360
|
+
const value =
|
|
361
|
+
bucket.period === "unlimited"
|
|
362
|
+
? "unlimited"
|
|
363
|
+
: bucket.limit && bucket.remaining !== undefined
|
|
364
|
+
? `${bucket.remaining} of ${bucket.limit} left · ${percentRemaining(bucket)}%${reset}`
|
|
365
|
+
: "unavailable";
|
|
366
|
+
lines.push(`${`${bucket.label}:`.padEnd(VALUE_COLUMN)}${value}`);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function formatMiniMaxStatusline(report: UsageReport, model?: UsageModel): string | undefined {
|
|
371
|
+
const prefix = report.providerId === "minimax-cn" ? "minimax cn" : "minimax";
|
|
372
|
+
if (report.source === "minimax-account-balance") {
|
|
373
|
+
const available = report.metrics.find((metric) => metric.id === "available-balance");
|
|
374
|
+
return available ? `${prefix} ${available.currency} ${available.value}` : undefined;
|
|
375
|
+
}
|
|
376
|
+
const selectedGroup = selectMiniMaxGroup(report, model);
|
|
377
|
+
if (!selectedGroup) return undefined;
|
|
378
|
+
const selected = report.buckets.filter((bucket) => bucket.groupId === selectedGroup);
|
|
379
|
+
const parts = [prefix];
|
|
380
|
+
for (const bucket of selected) {
|
|
381
|
+
const fallback = bucket.id.endsWith(":weekly") ? "weekly" : "5h";
|
|
382
|
+
const window = formatWindowLabel(bucket.windowMinutes, fallback, true);
|
|
383
|
+
if (bucket.period === "unlimited") {
|
|
384
|
+
parts.push(`unlimited ${window}`);
|
|
385
|
+
continue;
|
|
386
|
+
}
|
|
387
|
+
if (!bucket.limit || bucket.remaining === undefined) continue;
|
|
388
|
+
parts.push(`${percentRemaining(bucket)}% ${window}`);
|
|
389
|
+
}
|
|
390
|
+
return parts.length > 1 ? parts.join(" ") : undefined;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function selectMiniMaxGroup(report: UsageReport, model?: UsageModel): string | undefined {
|
|
394
|
+
const groups = [
|
|
395
|
+
...new Set(
|
|
396
|
+
report.buckets
|
|
397
|
+
.map((bucket) => bucket.groupId)
|
|
398
|
+
.filter((group): group is string => group !== undefined),
|
|
399
|
+
),
|
|
400
|
+
];
|
|
401
|
+
if (groups.length <= 1) return groups[0];
|
|
402
|
+
if (model?.provider !== report.providerId) return undefined;
|
|
403
|
+
const modelKeys = [model.id, model.name]
|
|
404
|
+
.map(normalizeMiniMaxModelKey)
|
|
405
|
+
.filter((key): key is string => key !== undefined);
|
|
406
|
+
const candidates = groups.map((group) => {
|
|
407
|
+
const bucket = report.buckets.find((candidate) => candidate.groupId === group);
|
|
408
|
+
const patterns = [bucket?.groupLabel, ...(bucket?.modelKeys ?? []), group]
|
|
409
|
+
.map(normalizeMiniMaxModelKey)
|
|
410
|
+
.filter((key): key is string => key !== undefined);
|
|
411
|
+
return { group, patterns };
|
|
412
|
+
});
|
|
413
|
+
const exact = candidates.find(({ patterns }) =>
|
|
414
|
+
patterns.some((pattern) => !pattern.includes("*") && modelKeys.includes(pattern)),
|
|
415
|
+
);
|
|
416
|
+
if (exact) return exact.group;
|
|
417
|
+
return candidates.find(({ patterns }) =>
|
|
418
|
+
patterns.some(
|
|
419
|
+
(pattern) =>
|
|
420
|
+
pattern.includes("*") && modelKeys.some((key) => wildcardKeyMatches(pattern, key)),
|
|
421
|
+
),
|
|
422
|
+
)?.group;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function normalizeMiniMaxModelKey(value: string | undefined): string | undefined {
|
|
426
|
+
const key = value?.toLowerCase().replace(/[^a-z0-9*]+/gu, "");
|
|
427
|
+
return key && /[a-z0-9]/u.test(key) ? key : undefined;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function wildcardKeyMatches(pattern: string, value: string): boolean {
|
|
431
|
+
if (!pattern.includes("*")) return pattern === value;
|
|
432
|
+
const segments = pattern.split("*").filter(Boolean);
|
|
433
|
+
let offset = 0;
|
|
434
|
+
for (const [index, segment] of segments.entries()) {
|
|
435
|
+
const found = value.indexOf(segment, offset);
|
|
436
|
+
if (found < 0 || (index === 0 && !pattern.startsWith("*") && found !== 0)) return false;
|
|
437
|
+
offset = found + segment.length;
|
|
438
|
+
}
|
|
439
|
+
const last = segments.at(-1);
|
|
440
|
+
return pattern.endsWith("*") || (last !== undefined && value.endsWith(last));
|
|
441
|
+
}
|
|
442
|
+
|
|
257
443
|
function formatZaiStatusline(report: UsageReport): string | undefined {
|
|
258
444
|
const selected = [
|
|
259
445
|
report.buckets.find((bucket) => bucket.id === "five-hour"),
|
package/src/index.ts
CHANGED
|
@@ -32,12 +32,25 @@ 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";
|
|
38
|
+
export {
|
|
39
|
+
normalizeFireworksAccountsPayload,
|
|
40
|
+
normalizeFireworksBillingSummaryPayload,
|
|
41
|
+
} from "./providers/fireworks.js";
|
|
37
42
|
export { normalizeGitHubCopilotUsagePayload } from "./providers/github-copilot.js";
|
|
38
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";
|
|
39
51
|
export { normalizeOpenCodeZenPayload } from "./providers/opencode-zen.js";
|
|
40
52
|
export { normalizeOpenRouterKeyPayload } from "./providers/openrouter.js";
|
|
53
|
+
export { normalizeVercelAIGatewayCreditsPayload } from "./providers/vercel-ai-gateway.js";
|
|
41
54
|
export { normalizeXaiBillingPayload } from "./providers/xai.js";
|
|
42
55
|
export { normalizeZaiQuotaPayload } from "./providers/zai.js";
|
|
43
56
|
export {
|
|
@@ -63,8 +76,13 @@ export {
|
|
|
63
76
|
usageSettingsPath,
|
|
64
77
|
} from "./settings.js";
|
|
65
78
|
export type {
|
|
79
|
+
BasetenBillingUsagePayload,
|
|
66
80
|
DeepSeekBalancePayload,
|
|
81
|
+
FireworksAccountsPayload,
|
|
82
|
+
FireworksBillingSummaryPayload,
|
|
67
83
|
KimiCodingUsagePayload,
|
|
84
|
+
MiniMaxUsagePayload,
|
|
85
|
+
MoonshotBalancePayload,
|
|
68
86
|
ProviderUsageState,
|
|
69
87
|
ResolvedUsageAuth,
|
|
70
88
|
UsageBucket,
|
|
@@ -72,10 +90,12 @@ export type {
|
|
|
72
90
|
UsageMetric,
|
|
73
91
|
UsageModel,
|
|
74
92
|
UsageProviderAdapter,
|
|
93
|
+
UsageQuerySettings,
|
|
75
94
|
UsageReport,
|
|
76
95
|
UsageSemantics,
|
|
77
96
|
UsageSemanticsKind,
|
|
78
97
|
UsageUnit,
|
|
98
|
+
VercelAIGatewayCreditsPayload,
|
|
79
99
|
XaiBillingPayload,
|
|
80
100
|
XaiUserPayload,
|
|
81
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,198 @@
|
|
|
1
|
+
import { sanitizeDisplayText } from "../core.js";
|
|
2
|
+
import type {
|
|
3
|
+
FireworksAccountsPayload,
|
|
4
|
+
FireworksBillingSummaryPayload,
|
|
5
|
+
UsageMetric,
|
|
6
|
+
UsageReport,
|
|
7
|
+
} from "../types.js";
|
|
8
|
+
|
|
9
|
+
const NANOS_PER_UNIT = 1_000_000_000n;
|
|
10
|
+
const CURRENCY_PATTERN = /^[A-Z]{3}$/u;
|
|
11
|
+
const ACCOUNT_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._~-]{0,127}$/u;
|
|
12
|
+
const INTEGER_PATTERN = /^-?\d+$/u;
|
|
13
|
+
const INT64_MIN = -(2n ** 63n);
|
|
14
|
+
const INT64_MAX = 2n ** 63n - 1n;
|
|
15
|
+
const MAX_UNITS_CHARS = 20;
|
|
16
|
+
const MAX_NANOS_CHARS = 11;
|
|
17
|
+
|
|
18
|
+
const SERIES_KEYS = ["serverless", "dedicated", "training", "other"] as const;
|
|
19
|
+
type SeriesKey = (typeof SERIES_KEYS)[number];
|
|
20
|
+
|
|
21
|
+
const SERIES_LABELS: Readonly<Record<SeriesKey, string>> = {
|
|
22
|
+
serverless: "Serverless",
|
|
23
|
+
dedicated: "Dedicated deployments",
|
|
24
|
+
training: "Training",
|
|
25
|
+
other: "Other",
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export function isFireworksAccountId(value: unknown): value is string {
|
|
29
|
+
return typeof value === "string" && ACCOUNT_ID_PATTERN.test(value);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function normalizeFireworksAccountsPayload(payload: FireworksAccountsPayload): string[] {
|
|
33
|
+
if (!Array.isArray(payload.accounts)) {
|
|
34
|
+
throw new Error("Fireworks accounts response did not contain an accounts array.");
|
|
35
|
+
}
|
|
36
|
+
const accounts: string[] = [];
|
|
37
|
+
for (const raw of payload.accounts) {
|
|
38
|
+
const account = asObject(raw);
|
|
39
|
+
if (!account) throw new Error("Fireworks accounts response row was not an object.");
|
|
40
|
+
if (typeof account.name !== "string") {
|
|
41
|
+
throw new Error("Fireworks accounts response omitted the account resource name.");
|
|
42
|
+
}
|
|
43
|
+
const match = /^accounts\/([^/]+)$/u.exec(account.name);
|
|
44
|
+
if (!match || !isFireworksAccountId(match[1])) {
|
|
45
|
+
throw new Error("Fireworks accounts response returned an unsafe account resource name.");
|
|
46
|
+
}
|
|
47
|
+
const accountId = match[1];
|
|
48
|
+
if (accounts.includes(accountId)) {
|
|
49
|
+
throw new Error(`Fireworks accounts response repeated ${accountId}.`);
|
|
50
|
+
}
|
|
51
|
+
accounts.push(accountId);
|
|
52
|
+
}
|
|
53
|
+
return accounts;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function normalizeFireworksBillingSummaryPayload(
|
|
57
|
+
payload: FireworksBillingSummaryPayload,
|
|
58
|
+
accountId: string,
|
|
59
|
+
capturedAt: number,
|
|
60
|
+
): UsageReport {
|
|
61
|
+
if (!isFireworksAccountId(accountId)) {
|
|
62
|
+
throw new Error("Fireworks billing summary received an unsafe account identifier.");
|
|
63
|
+
}
|
|
64
|
+
if (payload.lineItems !== undefined && !Array.isArray(payload.lineItems)) {
|
|
65
|
+
throw new Error("Fireworks billing summary lineItems was not an array.");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const totals = new Map<string, Map<SeriesKey, bigint>>();
|
|
69
|
+
for (const raw of payload.lineItems ?? []) {
|
|
70
|
+
const lineItem = asObject(raw);
|
|
71
|
+
if (!lineItem) throw new Error("Fireworks billing line item was not an object.");
|
|
72
|
+
const cost = moneyAmount(lineItem.totalCost, "line item total cost");
|
|
73
|
+
const series = seriesKey(lineItem.series);
|
|
74
|
+
let amounts = totals.get(cost.currency);
|
|
75
|
+
if (!amounts) {
|
|
76
|
+
amounts = new Map<SeriesKey, bigint>();
|
|
77
|
+
totals.set(cost.currency, amounts);
|
|
78
|
+
}
|
|
79
|
+
amounts.set(series, (amounts.get(series) ?? 0n) + cost.amount);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const metrics: UsageMetric[] = [];
|
|
83
|
+
for (const [currency, amounts] of totals) {
|
|
84
|
+
metrics.push({
|
|
85
|
+
id: `${currency.toLowerCase()}-total`,
|
|
86
|
+
label: "Total spend",
|
|
87
|
+
value: formatMoneyAmount(sumSeries(amounts)),
|
|
88
|
+
unit: "currency",
|
|
89
|
+
currency,
|
|
90
|
+
});
|
|
91
|
+
for (const series of SERIES_KEYS) {
|
|
92
|
+
const amount = amounts.get(series);
|
|
93
|
+
if (amount === undefined) continue;
|
|
94
|
+
metrics.push({
|
|
95
|
+
id: `${currency.toLowerCase()}-${series}`,
|
|
96
|
+
label: SERIES_LABELS[series],
|
|
97
|
+
value: formatMoneyAmount(amount),
|
|
98
|
+
unit: "currency",
|
|
99
|
+
currency,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const notes = [
|
|
105
|
+
"Rated line items may differ from the final invoice once credits or adjustments are applied.",
|
|
106
|
+
];
|
|
107
|
+
if (metrics.length === 0) {
|
|
108
|
+
notes.push("Fireworks returned no rated line items for the last 30 days.");
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
providerId: "fireworks",
|
|
113
|
+
providerName: "Fireworks",
|
|
114
|
+
capturedAt,
|
|
115
|
+
source: "fireworks-billing-summary",
|
|
116
|
+
semantics: { kind: "api-key", label: "Fireworks API spend" },
|
|
117
|
+
accountLabel: sanitizeDisplayText(accountId, 80),
|
|
118
|
+
buckets: [],
|
|
119
|
+
metrics,
|
|
120
|
+
notes,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
type RatedMoney = { currency: string; amount: bigint };
|
|
125
|
+
|
|
126
|
+
function moneyAmount(value: unknown, description: string): RatedMoney {
|
|
127
|
+
const money = asObject(value);
|
|
128
|
+
if (!money) throw new Error(`Fireworks billing ${description} was not a money object.`);
|
|
129
|
+
const currency = typeof money.currencyCode === "string" ? money.currencyCode : undefined;
|
|
130
|
+
if (!currency || !CURRENCY_PATTERN.test(currency)) {
|
|
131
|
+
throw new Error(`Fireworks billing ${description} currency was not an ISO 4217 code.`);
|
|
132
|
+
}
|
|
133
|
+
// proto3 JSON omits zero-valued money fields, so absent components default to zero.
|
|
134
|
+
const units =
|
|
135
|
+
money.units === undefined
|
|
136
|
+
? 0n
|
|
137
|
+
: integerComponent(money.units, description, "whole units", MAX_UNITS_CHARS);
|
|
138
|
+
if (units < INT64_MIN || units > INT64_MAX) {
|
|
139
|
+
throw new Error(`Fireworks billing ${description} whole units exceeded the int64 range.`);
|
|
140
|
+
}
|
|
141
|
+
const nanos =
|
|
142
|
+
money.nanos === undefined
|
|
143
|
+
? 0n
|
|
144
|
+
: integerComponent(money.nanos, description, "nano units", MAX_NANOS_CHARS);
|
|
145
|
+
if (nanos <= -NANOS_PER_UNIT || nanos >= NANOS_PER_UNIT) {
|
|
146
|
+
throw new Error(`Fireworks billing ${description} nano units exceeded the Money range.`);
|
|
147
|
+
}
|
|
148
|
+
if ((units > 0n && nanos < 0n) || (units < 0n && nanos > 0n)) {
|
|
149
|
+
throw new Error(`Fireworks billing ${description} mixed unit and nano signs.`);
|
|
150
|
+
}
|
|
151
|
+
return { currency, amount: units * NANOS_PER_UNIT + nanos };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function integerComponent(
|
|
155
|
+
value: unknown,
|
|
156
|
+
description: string,
|
|
157
|
+
component: string,
|
|
158
|
+
maxChars: number,
|
|
159
|
+
): bigint {
|
|
160
|
+
const text =
|
|
161
|
+
typeof value === "number" && Number.isSafeInteger(value)
|
|
162
|
+
? String(value)
|
|
163
|
+
: typeof value === "string" && INTEGER_PATTERN.test(value)
|
|
164
|
+
? value
|
|
165
|
+
: undefined;
|
|
166
|
+
if (text === undefined || text.length > maxChars) {
|
|
167
|
+
throw new Error(`Fireworks billing ${description} ${component} was not a bounded integer.`);
|
|
168
|
+
}
|
|
169
|
+
return BigInt(text);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function seriesKey(value: unknown): SeriesKey {
|
|
173
|
+
if (value === undefined || value === null) return "other";
|
|
174
|
+
if (typeof value !== "string") throw new Error("Fireworks billing line item series was invalid.");
|
|
175
|
+
if (value === "SERVERLESS") return "serverless";
|
|
176
|
+
if (value === "DEDICATED_DEPLOYMENT") return "dedicated";
|
|
177
|
+
if (value === "TRAINING") return "training";
|
|
178
|
+
return "other";
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function sumSeries(amounts: ReadonlyMap<SeriesKey, bigint>): bigint {
|
|
182
|
+
let total = 0n;
|
|
183
|
+
for (const amount of amounts.values()) total += amount;
|
|
184
|
+
return total;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function formatMoneyAmount(amount: bigint): string {
|
|
188
|
+
const negative = amount < 0n;
|
|
189
|
+
const magnitude = negative ? -amount : amount;
|
|
190
|
+
const units = magnitude / NANOS_PER_UNIT;
|
|
191
|
+
const nanos = (magnitude % NANOS_PER_UNIT).toString().padStart(9, "0").replace(/0+$/u, "");
|
|
192
|
+
return `${negative ? "-" : ""}${units.toString()}${nanos ? `.${nanos}` : ""}`;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function asObject(value: unknown): Record<string, unknown> | undefined {
|
|
196
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
197
|
+
return value as Record<string, unknown>;
|
|
198
|
+
}
|