@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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@narumitw/pi-usage",
3
- "version": "0.53.0",
4
- "description": "Pi extension that shows current-account usage for Codex, GitHub Copilot, OpenRouter, OpenCode Zen, and Z.AI.",
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.10",
49
- "@earendil-works/pi-ai": "0.84.3",
50
- "@earendil-works/pi-coding-agent": "0.84.3",
51
- "@types/node": "26.2.0",
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 lines = [`${report.providerName} Usage · ${stateLabel}`];
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(report: UsageReport, model?: UsageModel): string | undefined {
33
- if (report.providerId === "openai-codex") return formatCodexStatusline(report, model);
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(report: UsageReport, model?: UsageModel): string | undefined {
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
- parts.push(
214
- `${clampPercent(bucket.remaining).toFixed(0)}% ${formatWindowLabel(bucket.windowMinutes, fallback, true)}`,
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
+ }