@narumitw/pi-usage 0.60.4 → 0.60.7

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/dist/index.ts CHANGED
@@ -120,6 +120,9 @@ var UsageCache = class {
120
120
  }
121
121
  this.entries.set(key, { createdAt: now, report: report2 });
122
122
  }
123
+ delete(providerId, fingerprint) {
124
+ this.entries.delete(cacheKey(providerId, fingerprint));
125
+ }
123
126
  clearProvider(providerId) {
124
127
  for (const key of this.entries.keys()) {
125
128
  if (key.startsWith(`${providerId}:`)) this.entries.delete(key);
@@ -239,8 +242,6 @@ function redactUsageError(value, secrets = []) {
239
242
  function errorMessage(error) {
240
243
  return sanitizeDisplayText(error instanceof Error ? error.message : String(error), 600);
241
244
  }
242
- var UsageUnsupportedError = class extends Error {
243
- };
244
245
  function abortError() {
245
246
  return Object.assign(new Error("Usage query aborted."), { name: "AbortError" });
246
247
  }
@@ -1732,22 +1733,98 @@ function isRecord2(value) {
1732
1733
  return typeof value === "object" && value !== null && !Array.isArray(value);
1733
1734
  }
1734
1735
 
1736
+ // src/providers/zai-errors.ts
1737
+ var ERROR_MESSAGES = {
1738
+ "1000": "Authentication failed. Check your API key.",
1739
+ "1001": "Authentication header missing. Check your API key.",
1740
+ "1003": "Authentication token expired. Obtain a new token.",
1741
+ "1005": "Two-factor authentication required.",
1742
+ "1113": "Insufficient balance or no resource package. Recharge your account.",
1743
+ "1200": "API call error. Try again later.",
1744
+ "1210": "Invalid API parameter. Check the API documentation.",
1745
+ "1211": "Unknown model. Check the model ID.",
1746
+ "1212": "This model does not support the requested method.",
1747
+ "1213": "A required parameter is missing.",
1748
+ "1214": "Invalid parameter. Check the API documentation.",
1749
+ "1215": "Conflicting parameters. Check the API documentation.",
1750
+ "1220": "Access denied. Check your permissions.",
1751
+ "1221": "This API has been taken offline.",
1752
+ "1222": "This API does not exist.",
1753
+ "1230": "API processing error. Try again later.",
1754
+ "1234": "Network error. Try again later.",
1755
+ "1261": "Prompt too long. Reduce the input length.",
1756
+ "1301": "Content rejected by the safety policy.",
1757
+ "1302": "Request rate limit reached. Try again later.",
1758
+ "1305": "Service overloaded. Try again later.",
1759
+ "1308": "Usage limit reached. Wait for the quota reset.",
1760
+ "1309": "GLM Coding Plan expired. Renew your subscription.",
1761
+ "1310": "Weekly or monthly limit exhausted. Wait for the quota reset.",
1762
+ "1311": "Your subscription does not include this model.",
1763
+ "1313": "Request frequency restricted by the Fair Usage Policy. Contact support.",
1764
+ "1314": "Enterprise package expired. Contact your administrator.",
1765
+ "1315": "This API key requires an enterprise coding package scenario. Replace the key.",
1766
+ "1316": "5-hour limit reached; insufficient balance for extra usage. Wait for reset.",
1767
+ "1317": "7-day limit reached; insufficient balance for extra usage. Wait for reset.",
1768
+ "1318": "5-hour limit reached; extra usage blocked by monthly spend limit.",
1769
+ "1319": "7-day limit reached; extra usage blocked by monthly spend limit.",
1770
+ "1320": "5-hour limit reached; extra usage blocked by monthly spend limit.",
1771
+ "1321": "7-day limit reached; extra usage blocked by monthly spend limit."
1772
+ };
1773
+ var HTTP_MESSAGES = {
1774
+ 400: "Invalid request. Check the API documentation.",
1775
+ 401: "Authentication failed. Check your API key.",
1776
+ 403: "Access denied. Check your permissions.",
1777
+ 429: "Request or usage limit reached. Try again later.",
1778
+ 500: "Internal error. Try again later."
1779
+ };
1780
+ function zaiPayloadError(payload) {
1781
+ const object = asObject11(payload);
1782
+ if (!object) return void 0;
1783
+ const nested = asObject11(object.error);
1784
+ const rawCode = nested?.code === void 0 ? object.code : nested.code;
1785
+ const code = errorCode(rawCode);
1786
+ if (object.error === void 0 && object.success !== false && (rawCode === void 0 || code === "0" || code === "200")) {
1787
+ return void 0;
1788
+ }
1789
+ return code && code !== "0" && code !== "200" ? `Z.AI ${code}: ${ERROR_MESSAGES[code] ?? "API request failed."}` : "Z.AI: API request failed.";
1790
+ }
1791
+ function zaiResponseError(status, text) {
1792
+ let payload;
1793
+ try {
1794
+ payload = JSON.parse(text);
1795
+ } catch {
1796
+ if (status >= 200 && status < 300) return "Z.AI: Invalid JSON response.";
1797
+ }
1798
+ const error = zaiPayloadError(payload);
1799
+ if (error) return error;
1800
+ if (status < 200 || status >= 300) {
1801
+ return `Z.AI HTTP ${status}: ${HTTP_MESSAGES[status] ?? "API request failed."}`;
1802
+ }
1803
+ return void 0;
1804
+ }
1805
+ function errorCode(value) {
1806
+ if (typeof value === "number" && Number.isInteger(value) && value >= 0 && value <= 9999) {
1807
+ return String(value);
1808
+ }
1809
+ return typeof value === "string" && /^(?:0|[1-9]\d{0,3})$/u.test(value) ? value : void 0;
1810
+ }
1811
+ function asObject11(value) {
1812
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
1813
+ }
1814
+
1735
1815
  // src/providers/zai.ts
1736
1816
  var FIVE_HOUR_WINDOW_MINUTES2 = 300;
1737
1817
  var WEEKLY_WINDOW_MINUTES2 = 10080;
1738
1818
  function normalizeZaiQuotaPayload(providerId, providerName, payload, capturedAt, plan) {
1739
- const data = asObject11(payload.data);
1740
- if (!data) {
1741
- const reason = asString5(payload.msg);
1742
- throw new UsageUnsupportedError(
1743
- `No GLM Coding Plan on this Z.AI credential; API usage is not metered.${reason ? ` Z.AI reported: ${reason}` : ""}`
1744
- );
1745
- }
1819
+ const error = zaiPayloadError(payload);
1820
+ if (error) throw new Error(error);
1821
+ const data = asObject12(payload.data);
1822
+ if (!data) throw new Error("Z.AI quota response data was not an object.");
1746
1823
  const limits = Array.isArray(data.limits) ? data.limits : [];
1747
1824
  const buckets = [];
1748
1825
  const metrics = [];
1749
1826
  for (const raw of limits) {
1750
- const limit = asObject11(raw);
1827
+ const limit = asObject12(raw);
1751
1828
  if (!limit) continue;
1752
1829
  const type = asString5(limit.type);
1753
1830
  const unit = asNonnegativeNumber4(limit.unit);
@@ -1796,14 +1873,11 @@ function normalizeZaiQuotaPayload(providerId, providerName, payload, capturedAt,
1796
1873
  };
1797
1874
  }
1798
1875
  function normalizeZaiSubscriptionPayload(payload) {
1799
- if (payload.success === false) return void 0;
1800
- if (typeof payload.code === "number" && payload.code !== 0 && payload.code !== 200) {
1801
- return void 0;
1802
- }
1876
+ if (zaiPayloadError(payload)) return void 0;
1803
1877
  if (!Array.isArray(payload.data)) return void 0;
1804
1878
  const candidates = [];
1805
1879
  for (const raw of payload.data) {
1806
- const entry = asObject11(raw);
1880
+ const entry = asObject12(raw);
1807
1881
  if (!entry) continue;
1808
1882
  const name = asString5(entry.productName);
1809
1883
  if (!name) continue;
@@ -1881,7 +1955,7 @@ function addCountBucket(buckets, limit, id, label, windowMinutes) {
1881
1955
  function addUsageDetailMetrics(metrics, value) {
1882
1956
  if (!Array.isArray(value)) return;
1883
1957
  for (const raw of value) {
1884
- const detail = asObject11(raw);
1958
+ const detail = asObject12(raw);
1885
1959
  if (!detail) continue;
1886
1960
  const label = asString5(detail.modelCode);
1887
1961
  const usage = asNonnegativeNumber4(detail.usage);
@@ -1889,7 +1963,7 @@ function addUsageDetailMetrics(metrics, value) {
1889
1963
  metrics.push({ id: `mcp-${kebabCase(label)}`, label, value: usage, unit: "count" });
1890
1964
  }
1891
1965
  }
1892
- function asObject11(value) {
1966
+ function asObject12(value) {
1893
1967
  if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
1894
1968
  return value;
1895
1969
  }
@@ -2227,6 +2301,7 @@ var SUPPORTED_ADAPTERS = [
2227
2301
  {
2228
2302
  id: "zai",
2229
2303
  displayName: "Z.AI",
2304
+ invalidateCacheOnFailure: true,
2230
2305
  semantics: { kind: "consumer-subscription", label: "GLM Coding Plan usage" },
2231
2306
  async query(auth, signal, timeoutMs, guard) {
2232
2307
  return queryZaiUsage("zai", "Z.AI", auth, signal, timeoutMs, guard);
@@ -2235,6 +2310,7 @@ var SUPPORTED_ADAPTERS = [
2235
2310
  {
2236
2311
  id: "zai-coding-cn",
2237
2312
  displayName: "Z.AI Coding CN",
2313
+ invalidateCacheOnFailure: true,
2238
2314
  semantics: { kind: "consumer-subscription", label: "GLM Coding Plan usage" },
2239
2315
  async query(auth, signal, timeoutMs, guard) {
2240
2316
  return queryZaiUsage("zai-coding-cn", "Z.AI Coding CN", auth, signal, timeoutMs, guard);
@@ -2455,8 +2531,7 @@ async function queryProviderUsage(adapter, auth, signal, timeoutMs, guard, targe
2455
2531
  );
2456
2532
  } catch (error) {
2457
2533
  if (isStaleExtensionContextError(error) || isAbortError(error)) throw error;
2458
- const message = redactUsageError(errorMessage(error), auth.secrets);
2459
- throw error instanceof UsageUnsupportedError ? new UsageUnsupportedError(message) : new Error(message);
2534
+ throw new Error(redactUsageError(errorMessage(error), auth.secrets));
2460
2535
  }
2461
2536
  }
2462
2537
  function providerIsConfigured(ctx, providerId) {
@@ -2537,6 +2612,8 @@ async function fetchProviderJson(url, auth, signal, timeoutMs, description, requ
2537
2612
  );
2538
2613
  if (controller.signal.aborted)
2539
2614
  throw Object.assign(new Error("Usage query aborted."), { name: "AbortError" });
2615
+ const responseError = request.responseError?.(response.status, text);
2616
+ if (responseError) throw new Error(responseError);
2540
2617
  if (!response.ok) {
2541
2618
  throw new Error(
2542
2619
  `${description} returned ${response.status} ${response.statusText}: ${redactUsageError(text, auth.secrets)}`
@@ -2613,7 +2690,7 @@ function resolveXaiUsageAuth(auth, model, salt, candidates) {
2613
2690
  const matches = [];
2614
2691
  for (const candidate of candidates) {
2615
2692
  try {
2616
- const credential = asObject12(candidate);
2693
+ const credential = asObject13(candidate);
2617
2694
  if (credential?.type !== "oauth") continue;
2618
2695
  sawOAuth = true;
2619
2696
  if (credential.access !== resolvedAccess) continue;
@@ -2667,7 +2744,7 @@ function resolveGitHubCopilotUsageAuth(auth, model, salt, candidates, standalone
2667
2744
  const matches = /* @__PURE__ */ new Map();
2668
2745
  for (const candidate of candidates) {
2669
2746
  try {
2670
- const credential = asObject12(candidate);
2747
+ const credential = asObject13(candidate);
2671
2748
  if (credential?.type !== "oauth") continue;
2672
2749
  sawOAuth = true;
2673
2750
  const storedAccess2 = typeof credential.access === "string" && credential.access ? credential.access : void 0;
@@ -2729,7 +2806,7 @@ function bearerToken(authorization) {
2729
2806
  const match = /^Bearer\s+(.+)$/iu.exec(authorization ?? "");
2730
2807
  return match?.[1];
2731
2808
  }
2732
- function asObject12(value) {
2809
+ function asObject13(value) {
2733
2810
  if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
2734
2811
  return value;
2735
2812
  }
@@ -2858,7 +2935,8 @@ async function queryZaiUsage(providerId, providerName, auth, signal, timeoutMs,
2858
2935
  zaiMonitorAuth(auth),
2859
2936
  signal,
2860
2937
  remainingTimeout2(timeoutMs, startedAt, `fetching ${providerName} quota`),
2861
- `${providerName} quota endpoint`
2938
+ `${providerName} quota endpoint`,
2939
+ { responseError: zaiResponseError }
2862
2940
  );
2863
2941
  await guard();
2864
2942
  const planTimeoutMs = timeoutMs - (Date.now() - startedAt);
@@ -2873,7 +2951,8 @@ async function fetchZaiPlan(providerName, auth, signal, timeoutMs) {
2873
2951
  zaiMonitorAuth(auth),
2874
2952
  signal,
2875
2953
  timeoutMs,
2876
- `${providerName} plan endpoint`
2954
+ `${providerName} plan endpoint`,
2955
+ { responseError: zaiResponseError }
2877
2956
  );
2878
2957
  return normalizeZaiSubscriptionPayload(payload);
2879
2958
  } catch (error) {
@@ -3023,7 +3102,7 @@ function normalizeCodexResetCreditsPayload(payload) {
3023
3102
  if (rawCredits !== void 0 && !Array.isArray(rawCredits)) {
3024
3103
  throw new Error("Codex reset credits response returned invalid credits.");
3025
3104
  }
3026
- const options = (rawCredits ?? []).map(asObject13).filter((credit) => Boolean(credit)).filter((credit) => credit.status === "available" && credit.reset_type === "codex_rate_limits").map(normalizeResetOption).sort(
3105
+ const options = (rawCredits ?? []).map(asObject14).filter((credit) => Boolean(credit)).filter((credit) => credit.status === "available" && credit.reset_type === "codex_rate_limits").map(normalizeResetOption).sort(
3027
3106
  (left, right) => (left.expiresAt ?? Number.MAX_SAFE_INTEGER) - (right.expiresAt ?? Number.MAX_SAFE_INTEGER)
3028
3107
  ).slice(0, Math.min(availableCount, MAX_RESET_OPTIONS));
3029
3108
  if (availableCount > 0 && options.length === 0) {
@@ -3038,7 +3117,7 @@ function selectCodexResetCredential(candidates, resolvedAccess, resolvedAccountI
3038
3117
  const matches = /* @__PURE__ */ new Map();
3039
3118
  for (const candidate of candidates) {
3040
3119
  try {
3041
- const credential = asObject13(candidate);
3120
+ const credential = asObject14(candidate);
3042
3121
  if (credential?.type !== "oauth") continue;
3043
3122
  sawOAuth = true;
3044
3123
  const storedAccess = asNonemptyString(credential.access);
@@ -3077,7 +3156,7 @@ function codexAccountIdFromAccessToken(access) {
3077
3156
  const parts = access.split(".");
3078
3157
  if (parts.length !== 3 || !parts[1]) return void 0;
3079
3158
  const payload = JSON.parse(Buffer.from(parts[1], "base64url").toString("utf8"));
3080
- const claims = asObject13(asObject13(payload)?.["https://api.openai.com/auth"]);
3159
+ const claims = asObject14(asObject14(payload)?.["https://api.openai.com/auth"]);
3081
3160
  return validHeaderValue(claims?.chatgpt_account_id);
3082
3161
  } catch {
3083
3162
  return void 0;
@@ -3109,7 +3188,7 @@ function normalizeResetOption(credit) {
3109
3188
  function isCodexResetOutcomeCode(value) {
3110
3189
  return value === "reset" || value === "nothing_to_reset" || value === "no_credit" || value === "already_redeemed";
3111
3190
  }
3112
- function asObject13(value) {
3191
+ function asObject14(value) {
3113
3192
  if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
3114
3193
  return value;
3115
3194
  }
@@ -4217,33 +4296,15 @@ import { stripVTControlCharacters } from "node:util";
4217
4296
  import {
4218
4297
  getSettingsListTheme
4219
4298
  } from "@earendil-works/pi-coding-agent";
4220
- import {
4221
- Container,
4222
- Key,
4223
- matchesKey,
4224
- SettingsList,
4225
- Text
4226
- } from "@earendil-works/pi-tui";
4299
+ import { Key, matchesKey, SettingsList, Text } from "@earendil-works/pi-tui";
4227
4300
  var OFF = "Off";
4228
4301
  var ON = "On";
4229
- function fitSettingsRows(lines, maxRows) {
4230
- if (lines.length <= maxRows) return lines;
4231
- const content = lines.filter((line) => stripVTControlCharacters(line).trim().length > 0);
4232
- if (content.length <= maxRows) return content;
4233
- const selectedIndex = content.findIndex(
4234
- (line) => /^[→›]\s/u.test(stripVTControlCharacters(line))
4235
- );
4236
- if (selectedIndex < 0) return content.slice(0, maxRows);
4237
- return content.map((line, index) => ({ index, line })).sort(
4238
- (left, right) => Math.abs(left.index - selectedIndex) - Math.abs(right.index - selectedIndex) || left.index - right.index
4239
- ).slice(0, maxRows).sort((left, right) => left.index - right.index).map(({ line }) => line);
4240
- }
4241
4302
  async function showUsageSettings(ctx, settingsRuntime, parentSignal, isCurrent, onApplied) {
4242
4303
  if (ctx.mode !== "tui") {
4243
4304
  if (ctx.hasUI) ctx.ui.notify(`Edit settings manually: ${settingsRuntime.get().path}`, "info");
4244
4305
  return false;
4245
4306
  }
4246
- const { HorizontalRule } = await import("@narumitw/pi-tui-kit");
4307
+ const { HorizontalRule, renderBoundedFrame } = await import("@narumitw/pi-tui-kit");
4247
4308
  if (parentSignal.aborted || !isCurrent()) return false;
4248
4309
  return await ctx.ui.custom((tui, theme, _keybindings, done) => {
4249
4310
  const localController = new AbortController();
@@ -4268,9 +4329,7 @@ async function showUsageSettings(ctx, settingsRuntime, parentSignal, isCurrent,
4268
4329
  values: [OFF, ON]
4269
4330
  }
4270
4331
  ];
4271
- const container = new Container();
4272
4332
  const rule = new HorizontalRule({ ruleStyle: (text) => theme.fg("border", text) });
4273
- container.addChild(new Text(theme.fg("accent", theme.bold("pi-usage Settings")), 1, 1));
4274
4333
  let settingsList;
4275
4334
  const cancel = () => {
4276
4335
  if (closing) return;
@@ -4317,20 +4376,29 @@ async function showUsageSettings(ctx, settingsRuntime, parentSignal, isCurrent,
4317
4376
  },
4318
4377
  cancel
4319
4378
  );
4320
- container.addChild(settingsList);
4321
4379
  parentSignal.addEventListener("abort", cancel, { once: true });
4322
4380
  return {
4323
4381
  render(width) {
4324
- const content = container.render(width);
4382
+ const title = new Text(theme.fg("accent", theme.bold("pi-usage Settings")), 1, 0).render(
4383
+ width
4384
+ );
4385
+ const content = settingsList.render(width);
4386
+ const focusedRow = content.findIndex(
4387
+ (line) => /^[→›]\s/u.test(stripVTControlCharacters(line))
4388
+ );
4325
4389
  const terminalRows = Number.isFinite(tui.terminal?.rows) ? Math.floor(tui.terminal.rows) : 24;
4326
- const availableRows = Math.max(1, terminalRows - 3);
4327
- if (content.length + 2 > availableRows) {
4328
- return fitSettingsRows(content, availableRows);
4329
- }
4330
4390
  const [ruleLine = ""] = rule.render(width);
4331
- return [ruleLine, ...content, ruleLine];
4391
+ return renderBoundedFrame({
4392
+ width,
4393
+ maxRows: Math.max(1, terminalRows - 3),
4394
+ rule: ruleLine,
4395
+ title,
4396
+ content,
4397
+ priorityRows: focusedRow < 0 ? [] : [focusedRow],
4398
+ focusedRow
4399
+ });
4332
4400
  },
4333
- invalidate: () => container.invalidate(),
4401
+ invalidate: () => settingsList.invalidate(),
4334
4402
  handleInput(data) {
4335
4403
  if (closing) return;
4336
4404
  if (matchesKey(data, Key.ctrl("c"))) cancel();
@@ -4568,7 +4636,7 @@ function usageExtension(pi, dependencies = {}) {
4568
4636
  providerId: adapter.id,
4569
4637
  providerName,
4570
4638
  displayState,
4571
- status: previousDiscoveryFailure.status,
4639
+ status: "query-failed",
4572
4640
  message: previousDiscoveryFailure.message
4573
4641
  },
4574
4642
  fingerprint: auth.fingerprint,
@@ -4627,7 +4695,7 @@ function usageExtension(pi, dependencies = {}) {
4627
4695
  providerId: adapter.id,
4628
4696
  providerName,
4629
4697
  displayState,
4630
- status: previousFailure.status,
4698
+ status: "query-failed",
4631
4699
  message: previousFailure.message
4632
4700
  },
4633
4701
  fingerprint: auth.fingerprint,
@@ -4680,16 +4748,16 @@ function usageExtension(pi, dependencies = {}) {
4680
4748
  );
4681
4749
  }
4682
4750
  const message = errorMessage(error);
4683
- const status = error instanceof UsageUnsupportedError ? "unsupported" : "query-failed";
4684
4751
  const now = Date.now();
4685
4752
  for (const [key, failure] of failureBackoff) {
4686
4753
  if (failure.until <= now) failureBackoff.delete(key);
4687
4754
  }
4688
4755
  if (queryId === void 0 || latestQueries.get(failureKey) === queryId) {
4756
+ if (adapter.invalidateCacheOnFailure) cache.delete(adapter.id, queryFingerprint);
4689
4757
  setBoundedMap(
4690
4758
  failureBackoff,
4691
4759
  failureKey,
4692
- { until: now + FAILURE_BACKOFF_MS, message, status },
4760
+ { until: now + FAILURE_BACKOFF_MS, message },
4693
4761
  MAX_ACCOUNT_STATES
4694
4762
  );
4695
4763
  }
@@ -4698,7 +4766,7 @@ function usageExtension(pi, dependencies = {}) {
4698
4766
  providerId: adapter.id,
4699
4767
  providerName,
4700
4768
  displayState,
4701
- status,
4769
+ status: "query-failed",
4702
4770
  message
4703
4771
  },
4704
4772
  fingerprint: auth.fingerprint,