@narumitw/pi-usage 0.59.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 CHANGED
@@ -3,7 +3,6 @@
3
3
  [![npm](https://img.shields.io/npm/v/@narumitw/pi-usage)](https://www.npmjs.com/package/@narumitw/pi-usage) [![Pi extension](https://img.shields.io/badge/Pi-extension-blue)](https://pi.dev) [![License: MIT](https://img.shields.io/badge/license-MIT-green.svg)](./LICENSE)
4
4
 
5
5
  Inspect usage and DeepSeek API balance for Pi's active provider account, query other configured providers, and toggle Fast mode for supported OpenAI Codex models.
6
-
7
6
  The extension keeps each provider's native quota, allowance, and spending semantics instead of treating unlike values as equivalent.
8
7
  xAI OAuth subscription reporting follows the reviewed Grok Build contract and runs only after an explicit `/usage` action.
9
8
 
@@ -122,7 +121,6 @@ Submit a blank value from the TUI input, or remove the JSON field and run `/relo
122
121
  ### Codex Fast mode
123
122
 
124
123
  Run `/fast` without arguments to toggle Fast for the active supported Codex model, or use **Turn Fast mode on/off** in `/usage`.
125
-
126
124
  Fast is about 1.5× faster and uses more of your plan allowance.
127
125
  The `codexFastMode` preference defaults to Off.
128
126
 
@@ -138,7 +136,6 @@ Repair or remove an invalid file, then run `/reload` before trying the toggle ag
138
136
  ### Codex statusline reset countdown
139
137
 
140
138
  The `codexStatusResetCountdown` preference defaults to `true`. It replaces the window labels with the time remaining until each returned limit resets.
141
-
142
139
  Turn **Codex reset countdown** Off in the TUI Settings screen, or set it to `false` in `pi-usage.json` and run `/reload`, to restore the legacy `5h` and `wk` labels:
143
140
 
144
141
  ```json
@@ -374,9 +371,9 @@ xAI identity and billing requests occur only after an explicit current, configur
374
371
 
375
372
  - Provider ID: `zai` and `zai-coding-cn`
376
373
  - Semantics: GLM Coding Plan quota windows—the rolling 5-hour and weekly plan-usage windows plus the monthly MCP allowance
377
- - Source: the undocumented `GET {origin}/api/monitor/usage/quota/limit` endpoint also used by Z.AI's official coding plugin
374
+ - Source: the undocumented `GET {origin}/api/monitor/usage/quota/limit` endpoint also used by Z.AI's official coding plugin, plus the undocumented `GET {origin}/api/biz/subscription/list` plan endpoint
378
375
  - Allowed origins: the model base URL must resolve to `https://api.z.ai` or `https://open.bigmodel.cn`
379
- - Displayed data: explicit used and remaining values, reset times, provider-reported per-tool MCP details, and the reported plan level
376
+ - Displayed data: explicit used and remaining values, reset times, provider-reported per-tool MCP details, and the plan name with its renewal date
380
377
  - Percentage-only windows remain percent-based
381
378
  - Statusline: publishes remaining plan percentages such as `zai 87% 5h 76% wk`; monthly MCP details remain available through `/usage`
382
379
 
@@ -385,6 +382,7 @@ The extension classifies both forms by the provider's window unit and does not l
385
382
  The quota monitor expects a raw API key without a `Bearer` prefix.
386
383
  The extension removes that prefix from resolved authorization before sending it to the monitor endpoint.
387
384
  Fingerprinting and redaction keep using the original resolved credential.
385
+ The plan endpoint only contributes the plan name and renewal date; when it is unavailable or fails, the quota windows remain reported and the plan note falls back to the quota response's plan level.
388
386
  Only the official `api.z.ai` and `open.bigmodel.cn` origins are queried; other origins fail before sending the credential.
389
387
 
390
388
  ## 🧭 Current and configured accounts
@@ -501,7 +499,6 @@ packages/pi-usage/
501
499
  ```
502
500
 
503
501
  `index.ts` is the Pi entrypoint and forwards the default factory from `usage.ts` while retaining the package's named helper exports; other source modules are internal.
504
-
505
502
  The generated runtime is built from the authoritative `src/index.ts` graph and does not import back into `src`.
506
503
 
507
504
  ## 🔎 Keywords
package/dist/index.ts CHANGED
@@ -1604,7 +1604,7 @@ function isRecord2(value) {
1604
1604
  // src/providers/zai.ts
1605
1605
  var FIVE_HOUR_WINDOW_MINUTES2 = 300;
1606
1606
  var WEEKLY_WINDOW_MINUTES2 = 10080;
1607
- function normalizeZaiQuotaPayload(providerId, providerName, payload, capturedAt) {
1607
+ function normalizeZaiQuotaPayload(providerId, providerName, payload, capturedAt, plan) {
1608
1608
  const data = asObject11(payload.data);
1609
1609
  if (!data) throw new Error("Z.AI quota response data was not an object.");
1610
1610
  const limits = Array.isArray(data.limits) ? data.limits : [];
@@ -1620,14 +1620,20 @@ function normalizeZaiQuotaPayload(providerId, providerName, payload, capturedAt)
1620
1620
  addCountBucket(buckets, limit, "mcp-monthly", "MCP monthly allowance");
1621
1621
  addUsageDetailMetrics(metrics, limit.usageDetails);
1622
1622
  } else if (isPlanUsage && unit === 3) {
1623
- addPercentBucket(buckets, limit, "five-hour", "5h window", FIVE_HOUR_WINDOW_MINUTES2);
1623
+ addPercentBucket(
1624
+ buckets,
1625
+ limit,
1626
+ "five-hour",
1627
+ sessionWindowLabel(limit),
1628
+ sessionWindowMinutes(limit)
1629
+ );
1624
1630
  } else if (isPlanUsage && unit === 6) {
1625
1631
  const used = asNonnegativeNumber4(limit.currentValue);
1626
1632
  const quota = asNonnegativeNumber4(limit.usage);
1627
1633
  if (used !== void 0 && quota !== void 0) {
1628
- addCountBucket(buckets, limit, "weekly", "Weekly window", WEEKLY_WINDOW_MINUTES2);
1634
+ addCountBucket(buckets, limit, "weekly", "Weekly window", weeklyWindowMinutes(limit));
1629
1635
  } else {
1630
- addPercentBucket(buckets, limit, "weekly", "Weekly window", WEEKLY_WINDOW_MINUTES2);
1636
+ addPercentBucket(buckets, limit, "weekly", "Weekly window", weeklyWindowMinutes(limit));
1631
1637
  }
1632
1638
  }
1633
1639
  }
@@ -1636,7 +1642,12 @@ function normalizeZaiQuotaPayload(providerId, providerName, payload, capturedAt)
1636
1642
  }
1637
1643
  const notes = [];
1638
1644
  const level = asString5(data.level);
1639
- if (level) notes.push(`Plan: ${level}`);
1645
+ const planLabel = plan?.name ?? level;
1646
+ if (planLabel) {
1647
+ notes.push(
1648
+ plan?.renewsAt ? `Plan: ${planLabel} \xB7 renews ${plan.renewsAt}` : `Plan: ${planLabel}`
1649
+ );
1650
+ }
1640
1651
  return {
1641
1652
  providerId,
1642
1653
  providerName,
@@ -1648,6 +1659,57 @@ function normalizeZaiQuotaPayload(providerId, providerName, payload, capturedAt)
1648
1659
  ...notes.length > 0 ? { notes } : {}
1649
1660
  };
1650
1661
  }
1662
+ function normalizeZaiSubscriptionPayload(payload) {
1663
+ if (payload.success === false) return void 0;
1664
+ if (typeof payload.code === "number" && payload.code !== 0 && payload.code !== 200) {
1665
+ return void 0;
1666
+ }
1667
+ if (!Array.isArray(payload.data)) return void 0;
1668
+ const candidates = [];
1669
+ for (const raw of payload.data) {
1670
+ const entry = asObject11(raw);
1671
+ if (!entry) continue;
1672
+ const name = asString5(entry.productName);
1673
+ if (!name) continue;
1674
+ const renewsAt = planRenewalDate(entry.nextRenewTime);
1675
+ const status = asString5(entry.status)?.toUpperCase();
1676
+ const inCurrentPeriod = asBoolean(entry.inCurrentPeriod);
1677
+ candidates.push({
1678
+ plan: { name, ...renewsAt !== void 0 ? { renewsAt } : {} },
1679
+ ...status !== void 0 ? { status } : {},
1680
+ ...inCurrentPeriod !== void 0 ? { inCurrentPeriod } : {}
1681
+ });
1682
+ }
1683
+ const hasStateMetadata = candidates.some(
1684
+ (candidate) => candidate.status !== void 0 || candidate.inCurrentPeriod !== void 0
1685
+ );
1686
+ if (!hasStateMetadata) return candidates[0]?.plan;
1687
+ return candidates.find(
1688
+ (candidate) => candidate.inCurrentPeriod === true && candidate.status === "VALID"
1689
+ )?.plan ?? candidates.find(
1690
+ (candidate) => candidate.inCurrentPeriod === true && candidate.status === void 0
1691
+ )?.plan ?? candidates.find(
1692
+ (candidate) => candidate.status === "VALID" && candidate.inCurrentPeriod === void 0
1693
+ )?.plan;
1694
+ }
1695
+ function planRenewalDate(value) {
1696
+ if (typeof value === "string" && /^\d{4}-\d{2}-\d{2}/u.test(value)) return value.slice(0, 10);
1697
+ const millis = asNonnegativeNumber4(value);
1698
+ if (millis === void 0 || millis === 0) return void 0;
1699
+ return new Date(millis).toISOString().slice(0, 10);
1700
+ }
1701
+ function sessionWindowMinutes(limit) {
1702
+ const hours = asPositiveNumber(limit.number);
1703
+ return hours === void 0 ? FIVE_HOUR_WINDOW_MINUTES2 : Math.round(hours * 60);
1704
+ }
1705
+ function sessionWindowLabel(limit) {
1706
+ const minutes = sessionWindowMinutes(limit);
1707
+ return minutes === FIVE_HOUR_WINDOW_MINUTES2 ? "5h window" : `${Math.round(minutes / 60)}h window`;
1708
+ }
1709
+ function weeklyWindowMinutes(limit) {
1710
+ const weeks = asPositiveNumber(limit.number);
1711
+ return weeks === void 0 ? WEEKLY_WINDOW_MINUTES2 : Math.round(weeks * WEEKLY_WINDOW_MINUTES2);
1712
+ }
1651
1713
  function addPercentBucket(buckets, limit, id, label, windowMinutes) {
1652
1714
  const used = asNonnegativeNumber4(limit.percentage);
1653
1715
  if (used === void 0) return;
@@ -1703,6 +1765,16 @@ function asNonnegativeNumber4(value) {
1703
1765
  if (typeof value !== "number" || !Number.isFinite(value) || value < 0) return void 0;
1704
1766
  return value;
1705
1767
  }
1768
+ function asPositiveNumber(value) {
1769
+ const number = asNonnegativeNumber4(value);
1770
+ return number !== void 0 && number > 0 ? number : void 0;
1771
+ }
1772
+ function asBoolean(value) {
1773
+ if (typeof value === "boolean") return value;
1774
+ if (value === 1) return true;
1775
+ if (value === 0) return false;
1776
+ return void 0;
1777
+ }
1706
1778
  function asEpochSeconds2(value) {
1707
1779
  const millis = asNonnegativeNumber4(value);
1708
1780
  if (millis === void 0) return void 0;
@@ -1958,35 +2030,16 @@ var SUPPORTED_ADAPTERS = [
1958
2030
  id: "zai",
1959
2031
  displayName: "Z.AI",
1960
2032
  semantics: { kind: "consumer-subscription", label: "GLM Coding Plan usage" },
1961
- async query(auth, signal, timeoutMs) {
1962
- const payload = await fetchProviderJson(
1963
- zaiMonitorUrl(auth.model.baseUrl),
1964
- zaiMonitorAuth(auth),
1965
- signal,
1966
- timeoutMs,
1967
- "Z.AI quota endpoint"
1968
- );
1969
- return normalizeZaiQuotaPayload("zai", "Z.AI", payload, Date.now());
2033
+ async query(auth, signal, timeoutMs, guard) {
2034
+ return queryZaiUsage("zai", "Z.AI", auth, signal, timeoutMs, guard);
1970
2035
  }
1971
2036
  },
1972
2037
  {
1973
2038
  id: "zai-coding-cn",
1974
2039
  displayName: "Z.AI Coding CN",
1975
2040
  semantics: { kind: "consumer-subscription", label: "GLM Coding Plan usage" },
1976
- async query(auth, signal, timeoutMs) {
1977
- const payload = await fetchProviderJson(
1978
- zaiMonitorUrl(auth.model.baseUrl),
1979
- zaiMonitorAuth(auth),
1980
- signal,
1981
- timeoutMs,
1982
- "Z.AI Coding CN quota endpoint"
1983
- );
1984
- return normalizeZaiQuotaPayload(
1985
- "zai-coding-cn",
1986
- "Z.AI Coding CN",
1987
- payload,
1988
- Date.now()
1989
- );
2041
+ async query(auth, signal, timeoutMs, guard) {
2042
+ return queryZaiUsage("zai-coding-cn", "Z.AI Coding CN", auth, signal, timeoutMs, guard);
1990
2043
  }
1991
2044
  }
1992
2045
  ];
@@ -2590,10 +2643,13 @@ function fireworksBillingSummaryUrl(accountId, startedAt) {
2590
2643
  url.searchParams.set("endTime", dayFloor(startedAt + dayMs));
2591
2644
  return url.toString();
2592
2645
  }
2593
- function zaiMonitorUrl(baseUrl) {
2646
+ function zaiOrigin(baseUrl) {
2594
2647
  const base = baseUrl?.trim();
2595
2648
  if (!base) throw new Error("Z.AI model base URL is unavailable.");
2596
- return `${new URL(base).origin}/api/monitor/usage/quota/limit`;
2649
+ return new URL(base).origin;
2650
+ }
2651
+ function zaiMonitorUrl(baseUrl) {
2652
+ return `${zaiOrigin(baseUrl)}/api/monitor/usage/quota/limit`;
2597
2653
  }
2598
2654
  function zaiMonitorAuth(auth) {
2599
2655
  const authorization = headerValue(auth.headers, "Authorization");
@@ -2601,6 +2657,38 @@ function zaiMonitorAuth(auth) {
2601
2657
  if (token === void 0 || token === authorization) return auth;
2602
2658
  return { ...auth, headers: { ...auth.headers, Authorization: token } };
2603
2659
  }
2660
+ async function queryZaiUsage(providerId, providerName, auth, signal, timeoutMs, guard) {
2661
+ if (!guard) throw new Error("Z.AI usage requires request-boundary revalidation.");
2662
+ const startedAt = Date.now();
2663
+ await guard();
2664
+ const payload = await fetchProviderJson(
2665
+ zaiMonitorUrl(auth.model.baseUrl),
2666
+ zaiMonitorAuth(auth),
2667
+ signal,
2668
+ remainingTimeout(timeoutMs, startedAt, `fetching ${providerName} quota`),
2669
+ `${providerName} quota endpoint`
2670
+ );
2671
+ await guard();
2672
+ const planTimeoutMs = timeoutMs - (Date.now() - startedAt);
2673
+ const plan = await fetchZaiPlan(providerName, auth, signal, planTimeoutMs);
2674
+ return normalizeZaiQuotaPayload(providerId, providerName, payload, Date.now(), plan);
2675
+ }
2676
+ async function fetchZaiPlan(providerName, auth, signal, timeoutMs) {
2677
+ if (timeoutMs <= 0 || signal.aborted) return void 0;
2678
+ try {
2679
+ const payload = await fetchProviderJson(
2680
+ `${zaiOrigin(auth.model.baseUrl)}/api/biz/subscription/list`,
2681
+ zaiMonitorAuth(auth),
2682
+ signal,
2683
+ timeoutMs,
2684
+ `${providerName} plan endpoint`
2685
+ );
2686
+ return normalizeZaiSubscriptionPayload(payload);
2687
+ } catch (error) {
2688
+ if (isAbortError(error)) throw error;
2689
+ return void 0;
2690
+ }
2691
+ }
2604
2692
  function isAbortError(error) {
2605
2693
  return error instanceof Error && error.name === "AbortError";
2606
2694
  }
@@ -3077,6 +3165,10 @@ function formatOpenRouterReport(lines, report2) {
3077
3165
  }
3078
3166
  function formatOpenCodeZenReport(lines, report2) {
3079
3167
  for (const bucket of report2.buckets) {
3168
+ if (bucket.unit === "percent" && bucket.used !== void 0) {
3169
+ lines.push(`${`${bucket.label}:`.padEnd(VALUE_COLUMN)}${formatPercentBucket(bucket)}`);
3170
+ continue;
3171
+ }
3080
3172
  const reset = bucket.resetsAt ? ` (resets ${formatReset(bucket.resetsAt)})` : "";
3081
3173
  const used = bucket.used ?? "unavailable";
3082
3174
  lines.push(`${`${bucket.label}:`.padEnd(VALUE_COLUMN)}${used}% used${reset}`);
@@ -3251,8 +3343,7 @@ function formatXaiReport(lines, report2) {
3251
3343
  if (included) {
3252
3344
  let value = "unavailable";
3253
3345
  if (included.unit === "percent" && included.used !== void 0) {
3254
- value = `${included.used}% used`;
3255
- if (included.remaining !== void 0) value += ` \xB7 ${included.remaining}% left`;
3346
+ value = formatPercentBar(included);
3256
3347
  } else if (included.used !== void 0) {
3257
3348
  value = `${formatUsd(included.used)} used`;
3258
3349
  if (included.limit !== void 0) value += ` of ${formatUsd(included.limit)}`;
@@ -3277,12 +3368,13 @@ function formatXaiReport(lines, report2) {
3277
3368
  }
3278
3369
  function formatZaiReport(lines, report2) {
3279
3370
  for (const bucket of report2.buckets) {
3371
+ if (bucket.unit === "percent" && bucket.used !== void 0) {
3372
+ lines.push(`${`${bucket.label}:`.padEnd(VALUE_COLUMN)}${formatPercentBucket(bucket)}`);
3373
+ continue;
3374
+ }
3280
3375
  const reset = bucket.resetsAt ? ` (resets ${formatReset(bucket.resetsAt)})` : "";
3281
3376
  let value = "unavailable";
3282
- if (bucket.unit === "percent" && bucket.used !== void 0) {
3283
- value = `${bucket.used}% used`;
3284
- if (bucket.remaining !== void 0) value += ` \xB7 ${bucket.remaining}% left`;
3285
- } else if (bucket.used !== void 0 && bucket.limit !== void 0) {
3377
+ if (bucket.used !== void 0 && bucket.limit !== void 0) {
3286
3378
  value = `${bucket.used} of ${bucket.limit} used`;
3287
3379
  if (bucket.remaining !== void 0) value += ` \xB7 ${bucket.remaining} left`;
3288
3380
  } else if (bucket.used !== void 0) {
@@ -3394,10 +3486,12 @@ function compactLimitLabel(label) {
3394
3486
  return (suffix || normalized).toLowerCase().replace(/\s+/g, " ");
3395
3487
  }
3396
3488
  function formatPercentBucket(bucket) {
3489
+ return `${formatPercentBar(bucket)}${bucket.resetsAt ? ` (resets ${formatReset(bucket.resetsAt)})` : ""}`;
3490
+ }
3491
+ function formatPercentBar(bucket) {
3397
3492
  const remaining = clampPercent4(bucket.remaining ?? 0);
3398
3493
  const filled = Math.round(remaining / 100 * BAR_SEGMENTS);
3399
- const reset = bucket.resetsAt ? ` (resets ${formatReset(bucket.resetsAt)})` : "";
3400
- return `[${"\u2588".repeat(filled)}${"\u2591".repeat(BAR_SEGMENTS - filled)}] ${remaining.toFixed(0)}% left${reset}`;
3494
+ return `[${"\u2588".repeat(filled)}${"\u2591".repeat(BAR_SEGMENTS - filled)}] ${remaining.toFixed(0)}% left`;
3401
3495
  }
3402
3496
  function formatWindowLabel(minutes, fallback, compact) {
3403
3497
  if (!minutes || !Number.isFinite(minutes) || minutes <= 0) {
@@ -4157,7 +4251,9 @@ function usageExtension(pi, dependencies = {}) {
4157
4251
  "moonshotai",
4158
4252
  "moonshotai-cn",
4159
4253
  "vercel-ai-gateway",
4160
- "xai"
4254
+ "xai",
4255
+ "zai",
4256
+ "zai-coding-cn"
4161
4257
  ].includes(adapter.id);
4162
4258
  const requestContextChanged = () => expectedSessionGeneration !== sessionGeneration || ctx.sessionManager.getSessionId() !== expectedSessionId || modelIdentity(ctx.model) !== expectedModelIdentity || adapter.id === "fireworks" && settingsRuntime.get().settings.fireworksAccountId !== expectedFireworksAccountId;
4163
4259
  if (requiresRequestBoundaryGuard && requestContextChanged()) throw abortError();
@@ -4963,6 +5059,7 @@ export {
4963
5059
  normalizeVercelAIGatewayCreditsPayload,
4964
5060
  normalizeXaiBillingPayload,
4965
5061
  normalizeZaiQuotaPayload,
5062
+ normalizeZaiSubscriptionPayload,
4966
5063
  providerIsConfigured,
4967
5064
  queryProviderUsage,
4968
5065
  redactUsageError,