@narumitw/pi-usage 0.53.0 → 0.57.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 +171 -45
- package/dist/index.ts +1082 -67
- package/dist/index.ts.map +4 -4
- package/package.json +16 -11
- package/src/format.ts +184 -7
- package/src/index.ts +9 -0
- package/src/providers/deepseek.ts +85 -0
- package/src/providers/kimi-coding.ts +276 -0
- package/src/providers/xai.ts +186 -0
- package/src/query.ts +250 -9
- package/src/settings.ts +12 -0
- package/src/types.ts +28 -2
- package/src/usage-helpers.ts +2 -2
- package/src/usage-settings-ui.ts +127 -0
- package/src/usage.ts +156 -14
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@narumitw/pi-usage",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Pi extension that shows current-account usage
|
|
3
|
+
"version": "0.57.0",
|
|
4
|
+
"description": "Pi extension that shows current-account usage and DeepSeek API balance for supported providers.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"private": false,
|
|
@@ -11,13 +11,19 @@
|
|
|
11
11
|
"pi",
|
|
12
12
|
"usage",
|
|
13
13
|
"quota",
|
|
14
|
+
"balance",
|
|
15
|
+
"deepseek",
|
|
14
16
|
"codex",
|
|
17
|
+
"kimi",
|
|
18
|
+
"kimi-coding",
|
|
15
19
|
"copilot",
|
|
16
20
|
"openrouter",
|
|
17
21
|
"opencode",
|
|
18
22
|
"zen",
|
|
19
23
|
"zai",
|
|
20
|
-
"glm"
|
|
24
|
+
"glm",
|
|
25
|
+
"xai",
|
|
26
|
+
"grok"
|
|
21
27
|
],
|
|
22
28
|
"files": [
|
|
23
29
|
"src",
|
|
@@ -30,9 +36,6 @@
|
|
|
30
36
|
"./dist/index.ts"
|
|
31
37
|
]
|
|
32
38
|
},
|
|
33
|
-
"piExtension": {
|
|
34
|
-
"lifecycle": "stable"
|
|
35
|
-
},
|
|
36
39
|
"scripts": {
|
|
37
40
|
"build": "node scripts/build-runtime.mjs",
|
|
38
41
|
"check": "npm run build && biome check --vcs-use-ignore-file=false src test scripts package.json tsconfig.json README.md && npm run typecheck",
|
|
@@ -42,13 +45,15 @@
|
|
|
42
45
|
},
|
|
43
46
|
"peerDependencies": {
|
|
44
47
|
"@earendil-works/pi-ai": "*",
|
|
45
|
-
"@earendil-works/pi-coding-agent": "*"
|
|
48
|
+
"@earendil-works/pi-coding-agent": "*",
|
|
49
|
+
"@earendil-works/pi-tui": "*"
|
|
46
50
|
},
|
|
47
51
|
"devDependencies": {
|
|
48
|
-
"@biomejs/biome": "2.5.
|
|
49
|
-
"@earendil-works/pi-ai": "0.84.
|
|
50
|
-
"@earendil-works/pi-coding-agent": "0.84.
|
|
51
|
-
"@
|
|
52
|
+
"@biomejs/biome": "2.5.11",
|
|
53
|
+
"@earendil-works/pi-ai": "0.84.4",
|
|
54
|
+
"@earendil-works/pi-coding-agent": "0.84.4",
|
|
55
|
+
"@earendil-works/pi-tui": "0.84.4",
|
|
56
|
+
"@types/node": "26.4.0",
|
|
52
57
|
"esbuild": "0.28.2",
|
|
53
58
|
"typescript": "7.0.2"
|
|
54
59
|
},
|
package/src/format.ts
CHANGED
|
@@ -11,14 +11,19 @@ const VALUE_COLUMN = 29;
|
|
|
11
11
|
|
|
12
12
|
export function formatUsageReport(report: UsageReport, displayState: UsageDisplayState): string {
|
|
13
13
|
const stateLabel = displayState === "current" ? "Current" : "Configured";
|
|
14
|
-
const
|
|
14
|
+
const title =
|
|
15
|
+
report.providerId === "deepseek" ? "DeepSeek API Balance" : `${report.providerName} Usage`;
|
|
16
|
+
const lines = [`${title} · ${stateLabel}`];
|
|
15
17
|
if (report.accountLabel) lines.push(`Account: ${report.accountLabel}`);
|
|
16
18
|
lines.push(`Semantics: ${report.semantics.label}`, "");
|
|
17
19
|
|
|
18
20
|
if (report.providerId === "openai-codex") formatCodexReport(lines, report);
|
|
21
|
+
else if (report.providerId === "deepseek") formatDeepSeekReport(lines, report);
|
|
19
22
|
else if (report.providerId === "github-copilot") formatGitHubCopilotReport(lines, report);
|
|
20
23
|
else if (report.providerId === "openrouter") formatOpenRouterReport(lines, report);
|
|
21
24
|
else if (report.providerId === "opencode-go") formatOpenCodeZenReport(lines, report);
|
|
25
|
+
else if (report.providerId === "kimi-coding") formatKimiCodingReport(lines, report);
|
|
26
|
+
else if (report.providerId === "xai") formatXaiReport(lines, report);
|
|
22
27
|
else if (report.providerId === "zai" || report.providerId === "zai-coding-cn") {
|
|
23
28
|
formatZaiReport(lines, report);
|
|
24
29
|
} else formatGenericReport(lines, report);
|
|
@@ -29,8 +34,16 @@ export function formatUsageReport(report: UsageReport, displayState: UsageDispla
|
|
|
29
34
|
return lines.join("\n").trimEnd();
|
|
30
35
|
}
|
|
31
36
|
|
|
32
|
-
export function formatUsageStatusline(
|
|
33
|
-
|
|
37
|
+
export function formatUsageStatusline(
|
|
38
|
+
report: UsageReport,
|
|
39
|
+
model?: UsageModel,
|
|
40
|
+
now = Date.now(),
|
|
41
|
+
showCodexResetCountdown = true,
|
|
42
|
+
): string | undefined {
|
|
43
|
+
if (report.providerId === "openai-codex") {
|
|
44
|
+
return formatCodexStatusline(report, model, now, showCodexResetCountdown);
|
|
45
|
+
}
|
|
46
|
+
if (report.providerId === "deepseek") return formatDeepSeekStatusline(report);
|
|
34
47
|
if (report.providerId === "github-copilot") return formatGitHubCopilotStatusline(report);
|
|
35
48
|
if (report.providerId === "openrouter") {
|
|
36
49
|
const limit = report.buckets.find((bucket) => bucket.id === "key-limit");
|
|
@@ -39,6 +52,10 @@ export function formatUsageStatusline(report: UsageReport, model?: UsageModel):
|
|
|
39
52
|
if (typeof total?.value === "number") return `openrouter ${formatUsd(total.value)} used`;
|
|
40
53
|
}
|
|
41
54
|
if (report.providerId === "opencode-go") return formatOpenCodeZenStatusline(report);
|
|
55
|
+
if (report.providerId === "kimi-coding") return formatKimiCodingStatusline(report);
|
|
56
|
+
if (report.providerId === "zai" || report.providerId === "zai-coding-cn") {
|
|
57
|
+
return formatZaiStatusline(report);
|
|
58
|
+
}
|
|
42
59
|
return undefined;
|
|
43
60
|
}
|
|
44
61
|
|
|
@@ -81,6 +98,33 @@ function formatCodexReport(lines: string[], report: UsageReport): void {
|
|
|
81
98
|
}
|
|
82
99
|
}
|
|
83
100
|
|
|
101
|
+
function formatDeepSeekReport(lines: string[], report: UsageReport): void {
|
|
102
|
+
const availability = report.metrics.find((metric) => metric.id === "api-availability");
|
|
103
|
+
lines.push(
|
|
104
|
+
`${"API calls:".padEnd(VALUE_COLUMN)}${availability?.value === "available" ? "Available" : "Unavailable"}`,
|
|
105
|
+
);
|
|
106
|
+
for (const currency of ["CNY", "USD"]) {
|
|
107
|
+
const metrics = report.metrics.filter((metric) => metric.currency === currency);
|
|
108
|
+
if (metrics.length === 0) continue;
|
|
109
|
+
lines.push("", `${currency} balance:`);
|
|
110
|
+
for (const metric of metrics) {
|
|
111
|
+
lines.push(`${`${metric.label}:`.padEnd(VALUE_COLUMN)}${currency} ${metric.value}`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function formatDeepSeekStatusline(report: UsageReport): string {
|
|
117
|
+
const availability = report.metrics.find((metric) => metric.id === "api-availability");
|
|
118
|
+
if (availability?.value !== "available") return "deepseek API unavailable";
|
|
119
|
+
const totals = ["CNY", "USD"].flatMap((currency) => {
|
|
120
|
+
const metric = report.metrics.find(
|
|
121
|
+
(candidate) => candidate.id === `${currency.toLowerCase()}-total`,
|
|
122
|
+
);
|
|
123
|
+
return metric ? [`${currency} ${metric.value}`] : [];
|
|
124
|
+
});
|
|
125
|
+
return totals.length > 0 ? `deepseek ${totals.join(" · ")}` : "deepseek balance unavailable";
|
|
126
|
+
}
|
|
127
|
+
|
|
84
128
|
function formatGitHubCopilotReport(lines: string[], report: UsageReport): void {
|
|
85
129
|
const quota = findGitHubCopilotQuota(report);
|
|
86
130
|
if (!quota || quota.limit === undefined || quota.remaining === undefined) {
|
|
@@ -162,6 +206,109 @@ function formatOpenCodeZenStatusline(report: UsageReport): string | undefined {
|
|
|
162
206
|
return parts.length > 1 ? parts.join(" ") : undefined;
|
|
163
207
|
}
|
|
164
208
|
|
|
209
|
+
function formatKimiCodingReport(lines: string[], report: UsageReport): void {
|
|
210
|
+
for (const bucket of report.buckets) {
|
|
211
|
+
const reset = bucket.resetsAt ? ` (resets ${formatReset(bucket.resetsAt)})` : "";
|
|
212
|
+
if (bucket.used === undefined || bucket.limit === undefined) {
|
|
213
|
+
lines.push(`${`${bucket.label}:`.padEnd(VALUE_COLUMN)}unavailable${reset}`);
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
lines.push(
|
|
217
|
+
`${`${bucket.label}:`.padEnd(VALUE_COLUMN)}${bucket.used} of ${bucket.limit} used · ${percentRemaining(bucket)}% left${reset}`,
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
const balance = report.metrics.find((metric) => metric.id === "booster-balance");
|
|
221
|
+
const total = report.metrics.find((metric) => metric.id === "booster-total");
|
|
222
|
+
const monthlyUsed = report.metrics.find((metric) => metric.id === "booster-monthly-used");
|
|
223
|
+
const monthlyLimit = report.metrics.find((metric) => metric.id === "booster-monthly-limit");
|
|
224
|
+
if (!balance && !monthlyUsed && !monthlyLimit) return;
|
|
225
|
+
lines.push("", "Extra usage wallet:");
|
|
226
|
+
if (balance) {
|
|
227
|
+
const totalSuffix = total ? ` of ${formatCurrencyMetric(total)}` : "";
|
|
228
|
+
lines.push(`${"Balance:".padEnd(VALUE_COLUMN)}${formatCurrencyMetric(balance)}${totalSuffix}`);
|
|
229
|
+
}
|
|
230
|
+
if (monthlyUsed) {
|
|
231
|
+
lines.push(`${"Used this month:".padEnd(VALUE_COLUMN)}${formatCurrencyMetric(monthlyUsed)}`);
|
|
232
|
+
}
|
|
233
|
+
if (monthlyLimit) {
|
|
234
|
+
lines.push(`${"Monthly limit:".padEnd(VALUE_COLUMN)}${formatCurrencyMetric(monthlyLimit)}`);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function formatKimiCodingStatusline(report: UsageReport): string | undefined {
|
|
239
|
+
const fiveHour = report.buckets.find((bucket) => bucket.id === "five-hour");
|
|
240
|
+
const weekly = report.buckets.find((bucket) => bucket.id === "weekly");
|
|
241
|
+
const subWindow = fiveHour ?? report.buckets.find((bucket) => bucket.id !== "weekly");
|
|
242
|
+
const selected = [subWindow, weekly].filter(
|
|
243
|
+
(bucket, index, buckets): bucket is UsageBucket =>
|
|
244
|
+
bucket !== undefined && buckets.indexOf(bucket) === index,
|
|
245
|
+
);
|
|
246
|
+
const parts = ["kimi"];
|
|
247
|
+
for (const bucket of selected) {
|
|
248
|
+
if (!bucket.limit || bucket.remaining === undefined) continue;
|
|
249
|
+
const fallback = bucket.id === "weekly" ? "weekly" : "5h";
|
|
250
|
+
parts.push(
|
|
251
|
+
`${percentRemaining(bucket)}% ${formatWindowLabel(bucket.windowMinutes, fallback, true)}`,
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
return parts.length > 1 ? parts.join(" ") : undefined;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function formatZaiStatusline(report: UsageReport): string | undefined {
|
|
258
|
+
const selected = [
|
|
259
|
+
report.buckets.find((bucket) => bucket.id === "five-hour"),
|
|
260
|
+
report.buckets.find((bucket) => bucket.id === "weekly"),
|
|
261
|
+
];
|
|
262
|
+
const parts = ["zai"];
|
|
263
|
+
for (const bucket of selected) {
|
|
264
|
+
if (!bucket?.limit || bucket.remaining === undefined) continue;
|
|
265
|
+
const fallback = bucket.id === "weekly" ? "weekly" : "5h";
|
|
266
|
+
parts.push(
|
|
267
|
+
`${percentRemaining(bucket)}% ${formatWindowLabel(bucket.windowMinutes, fallback, true)}`,
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
return parts.length > 1 ? parts.join(" ") : undefined;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function formatCurrencyMetric(metric: UsageReport["metrics"][number]): string {
|
|
274
|
+
if (typeof metric.value !== "number") return String(metric.value);
|
|
275
|
+
if (!metric.currency) return "unavailable";
|
|
276
|
+
if (metric.currency === "USD") return `$${metric.value.toFixed(2)}`;
|
|
277
|
+
if (metric.currency === "CNY") return `¥${metric.value.toFixed(2)}`;
|
|
278
|
+
return `${metric.value.toFixed(2)} ${metric.currency}`;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function formatXaiReport(lines: string[], report: UsageReport): void {
|
|
282
|
+
const included = report.buckets.find((bucket) => bucket.id === "included-allowance");
|
|
283
|
+
if (included) {
|
|
284
|
+
let value = "unavailable";
|
|
285
|
+
if (included.unit === "percent" && included.used !== undefined) {
|
|
286
|
+
value = `${included.used}% used`;
|
|
287
|
+
if (included.remaining !== undefined) value += ` · ${included.remaining}% left`;
|
|
288
|
+
} else if (included.used !== undefined) {
|
|
289
|
+
value = `${formatUsd(included.used)} used`;
|
|
290
|
+
if (included.limit !== undefined) value += ` of ${formatUsd(included.limit)}`;
|
|
291
|
+
} else if (included.limit !== undefined) {
|
|
292
|
+
value = `usage unavailable · ${formatUsd(included.limit)} limit`;
|
|
293
|
+
}
|
|
294
|
+
const period = included.period ? ` · ${included.period}` : "";
|
|
295
|
+
const reset = included.resetsAt ? ` (resets ${formatReset(included.resetsAt)})` : "";
|
|
296
|
+
lines.push(`${"Included allowance:".padEnd(VALUE_COLUMN)}${value}${period}${reset}`);
|
|
297
|
+
}
|
|
298
|
+
const onDemand = report.buckets.find((bucket) => bucket.id === "on-demand");
|
|
299
|
+
if (onDemand) {
|
|
300
|
+
let value =
|
|
301
|
+
onDemand.used === undefined ? "usage unavailable" : `${formatUsd(onDemand.used)} used`;
|
|
302
|
+
if (onDemand.limit !== undefined) value += ` of ${formatUsd(onDemand.limit)} cap`;
|
|
303
|
+
lines.push(`${"On-demand usage:".padEnd(VALUE_COLUMN)}${value}`);
|
|
304
|
+
}
|
|
305
|
+
for (const metric of report.metrics) {
|
|
306
|
+
lines.push(
|
|
307
|
+
`${`${metric.label}:`.padEnd(VALUE_COLUMN)}${formatMetricValue(metric.value, metric.unit)}`,
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
165
312
|
function formatZaiReport(lines: string[], report: UsageReport): void {
|
|
166
313
|
for (const bucket of report.buckets) {
|
|
167
314
|
const reset = bucket.resetsAt ? ` (resets ${formatReset(bucket.resetsAt)})` : "";
|
|
@@ -199,7 +346,12 @@ function formatGenericReport(lines: string[], report: UsageReport): void {
|
|
|
199
346
|
}
|
|
200
347
|
}
|
|
201
348
|
|
|
202
|
-
function formatCodexStatusline(
|
|
349
|
+
function formatCodexStatusline(
|
|
350
|
+
report: UsageReport,
|
|
351
|
+
model?: UsageModel,
|
|
352
|
+
now = Date.now(),
|
|
353
|
+
showResetCountdown = true,
|
|
354
|
+
): string | undefined {
|
|
203
355
|
const group = selectCodexGroup(report, model);
|
|
204
356
|
if (!group) return formatCodexCreditsStatus(report);
|
|
205
357
|
const buckets = report.buckets.filter((bucket) => (bucket.groupId ?? bucket.id) === group);
|
|
@@ -209,10 +361,15 @@ function formatCodexStatusline(report: UsageReport, model?: UsageModel): string
|
|
|
209
361
|
];
|
|
210
362
|
for (const bucket of buckets) {
|
|
211
363
|
if (bucket.remaining === undefined) continue;
|
|
364
|
+
const percent = `${clampPercent(bucket.remaining).toFixed(0)}%`;
|
|
212
365
|
const fallback = bucket.id.endsWith(":secondary") ? "weekly" : "5h";
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
366
|
+
const window = formatWindowLabel(bucket.windowMinutes, fallback, true);
|
|
367
|
+
if (!showResetCountdown) {
|
|
368
|
+
parts.push(`${percent} ${window}`);
|
|
369
|
+
continue;
|
|
370
|
+
}
|
|
371
|
+
const reset = formatResetCountdown(bucket.resetsAt, now);
|
|
372
|
+
parts.push(`${percent} ${reset ? `↻ ${reset}` : window}`);
|
|
216
373
|
}
|
|
217
374
|
return parts.length > 1 ? parts.join(" ") : formatCodexCreditsStatus(report);
|
|
218
375
|
}
|
|
@@ -315,6 +472,26 @@ function formatWindowLabel(
|
|
|
315
472
|
return `${minutes}m`;
|
|
316
473
|
}
|
|
317
474
|
|
|
475
|
+
function formatResetCountdown(resetsAt: number | undefined, now: number): string | undefined {
|
|
476
|
+
if (resetsAt === undefined || !Number.isFinite(resetsAt) || !Number.isFinite(now))
|
|
477
|
+
return undefined;
|
|
478
|
+
const totalMinutes = Math.max(0, Math.ceil((resetsAt * 1_000 - now) / 60_000));
|
|
479
|
+
const days = Math.floor(totalMinutes / 1_440);
|
|
480
|
+
const hours = Math.floor((totalMinutes % 1_440) / 60);
|
|
481
|
+
const minutes = totalMinutes % 60;
|
|
482
|
+
if (days > 0) {
|
|
483
|
+
return [
|
|
484
|
+
`${String(days)}d`,
|
|
485
|
+
hours > 0 ? `${String(hours)}h` : minutes > 0 ? `${String(minutes)}m` : "",
|
|
486
|
+
]
|
|
487
|
+
.filter(Boolean)
|
|
488
|
+
.join("");
|
|
489
|
+
}
|
|
490
|
+
if (hours > 0)
|
|
491
|
+
return [`${String(hours)}h`, minutes > 0 ? `${String(minutes)}m` : ""].filter(Boolean).join("");
|
|
492
|
+
return `${String(minutes)}m`;
|
|
493
|
+
}
|
|
494
|
+
|
|
318
495
|
function formatMetricValue(value: number | string, unit: UsageBucket["unit"] | undefined): string {
|
|
319
496
|
if (unit === "usd" && typeof value === "number") return formatUsd(value);
|
|
320
497
|
return String(value);
|
package/src/index.ts
CHANGED
|
@@ -33,9 +33,12 @@ export {
|
|
|
33
33
|
} from "./core.js";
|
|
34
34
|
export { formatProviderStates, formatUsageReport, formatUsageStatusline } from "./format.js";
|
|
35
35
|
export { normalizeCodexBackendPayload } from "./providers/codex.js";
|
|
36
|
+
export { normalizeDeepSeekBalancePayload } from "./providers/deepseek.js";
|
|
36
37
|
export { normalizeGitHubCopilotUsagePayload } from "./providers/github-copilot.js";
|
|
38
|
+
export { normalizeKimiCodingUsagePayload } from "./providers/kimi-coding.js";
|
|
37
39
|
export { normalizeOpenCodeZenPayload } from "./providers/opencode-zen.js";
|
|
38
40
|
export { normalizeOpenRouterKeyPayload } from "./providers/openrouter.js";
|
|
41
|
+
export { normalizeXaiBillingPayload } from "./providers/xai.js";
|
|
39
42
|
export { normalizeZaiQuotaPayload } from "./providers/zai.js";
|
|
40
43
|
export {
|
|
41
44
|
adapterForProvider,
|
|
@@ -44,6 +47,8 @@ export {
|
|
|
44
47
|
queryProviderUsage,
|
|
45
48
|
resolveUsageAuth,
|
|
46
49
|
SUPPORTED_ADAPTERS,
|
|
50
|
+
usageAdapters,
|
|
51
|
+
XAI_ADAPTER,
|
|
47
52
|
} from "./query.js";
|
|
48
53
|
export type {
|
|
49
54
|
UsageSettings,
|
|
@@ -58,6 +63,8 @@ export {
|
|
|
58
63
|
usageSettingsPath,
|
|
59
64
|
} from "./settings.js";
|
|
60
65
|
export type {
|
|
66
|
+
DeepSeekBalancePayload,
|
|
67
|
+
KimiCodingUsagePayload,
|
|
61
68
|
ProviderUsageState,
|
|
62
69
|
ResolvedUsageAuth,
|
|
63
70
|
UsageBucket,
|
|
@@ -69,5 +76,7 @@ export type {
|
|
|
69
76
|
UsageSemantics,
|
|
70
77
|
UsageSemanticsKind,
|
|
71
78
|
UsageUnit,
|
|
79
|
+
XaiBillingPayload,
|
|
80
|
+
XaiUserPayload,
|
|
72
81
|
} from "./types.js";
|
|
73
82
|
export { default } from "./usage.js";
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import type { DeepSeekBalancePayload, UsageMetric, UsageReport } from "../types.js";
|
|
2
|
+
|
|
3
|
+
const CURRENCIES = ["CNY", "USD"] as const;
|
|
4
|
+
type DeepSeekCurrency = (typeof CURRENCIES)[number];
|
|
5
|
+
|
|
6
|
+
const BALANCE_FIELDS = [
|
|
7
|
+
["total", "Total balance", "total_balance"],
|
|
8
|
+
["granted", "Granted balance", "granted_balance"],
|
|
9
|
+
["topped-up", "Topped-up balance", "topped_up_balance"],
|
|
10
|
+
] as const;
|
|
11
|
+
|
|
12
|
+
export function normalizeDeepSeekBalancePayload(
|
|
13
|
+
payload: DeepSeekBalancePayload,
|
|
14
|
+
capturedAt: number,
|
|
15
|
+
): UsageReport {
|
|
16
|
+
if (typeof payload.is_available !== "boolean") {
|
|
17
|
+
throw new Error("DeepSeek API balance response availability was not a boolean.");
|
|
18
|
+
}
|
|
19
|
+
if (!Array.isArray(payload.balance_infos) || payload.balance_infos.length === 0) {
|
|
20
|
+
throw new Error("DeepSeek API balance response returned no balance information.");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const balances = new Map<DeepSeekCurrency, Record<string, unknown>>();
|
|
24
|
+
for (const raw of payload.balance_infos) {
|
|
25
|
+
const balance = asObject(raw);
|
|
26
|
+
if (!balance) throw new Error("DeepSeek API balance row was not an object.");
|
|
27
|
+
const currency = deepSeekCurrency(balance.currency);
|
|
28
|
+
if (!currency) throw new Error("DeepSeek API balance row returned an unsupported currency.");
|
|
29
|
+
if (balances.has(currency)) {
|
|
30
|
+
throw new Error(`DeepSeek API balance response repeated ${currency}.`);
|
|
31
|
+
}
|
|
32
|
+
for (const [, label, field] of BALANCE_FIELDS) {
|
|
33
|
+
if (!decimalAmount(balance[field])) {
|
|
34
|
+
throw new Error(`DeepSeek API balance ${label.toLowerCase()} was not a valid amount.`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
balances.set(currency, balance);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const metrics: UsageMetric[] = [
|
|
41
|
+
{
|
|
42
|
+
id: "api-availability",
|
|
43
|
+
label: "API calls",
|
|
44
|
+
value: payload.is_available ? "available" : "unavailable",
|
|
45
|
+
},
|
|
46
|
+
];
|
|
47
|
+
for (const currency of CURRENCIES) {
|
|
48
|
+
const balance = balances.get(currency);
|
|
49
|
+
if (!balance) continue;
|
|
50
|
+
for (const [id, label, field] of BALANCE_FIELDS) {
|
|
51
|
+
metrics.push({
|
|
52
|
+
id: `${currency.toLowerCase()}-${id}`,
|
|
53
|
+
label,
|
|
54
|
+
value: balance[field] as string,
|
|
55
|
+
unit: "currency",
|
|
56
|
+
currency,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
providerId: "deepseek",
|
|
63
|
+
providerName: "DeepSeek",
|
|
64
|
+
capturedAt,
|
|
65
|
+
source: "deepseek-balance",
|
|
66
|
+
semantics: { kind: "api-key", label: "DeepSeek API balance" },
|
|
67
|
+
buckets: [],
|
|
68
|
+
metrics,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function asObject(value: unknown): Record<string, unknown> | undefined {
|
|
73
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
74
|
+
return value as Record<string, unknown>;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function deepSeekCurrency(value: unknown): DeepSeekCurrency | undefined {
|
|
78
|
+
return CURRENCIES.find((currency) => currency === value);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function decimalAmount(value: unknown): value is string {
|
|
82
|
+
return (
|
|
83
|
+
typeof value === "string" && value.length <= 64 && /^(?:0|[1-9]\d*)(?:\.\d+)?$/u.test(value)
|
|
84
|
+
);
|
|
85
|
+
}
|