@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/dist/index.ts CHANGED
@@ -449,10 +449,81 @@ function clampPercent(value) {
449
449
  return Math.min(100, Math.max(0, value));
450
450
  }
451
451
 
452
+ // src/providers/deepseek.ts
453
+ var CURRENCIES = ["CNY", "USD"];
454
+ var BALANCE_FIELDS = [
455
+ ["total", "Total balance", "total_balance"],
456
+ ["granted", "Granted balance", "granted_balance"],
457
+ ["topped-up", "Topped-up balance", "topped_up_balance"]
458
+ ];
459
+ function normalizeDeepSeekBalancePayload(payload, capturedAt) {
460
+ if (typeof payload.is_available !== "boolean") {
461
+ throw new Error("DeepSeek API balance response availability was not a boolean.");
462
+ }
463
+ if (!Array.isArray(payload.balance_infos) || payload.balance_infos.length === 0) {
464
+ throw new Error("DeepSeek API balance response returned no balance information.");
465
+ }
466
+ const balances = /* @__PURE__ */ new Map();
467
+ for (const raw of payload.balance_infos) {
468
+ const balance = asObject2(raw);
469
+ if (!balance) throw new Error("DeepSeek API balance row was not an object.");
470
+ const currency = deepSeekCurrency(balance.currency);
471
+ if (!currency) throw new Error("DeepSeek API balance row returned an unsupported currency.");
472
+ if (balances.has(currency)) {
473
+ throw new Error(`DeepSeek API balance response repeated ${currency}.`);
474
+ }
475
+ for (const [, label, field] of BALANCE_FIELDS) {
476
+ if (!decimalAmount(balance[field])) {
477
+ throw new Error(`DeepSeek API balance ${label.toLowerCase()} was not a valid amount.`);
478
+ }
479
+ }
480
+ balances.set(currency, balance);
481
+ }
482
+ const metrics = [
483
+ {
484
+ id: "api-availability",
485
+ label: "API calls",
486
+ value: payload.is_available ? "available" : "unavailable"
487
+ }
488
+ ];
489
+ for (const currency of CURRENCIES) {
490
+ const balance = balances.get(currency);
491
+ if (!balance) continue;
492
+ for (const [id, label, field] of BALANCE_FIELDS) {
493
+ metrics.push({
494
+ id: `${currency.toLowerCase()}-${id}`,
495
+ label,
496
+ value: balance[field],
497
+ unit: "currency",
498
+ currency
499
+ });
500
+ }
501
+ }
502
+ return {
503
+ providerId: "deepseek",
504
+ providerName: "DeepSeek",
505
+ capturedAt,
506
+ source: "deepseek-balance",
507
+ semantics: { kind: "api-key", label: "DeepSeek API balance" },
508
+ buckets: [],
509
+ metrics
510
+ };
511
+ }
512
+ function asObject2(value) {
513
+ if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
514
+ return value;
515
+ }
516
+ function deepSeekCurrency(value) {
517
+ return CURRENCIES.find((currency) => currency === value);
518
+ }
519
+ function decimalAmount(value) {
520
+ return typeof value === "string" && value.length <= 64 && /^(?:0|[1-9]\d*)(?:\.\d+)?$/u.test(value);
521
+ }
522
+
452
523
  // src/providers/github-copilot.ts
453
524
  function normalizeGitHubCopilotUsagePayload(payload, capturedAt) {
454
- const snapshots = asObject2(payload.quota_snapshots);
455
- const premium = asObject2(snapshots?.premium_interactions);
525
+ const snapshots = asObject3(payload.quota_snapshots);
526
+ const premium = asObject3(snapshots?.premium_interactions);
456
527
  const metrics = [];
457
528
  let semanticsLabel;
458
529
  let bucket;
@@ -493,8 +564,8 @@ function normalizeGitHubCopilotUsagePayload(payload, capturedAt) {
493
564
  };
494
565
  }
495
566
  } else {
496
- const limited = asObject2(payload.limited_user_quotas);
497
- const monthly = asObject2(payload.monthly_quotas);
567
+ const limited = asObject3(payload.limited_user_quotas);
568
+ const monthly = asObject3(payload.monthly_quotas);
498
569
  const remaining = asNonnegativeNumber(limited?.chat);
499
570
  const entitlement = asNonnegativeNumber(monthly?.chat);
500
571
  if (remaining === void 0 || entitlement === void 0) {
@@ -533,7 +604,7 @@ function resetTimestamp(payload) {
533
604
  const milliseconds = Date.parse(raw);
534
605
  return Number.isNaN(milliseconds) ? {} : { resetsAt: Math.floor(milliseconds / 1e3) };
535
606
  }
536
- function asObject2(value) {
607
+ function asObject3(value) {
537
608
  if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
538
609
  return value;
539
610
  }
@@ -550,6 +621,219 @@ function asNonnegativeNumber(value) {
550
621
  return number === void 0 || number < 0 ? void 0 : number;
551
622
  }
552
623
 
624
+ // src/providers/kimi-coding.ts
625
+ var FIVE_HOUR_WINDOW_MINUTES = 300;
626
+ var DAILY_WINDOW_MINUTES = 1440;
627
+ var WEEKLY_WINDOW_MINUTES = 10080;
628
+ var FIXED_POINT_UNITS_PER_CENT = 1e6;
629
+ function normalizeKimiCodingUsagePayload(payload, capturedAt) {
630
+ const root = asObject4(payload);
631
+ if (!root) throw new Error("Kimi Coding usage response was not an object.");
632
+ const candidates = [];
633
+ let omittedWindow = false;
634
+ const summary = parseUsageRow(root.usage, WEEKLY_WINDOW_MINUTES, "Weekly window");
635
+ if (summary) candidates.push(summary);
636
+ else if (root.usage !== void 0) omittedWindow = true;
637
+ if (Array.isArray(root.limits)) {
638
+ for (const raw of root.limits) {
639
+ const item = asObject4(raw);
640
+ const windowMinutes = parseWindowMinutes(item?.window);
641
+ const label = sanitizedLabel(item?.name);
642
+ const bucket = windowMinutes === void 0 ? void 0 : parseUsageRow(item?.detail, windowMinutes, label ?? defaultWindowLabel(windowMinutes));
643
+ if (bucket) candidates.push(bucket);
644
+ else omittedWindow = true;
645
+ }
646
+ } else if (root.limits !== void 0) {
647
+ omittedWindow = true;
648
+ }
649
+ const buckets = [];
650
+ const byWindow = /* @__PURE__ */ new Map();
651
+ for (const bucket of candidates) {
652
+ const windowMinutes = bucket.windowMinutes;
653
+ byWindow.set(windowMinutes, [...byWindow.get(windowMinutes) ?? [], bucket]);
654
+ }
655
+ for (const rows of byWindow.values()) {
656
+ if (rows.length === 1) buckets.push(rows[0]);
657
+ else omittedWindow = true;
658
+ }
659
+ buckets.sort((left, right) => (left.windowMinutes ?? 0) - (right.windowMinutes ?? 0));
660
+ const metrics = parseBoosterWallet(root.boosterWallet);
661
+ if (buckets.length === 0 && metrics.length === 0) {
662
+ throw new Error("Kimi Coding usage endpoint returned no displayable usage data.");
663
+ }
664
+ return {
665
+ providerId: "kimi-coding",
666
+ providerName: "Kimi For Coding",
667
+ capturedAt,
668
+ source: "kimi-managed-usage",
669
+ semantics: { kind: "consumer-subscription", label: "Kimi Coding Plan usage" },
670
+ buckets,
671
+ metrics,
672
+ ...omittedWindow ? { notes: ["Unsupported, malformed, or duplicate plan windows were unavailable."] } : {}
673
+ };
674
+ }
675
+ function parseUsageRow(value, windowMinutes, label) {
676
+ const row = asObject4(value);
677
+ if (!row) return void 0;
678
+ const used = asNonnegativeInteger2(row.used);
679
+ const limit = asNonnegativeInteger2(row.limit);
680
+ if (used === void 0 || limit === void 0 || limit === 0) return void 0;
681
+ const resetsAt = asIsoEpochSeconds(row.resetTime);
682
+ return {
683
+ id: windowId(windowMinutes),
684
+ label,
685
+ used,
686
+ remaining: Math.max(0, limit - used),
687
+ limit,
688
+ unit: "count",
689
+ windowMinutes,
690
+ ...resetsAt !== void 0 ? { resetsAt } : {}
691
+ };
692
+ }
693
+ function parseWindowMinutes(value) {
694
+ const window = asObject4(value);
695
+ if (!window) return void 0;
696
+ const duration = asPositiveInteger(window.duration);
697
+ if (duration === void 0) return void 0;
698
+ const multiplier = window.timeUnit === "TIME_UNIT_MINUTE" ? 1 : window.timeUnit === "TIME_UNIT_HOUR" ? 60 : window.timeUnit === "TIME_UNIT_DAY" ? 1440 : window.timeUnit === "TIME_UNIT_WEEK" ? 10080 : void 0;
699
+ if (multiplier === void 0) return void 0;
700
+ const minutes = duration * multiplier;
701
+ return Number.isSafeInteger(minutes) ? minutes : void 0;
702
+ }
703
+ function parseBoosterWallet(value) {
704
+ const wallet = asObject4(value);
705
+ const balance = asObject4(wallet?.balance);
706
+ if (!wallet || !balance || balance.type !== "BOOSTER") return [];
707
+ const totalRaw = asPositiveInteger(balance.amount);
708
+ if (totalRaw === void 0) return [];
709
+ const leftRaw = asNonnegativeInteger2(balance.amountLeft) ?? 0;
710
+ const monthlyLimit = parseMoney(wallet.monthlyChargeLimit);
711
+ const monthlyUsed = parseMoney(wallet.monthlyUsed);
712
+ const currencies = new Set(
713
+ [monthlyLimit?.currency, monthlyUsed?.currency].filter(
714
+ (currency2) => currency2 !== void 0
715
+ )
716
+ );
717
+ if (currencies.size !== 1) return [];
718
+ const currency = currencies.values().next().value;
719
+ if (!currency) return [];
720
+ const total = fixedPointToMajor(totalRaw);
721
+ const left = fixedPointToMajor(leftRaw);
722
+ if (total === void 0 || left === void 0) return [];
723
+ const metrics = [
724
+ { id: "booster-balance", label: "Balance", value: left, unit: "currency", currency },
725
+ { id: "booster-total", label: "Total balance", value: total, unit: "currency", currency }
726
+ ];
727
+ if (monthlyUsed) {
728
+ metrics.push({
729
+ id: "booster-monthly-used",
730
+ label: "Used this month",
731
+ value: monthlyUsed.cents / 100,
732
+ unit: "currency",
733
+ currency
734
+ });
735
+ }
736
+ if (wallet.monthlyChargeLimitEnabled === false) {
737
+ metrics.push({
738
+ id: "booster-monthly-limit",
739
+ label: "Monthly limit",
740
+ value: "unlimited",
741
+ unit: "currency",
742
+ currency
743
+ });
744
+ } else if (wallet.monthlyChargeLimitEnabled === true && monthlyLimit) {
745
+ metrics.push({
746
+ id: "booster-monthly-limit",
747
+ label: "Monthly limit",
748
+ value: monthlyLimit.cents / 100,
749
+ unit: "currency",
750
+ currency
751
+ });
752
+ }
753
+ return metrics;
754
+ }
755
+ function parseMoney(value) {
756
+ const money = asObject4(value);
757
+ if (!money) return void 0;
758
+ const cents = asNonnegativeInteger2(money.priceInCents);
759
+ if (cents === void 0) return void 0;
760
+ const currency = asCurrency(money.currency);
761
+ if (!currency) return void 0;
762
+ return { cents, currency };
763
+ }
764
+ function fixedPointToMajor(value) {
765
+ const cents = value / FIXED_POINT_UNITS_PER_CENT;
766
+ const roundedCents = cents > 0 && cents < 1 ? 1 : Math.round(cents);
767
+ const major = roundedCents / 100;
768
+ return Number.isSafeInteger(roundedCents) && Number.isFinite(major) ? major : void 0;
769
+ }
770
+ function asObject4(value) {
771
+ if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
772
+ return value;
773
+ }
774
+ function asNonnegativeInteger2(value) {
775
+ if (typeof value === "string" && !/^\d+$/u.test(value)) return void 0;
776
+ const number = typeof value === "string" ? Number(value) : value;
777
+ if (typeof number !== "number" || !Number.isSafeInteger(number) || number < 0) return void 0;
778
+ return number;
779
+ }
780
+ function asPositiveInteger(value) {
781
+ const number = asNonnegativeInteger2(value);
782
+ return number !== void 0 && number > 0 ? number : void 0;
783
+ }
784
+ function asIsoEpochSeconds(value) {
785
+ if (typeof value !== "string") return void 0;
786
+ const match = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.\d+)?(?:Z|([+-])(\d{2}):(\d{2}))$/u.exec(
787
+ value
788
+ );
789
+ if (!match) return void 0;
790
+ const [
791
+ ,
792
+ yearText,
793
+ monthText,
794
+ dayText,
795
+ hourText,
796
+ minuteText,
797
+ secondText,
798
+ ,
799
+ offsetHour,
800
+ offsetMinute
801
+ ] = match;
802
+ const year = Number(yearText);
803
+ const month = Number(monthText);
804
+ const day = Number(dayText);
805
+ const hour = Number(hourText);
806
+ const minute = Number(minuteText);
807
+ const second = Number(secondText);
808
+ if (month < 1 || month > 12 || day < 1 || day > new Date(Date.UTC(year, month, 0)).getUTCDate() || hour > 23 || minute > 59 || second > 59 || offsetHour !== void 0 && Number(offsetHour) > 23 || offsetMinute !== void 0 && Number(offsetMinute) > 59) {
809
+ return void 0;
810
+ }
811
+ const millis = Date.parse(value);
812
+ return Number.isFinite(millis) && millis >= 0 ? Math.floor(millis / 1e3) : void 0;
813
+ }
814
+ function sanitizedLabel(value) {
815
+ if (typeof value !== "string") return void 0;
816
+ return sanitizeDisplayText(value, 80) || void 0;
817
+ }
818
+ function asCurrency(value) {
819
+ if (typeof value !== "string") return void 0;
820
+ const currency = sanitizeDisplayText(value, 3).toUpperCase();
821
+ return /^[A-Z]{3}$/u.test(currency) ? currency : void 0;
822
+ }
823
+ function windowId(minutes) {
824
+ if (minutes === FIVE_HOUR_WINDOW_MINUTES) return "five-hour";
825
+ if (minutes === DAILY_WINDOW_MINUTES) return "daily";
826
+ if (minutes === WEEKLY_WINDOW_MINUTES) return "weekly";
827
+ return `window-${minutes}-minutes`;
828
+ }
829
+ function defaultWindowLabel(minutes) {
830
+ if (minutes === WEEKLY_WINDOW_MINUTES) return "Weekly window";
831
+ if (minutes % 10080 === 0) return `${minutes / 10080}w window`;
832
+ if (minutes % 1440 === 0) return `${minutes / 1440}d window`;
833
+ if (minutes % 60 === 0) return `${minutes / 60}h window`;
834
+ return `${minutes}m window`;
835
+ }
836
+
553
837
  // src/providers/opencode-zen.ts
554
838
  var ZEN_WINDOWS = [
555
839
  { key: "rolling", label: "Rolling" },
@@ -557,12 +841,12 @@ var ZEN_WINDOWS = [
557
841
  { key: "monthly", label: "Monthly" }
558
842
  ];
559
843
  function normalizeOpenCodeZenPayload(payload, capturedAt) {
560
- const usage = asObject3(payload.usage);
844
+ const usage = asObject5(payload.usage);
561
845
  if (!usage) throw new Error("OpenCode Zen usage response was not an object.");
562
846
  const buckets = [];
563
847
  const notes = [];
564
848
  for (const window of ZEN_WINDOWS) {
565
- const raw = asObject3(usage[window.key]);
849
+ const raw = asObject5(usage[window.key]);
566
850
  if (!raw) continue;
567
851
  const status = asString3(raw.status);
568
852
  if (status !== "ok" && status !== "rate-limited") {
@@ -599,7 +883,7 @@ function normalizeOpenCodeZenPayload(payload, capturedAt) {
599
883
  ...notes.length > 0 ? { notes } : {}
600
884
  };
601
885
  }
602
- function asObject3(value) {
886
+ function asObject5(value) {
603
887
  if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
604
888
  return value;
605
889
  }
@@ -623,7 +907,7 @@ function clampPercent2(value) {
623
907
 
624
908
  // src/providers/openrouter.ts
625
909
  function normalizeOpenRouterKeyPayload(payload, capturedAt) {
626
- const data = asObject4(payload.data);
910
+ const data = asObject6(payload.data);
627
911
  if (!data) throw new Error("OpenRouter key response data was not an object.");
628
912
  const limit = asNonnegativeNumber3(data.limit);
629
913
  const remaining = asNonnegativeNumber3(data.limit_remaining);
@@ -669,7 +953,7 @@ function addUsageMetric(metrics, id, label, value) {
669
953
  if (amount === void 0) return;
670
954
  metrics.push({ id, label, value: amount, unit: "usd" });
671
955
  }
672
- function asObject4(value) {
956
+ function asObject6(value) {
673
957
  if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
674
958
  return value;
675
959
  }
@@ -682,17 +966,178 @@ function asNonnegativeNumber3(value) {
682
966
  return value;
683
967
  }
684
968
 
969
+ // src/providers/xai.ts
970
+ var MAX_SAFE_CENTS = Number.MAX_SAFE_INTEGER;
971
+ function normalizeXaiBillingPayload(payload, subscriptionTier, capturedAt) {
972
+ const configValue = payload.config;
973
+ if (configValue !== null && configValue !== void 0 && !isRecord2(configValue)) {
974
+ throw new Error("xAI billing response config was not an object or null.");
975
+ }
976
+ const config = isRecord2(configValue) ? configValue : void 0;
977
+ const buckets = [];
978
+ const metrics = [];
979
+ const notes = [];
980
+ if (config) {
981
+ const period = normalizePeriod(
982
+ config.currentPeriod,
983
+ config.billingPeriodStart,
984
+ config.billingPeriodEnd
985
+ );
986
+ const preferredPercent = optionalPercent(config.creditUsagePercent, "creditUsagePercent");
987
+ if (preferredPercent !== void 0) {
988
+ buckets.push({
989
+ id: "included-allowance",
990
+ label: "Included allowance",
991
+ used: preferredPercent,
992
+ remaining: 100 - preferredPercent,
993
+ unit: "percent",
994
+ ...period
995
+ });
996
+ } else {
997
+ const limit = optionalUsd(config.monthlyLimit, "monthlyLimit");
998
+ const used = optionalUsd(config.used, "used");
999
+ if (limit !== void 0 || used !== void 0) {
1000
+ buckets.push({
1001
+ id: "included-allowance",
1002
+ label: "Included allowance",
1003
+ ...limit !== void 0 ? { limit } : {},
1004
+ ...used !== void 0 ? { used } : {},
1005
+ ...limit !== void 0 && used !== void 0 ? { remaining: limit - used } : {},
1006
+ unit: "usd",
1007
+ ...period
1008
+ });
1009
+ } else if (period.period || period.resetsAt !== void 0) {
1010
+ buckets.push({
1011
+ id: "included-allowance",
1012
+ label: "Included allowance",
1013
+ unit: "percent",
1014
+ ...period
1015
+ });
1016
+ }
1017
+ }
1018
+ const onDemandCap = optionalUsd(config.onDemandCap, "onDemandCap");
1019
+ const onDemandUsed = optionalUsd(config.onDemandUsed, "onDemandUsed");
1020
+ if (onDemandCap !== void 0 || onDemandUsed !== void 0) {
1021
+ buckets.push({
1022
+ id: "on-demand",
1023
+ label: "On-demand usage",
1024
+ ...onDemandCap !== void 0 ? { limit: onDemandCap } : {},
1025
+ ...onDemandUsed !== void 0 ? { used: onDemandUsed } : {},
1026
+ ...onDemandCap !== void 0 && onDemandUsed !== void 0 ? { remaining: onDemandCap - onDemandUsed } : {},
1027
+ unit: "usd"
1028
+ });
1029
+ }
1030
+ const prepaidBalance = optionalUsd(config.prepaidBalance, "prepaidBalance");
1031
+ if (prepaidBalance !== void 0) {
1032
+ metrics.push({
1033
+ id: "prepaid-balance",
1034
+ label: "Prepaid balance",
1035
+ value: prepaidBalance,
1036
+ unit: "usd"
1037
+ });
1038
+ }
1039
+ }
1040
+ const tier = optionalTier(subscriptionTier);
1041
+ if (tier) metrics.push({ id: "subscription-tier", label: "Plan tier", value: tier });
1042
+ if (!config) notes.push("No xAI consumer billing configuration is available for this account.");
1043
+ else if (buckets.length === 0 && metrics.length === 0) {
1044
+ notes.push("The xAI consumer billing response contained no displayable usage fields.");
1045
+ }
1046
+ return {
1047
+ providerId: "xai",
1048
+ providerName: "xAI",
1049
+ capturedAt,
1050
+ source: "cli-chat-proxy.grok.com consumer billing",
1051
+ semantics: {
1052
+ kind: "consumer-subscription",
1053
+ label: "xAI consumer subscription usage"
1054
+ },
1055
+ buckets,
1056
+ metrics,
1057
+ ...notes.length > 0 ? { notes } : {}
1058
+ };
1059
+ }
1060
+ function normalizePeriod(currentPeriod, legacyStart, legacyEnd) {
1061
+ if (currentPeriod !== void 0 && currentPeriod !== null && !isRecord2(currentPeriod)) {
1062
+ throw new Error("xAI billing currentPeriod was not an object or null.");
1063
+ }
1064
+ if (isRecord2(currentPeriod)) {
1065
+ const type = optionalString(currentPeriod.type, "currentPeriod.type");
1066
+ const start2 = optionalTimestamp(currentPeriod.start, "currentPeriod.start");
1067
+ const end2 = optionalTimestamp(currentPeriod.end, "currentPeriod.end");
1068
+ return {
1069
+ ...periodLabel(type, start2) ? { period: periodLabel(type, start2) } : {},
1070
+ ...end2 !== void 0 ? { resetsAt: end2 } : {}
1071
+ };
1072
+ }
1073
+ const start = optionalTimestamp(legacyStart, "billingPeriodStart");
1074
+ const end = optionalTimestamp(legacyEnd, "billingPeriodEnd");
1075
+ return {
1076
+ ...start !== void 0 ? { period: "Monthly" } : {},
1077
+ ...end !== void 0 ? { resetsAt: end } : {}
1078
+ };
1079
+ }
1080
+ function periodLabel(type, start) {
1081
+ if (type === "USAGE_PERIOD_TYPE_WEEKLY") return "Weekly";
1082
+ if (type === "USAGE_PERIOD_TYPE_MONTHLY") return "Monthly";
1083
+ if (type)
1084
+ return sanitizeDisplayText(type.replace(/^USAGE_PERIOD_TYPE_/u, "").replaceAll("_", " "), 40);
1085
+ return start === void 0 ? void 0 : "Current period";
1086
+ }
1087
+ function optionalUsd(value, field) {
1088
+ if (value === void 0 || value === null) return void 0;
1089
+ if (!isRecord2(value)) throw new Error(`xAI billing ${field} was not a cent wrapper.`);
1090
+ const cents = value.val === void 0 ? 0 : value.val;
1091
+ if (!Number.isSafeInteger(cents) || Math.abs(cents) > MAX_SAFE_CENTS) {
1092
+ throw new Error(`xAI billing ${field}.val was not a safe signed integer.`);
1093
+ }
1094
+ return cents / 100;
1095
+ }
1096
+ function optionalPercent(value, field) {
1097
+ if (value === void 0 || value === null) return void 0;
1098
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 0 || value > 100) {
1099
+ throw new Error(`xAI billing ${field} was outside 0\u2013100.`);
1100
+ }
1101
+ return value;
1102
+ }
1103
+ function optionalTimestamp(value, field) {
1104
+ if (value === void 0 || value === null) return void 0;
1105
+ if (typeof value !== "string" || value.length > 80) {
1106
+ throw new Error(`xAI billing ${field} was not a bounded timestamp.`);
1107
+ }
1108
+ const milliseconds = Date.parse(value);
1109
+ if (!Number.isFinite(milliseconds)) throw new Error(`xAI billing ${field} was invalid.`);
1110
+ return Math.floor(milliseconds / 1e3);
1111
+ }
1112
+ function optionalString(value, field) {
1113
+ if (value === void 0 || value === null) return void 0;
1114
+ if (typeof value !== "string" || value.length > 80) {
1115
+ throw new Error(`xAI billing ${field} was not a bounded string.`);
1116
+ }
1117
+ return value;
1118
+ }
1119
+ function optionalTier(value) {
1120
+ if (value === void 0 || value === null) return void 0;
1121
+ if (typeof value !== "string" || value.length > 160) {
1122
+ throw new Error("xAI subscription tier was not a bounded string or null.");
1123
+ }
1124
+ return sanitizeDisplayText(value, 80) || void 0;
1125
+ }
1126
+ function isRecord2(value) {
1127
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1128
+ }
1129
+
685
1130
  // src/providers/zai.ts
686
- var FIVE_HOUR_WINDOW_MINUTES = 300;
687
- var WEEKLY_WINDOW_MINUTES = 10080;
1131
+ var FIVE_HOUR_WINDOW_MINUTES2 = 300;
1132
+ var WEEKLY_WINDOW_MINUTES2 = 10080;
688
1133
  function normalizeZaiQuotaPayload(providerId, providerName, payload, capturedAt) {
689
- const data = asObject5(payload.data);
1134
+ const data = asObject7(payload.data);
690
1135
  if (!data) throw new Error("Z.AI quota response data was not an object.");
691
1136
  const limits = Array.isArray(data.limits) ? data.limits : [];
692
1137
  const buckets = [];
693
1138
  const metrics = [];
694
1139
  for (const raw of limits) {
695
- const limit = asObject5(raw);
1140
+ const limit = asObject7(raw);
696
1141
  if (!limit) continue;
697
1142
  const type = asString5(limit.type);
698
1143
  const unit = asNonnegativeNumber4(limit.unit);
@@ -701,14 +1146,14 @@ function normalizeZaiQuotaPayload(providerId, providerName, payload, capturedAt)
701
1146
  addCountBucket(buckets, limit, "mcp-monthly", "MCP monthly allowance");
702
1147
  addUsageDetailMetrics(metrics, limit.usageDetails);
703
1148
  } else if (isPlanUsage && unit === 3) {
704
- addPercentBucket(buckets, limit, "five-hour", "5h window", FIVE_HOUR_WINDOW_MINUTES);
1149
+ addPercentBucket(buckets, limit, "five-hour", "5h window", FIVE_HOUR_WINDOW_MINUTES2);
705
1150
  } else if (isPlanUsage && unit === 6) {
706
1151
  const used = asNonnegativeNumber4(limit.currentValue);
707
1152
  const quota = asNonnegativeNumber4(limit.usage);
708
1153
  if (used !== void 0 && quota !== void 0) {
709
- addCountBucket(buckets, limit, "weekly", "Weekly window", WEEKLY_WINDOW_MINUTES);
1154
+ addCountBucket(buckets, limit, "weekly", "Weekly window", WEEKLY_WINDOW_MINUTES2);
710
1155
  } else {
711
- addPercentBucket(buckets, limit, "weekly", "Weekly window", WEEKLY_WINDOW_MINUTES);
1156
+ addPercentBucket(buckets, limit, "weekly", "Weekly window", WEEKLY_WINDOW_MINUTES2);
712
1157
  }
713
1158
  }
714
1159
  }
@@ -764,7 +1209,7 @@ function addCountBucket(buckets, limit, id, label, windowMinutes) {
764
1209
  function addUsageDetailMetrics(metrics, value) {
765
1210
  if (!Array.isArray(value)) return;
766
1211
  for (const raw of value) {
767
- const detail = asObject5(raw);
1212
+ const detail = asObject7(raw);
768
1213
  if (!detail) continue;
769
1214
  const label = asString5(detail.modelCode);
770
1215
  const usage = asNonnegativeNumber4(detail.usage);
@@ -772,7 +1217,7 @@ function addUsageDetailMetrics(metrics, value) {
772
1217
  metrics.push({ id: `mcp-${kebabCase(label)}`, label, value: usage, unit: "count" });
773
1218
  }
774
1219
  }
775
- function asObject5(value) {
1220
+ function asObject7(value) {
776
1221
  if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
777
1222
  return value;
778
1223
  }
@@ -798,9 +1243,18 @@ function clampPercent3(value) {
798
1243
 
799
1244
  // src/query.ts
800
1245
  var CODEX_USAGE_URL = "https://chatgpt.com/backend-api/wham/usage";
1246
+ var DEEPSEEK_BALANCE_URL = "https://api.deepseek.com/user/balance";
801
1247
  var GITHUB_COPILOT_USAGE_URL = "https://api.github.com/copilot_internal/user";
802
1248
  var OPENROUTER_KEY_URL = "https://openrouter.ai/api/v1/key";
803
1249
  var OPENCODE_GO_USAGE_URL = "https://opencode.ai/zen/go/v1/usage";
1250
+ var KIMI_CODING_USAGE_URL = "https://api.kimi.com/coding/v1/usages";
1251
+ var XAI_USER_URL = "https://cli-chat-proxy.grok.com/v1/user?include=subscription";
1252
+ var XAI_BILLING_URL = "https://cli-chat-proxy.grok.com/v1/billing?format=credits";
1253
+ var XAI_CLIENT_HEADERS = Object.freeze({
1254
+ "X-XAI-Token-Auth": "xai-grok-cli",
1255
+ "x-grok-client-version": "1.0.10",
1256
+ "x-grok-client-mode": "interactive"
1257
+ });
804
1258
  var MAX_SUCCESS_BODY_BYTES = 64 * 1024;
805
1259
  var MAX_ERROR_BODY_BYTES = 4 * 1024;
806
1260
  var AUTH_FINGERPRINT_SALT = randomBytes(32);
@@ -823,6 +1277,27 @@ var SUPPORTED_ADAPTERS = [
823
1277
  return normalizeCodexBackendPayload(payload, Date.now());
824
1278
  }
825
1279
  },
1280
+ {
1281
+ id: "deepseek",
1282
+ displayName: "DeepSeek",
1283
+ semantics: { kind: "api-key", label: "DeepSeek API balance" },
1284
+ async query(auth, signal, timeoutMs, guard) {
1285
+ if (!guard) throw new Error("DeepSeek API balance requires request-boundary revalidation.");
1286
+ const startedAt = Date.now();
1287
+ await guard();
1288
+ const remainingMs = timeoutMs - (Date.now() - startedAt);
1289
+ if (remainingMs <= 0) throw new Error("Timed out while revalidating DeepSeek runtime auth.");
1290
+ const payload = await fetchProviderJson(
1291
+ DEEPSEEK_BALANCE_URL,
1292
+ auth,
1293
+ signal,
1294
+ remainingMs,
1295
+ "DeepSeek API balance endpoint",
1296
+ { redirect: "error" }
1297
+ );
1298
+ return normalizeDeepSeekBalancePayload(payload, Date.now());
1299
+ }
1300
+ },
826
1301
  {
827
1302
  id: "github-copilot",
828
1303
  displayName: "GitHub Copilot",
@@ -871,11 +1346,26 @@ var SUPPORTED_ADAPTERS = [
871
1346
  return normalizeOpenCodeZenPayload(payload, Date.now());
872
1347
  }
873
1348
  },
1349
+ {
1350
+ id: "kimi-coding",
1351
+ displayName: "Kimi For Coding",
1352
+ semantics: { kind: "consumer-subscription", label: "Kimi Coding Plan usage" },
1353
+ async query(auth, signal, timeoutMs) {
1354
+ const payload = await fetchProviderJson(
1355
+ KIMI_CODING_USAGE_URL,
1356
+ auth,
1357
+ signal,
1358
+ timeoutMs,
1359
+ "Kimi Coding usage endpoint",
1360
+ { redirect: "error" }
1361
+ );
1362
+ return normalizeKimiCodingUsagePayload(payload, Date.now());
1363
+ }
1364
+ },
874
1365
  {
875
1366
  id: "zai",
876
1367
  displayName: "Z.AI",
877
1368
  semantics: { kind: "consumer-subscription", label: "GLM Coding Plan usage" },
878
- publishesStatusline: false,
879
1369
  async query(auth, signal, timeoutMs) {
880
1370
  const payload = await fetchProviderJson(
881
1371
  zaiMonitorUrl(auth.model.baseUrl),
@@ -891,7 +1381,6 @@ var SUPPORTED_ADAPTERS = [
891
1381
  id: "zai-coding-cn",
892
1382
  displayName: "Z.AI Coding CN",
893
1383
  semantics: { kind: "consumer-subscription", label: "GLM Coding Plan usage" },
894
- publishesStatusline: false,
895
1384
  async query(auth, signal, timeoutMs) {
896
1385
  const payload = await fetchProviderJson(
897
1386
  zaiMonitorUrl(auth.model.baseUrl),
@@ -909,8 +1398,55 @@ var SUPPORTED_ADAPTERS = [
909
1398
  }
910
1399
  }
911
1400
  ];
1401
+ var XAI_ADAPTER = {
1402
+ id: "xai",
1403
+ displayName: "xAI",
1404
+ semantics: {
1405
+ kind: "consumer-subscription",
1406
+ label: "xAI consumer subscription usage"
1407
+ },
1408
+ publishesStatusline: false,
1409
+ async query(auth, signal, timeoutMs, guard) {
1410
+ if (!guard) throw new Error("xAI usage requires request-boundary revalidation.");
1411
+ const startedAt = Date.now();
1412
+ const clientAuth = {
1413
+ ...auth,
1414
+ headers: { ...auth.headers, ...XAI_CLIENT_HEADERS }
1415
+ };
1416
+ await guard();
1417
+ const userPayload = await fetchProviderJson(
1418
+ XAI_USER_URL,
1419
+ clientAuth,
1420
+ signal,
1421
+ remainingTimeout(timeoutMs, startedAt),
1422
+ "xAI consumer identity endpoint",
1423
+ { redirect: "error", userAgent: false }
1424
+ );
1425
+ await guard();
1426
+ const userId = validatedXaiUserId(userPayload.userId);
1427
+ const billingAuth = {
1428
+ ...clientAuth,
1429
+ headers: { ...clientAuth.headers, "x-userid": userId },
1430
+ secrets: [...clientAuth.secrets, userId]
1431
+ };
1432
+ await guard();
1433
+ const billingPayload = await fetchProviderJson(
1434
+ XAI_BILLING_URL,
1435
+ billingAuth,
1436
+ signal,
1437
+ remainingTimeout(timeoutMs, startedAt),
1438
+ "xAI consumer billing endpoint",
1439
+ { redirect: "error", userAgent: false }
1440
+ );
1441
+ await guard();
1442
+ return normalizeXaiBillingPayload(billingPayload, userPayload.subscriptionTier, Date.now());
1443
+ }
1444
+ };
1445
+ function usageAdapters() {
1446
+ return [...SUPPORTED_ADAPTERS, XAI_ADAPTER];
1447
+ }
912
1448
  function adapterForProvider(providerId) {
913
- return SUPPORTED_ADAPTERS.find((adapter) => adapter.id === providerId);
1449
+ return usageAdapters().find((adapter) => adapter.id === providerId);
914
1450
  }
915
1451
  function isStaleExtensionContextError(error) {
916
1452
  return error instanceof Error && error.message.includes("This extension ctx is stale after session replacement or reload");
@@ -927,11 +1463,14 @@ async function resolveUsageAuth(ctx, adapter, salt = AUTH_FINGERPRINT_SALT, cred
927
1463
  if (!model) return void 0;
928
1464
  const registry = ctx.modelRegistry;
929
1465
  let modelAuth;
930
- if (ctx.model?.provider === adapter.id && typeof registry.getApiKeyAndHeaders === "function") {
931
- const result = await registry.getApiKeyAndHeaders(ctx.model);
1466
+ const currentModel = ctx.model?.provider === adapter.id ? ctx.model : void 0;
1467
+ const resolveCurrentModelAuth = async () => {
1468
+ if (!currentModel || typeof registry.getApiKeyAndHeaders !== "function") return void 0;
1469
+ const result = await registry.getApiKeyAndHeaders(currentModel);
932
1470
  if (!result.ok) throw new Error(redactUsageError(result.error));
933
- if (authorizationFrom(result)) modelAuth = result;
934
- }
1471
+ return authorizationFrom(result) ? result : void 0;
1472
+ };
1473
+ if (adapter.id !== "deepseek") modelAuth = await resolveCurrentModelAuth();
935
1474
  if (typeof registry.getProviderAuth !== "function") {
936
1475
  throw new Error("pi-usage requires Pi 0.81.0 or newer to validate resolved provider auth.");
937
1476
  }
@@ -941,6 +1480,7 @@ async function resolveUsageAuth(ctx, adapter, salt = AUTH_FINGERPRINT_SALT, cred
941
1480
  `${adapter.displayName} usage cannot send a proxy-resolved credential to the official usage endpoint.`
942
1481
  );
943
1482
  }
1483
+ if (adapter.id === "deepseek") modelAuth = await resolveCurrentModelAuth();
944
1484
  const auth = modelAuth ?? providerResult?.auth;
945
1485
  if (!auth) return void 0;
946
1486
  if (adapter.id === "github-copilot") {
@@ -956,6 +1496,31 @@ async function resolveUsageAuth(ctx, adapter, salt = AUTH_FINGERPRINT_SALT, cred
956
1496
  offered.offeredCount === 0
957
1497
  );
958
1498
  }
1499
+ if (adapter.id === "xai") {
1500
+ const offered = candidateReader ? candidateReader(ctx, adapter.id) : fallbackOAuthCredentialCandidates(adapter.id, credentialReader);
1501
+ if (!offered.ok) throw new Error("xAI OAuth credential discovery failed closed.");
1502
+ return resolveXaiUsageAuth(auth, model, salt, offered.candidates);
1503
+ }
1504
+ if (adapter.id === "deepseek") {
1505
+ const resolvedAuthorization = authorizationFrom(auth);
1506
+ const access = bearerToken(resolvedAuthorization);
1507
+ if (!access) throw new Error("DeepSeek API balance requires Bearer authentication.");
1508
+ const authorization2 = `Bearer ${access}`;
1509
+ const headers2 = { Authorization: authorization2 };
1510
+ return {
1511
+ apiKey: access,
1512
+ headers: headers2,
1513
+ fingerprint: fingerprintResolvedAuth({ headers: headers2 }, salt),
1514
+ secrets: [
1515
+ access,
1516
+ auth.apiKey,
1517
+ headerValue(auth.headers, "Authorization"),
1518
+ resolvedAuthorization,
1519
+ authorization2
1520
+ ].filter((value) => Boolean(value)),
1521
+ model
1522
+ };
1523
+ }
959
1524
  const authorization = authorizationFrom(auth);
960
1525
  if (!authorization) return void 0;
961
1526
  const headers = { Authorization: authorization };
@@ -970,9 +1535,9 @@ async function resolveUsageAuth(ctx, adapter, salt = AUTH_FINGERPRINT_SALT, cred
970
1535
  model
971
1536
  };
972
1537
  }
973
- async function queryProviderUsage(adapter, auth, signal, timeoutMs) {
1538
+ async function queryProviderUsage(adapter, auth, signal, timeoutMs, guard) {
974
1539
  try {
975
- return await adapter.query(auth, signal, timeoutMs);
1540
+ return await adapter.query(auth, signal, timeoutMs, guard);
976
1541
  } catch (error) {
977
1542
  if (isStaleExtensionContextError(error) || isAbortError(error)) throw error;
978
1543
  throw new Error(redactUsageError(errorMessage(error), auth.secrets));
@@ -1012,7 +1577,9 @@ async function fetchProviderJson(url, auth, signal, timeoutMs, description, requ
1012
1577
  }, timeoutMs);
1013
1578
  try {
1014
1579
  const headers = { ...auth.headers };
1015
- if (!hasHeader(headers, "User-Agent")) headers["User-Agent"] = "pi-usage";
1580
+ if (request.userAgent !== false && !hasHeader(headers, "User-Agent")) {
1581
+ headers["User-Agent"] = "pi-usage";
1582
+ }
1016
1583
  if (request.body && !hasHeader(headers, "Content-Type")) {
1017
1584
  headers["Content-Type"] = "application/json";
1018
1585
  }
@@ -1020,15 +1587,18 @@ async function fetchProviderJson(url, auth, signal, timeoutMs, description, requ
1020
1587
  method: request.method ?? "GET",
1021
1588
  headers,
1022
1589
  ...request.body ? { body: JSON.stringify(request.body) } : {},
1590
+ ...request.redirect ? { redirect: request.redirect } : {},
1023
1591
  signal: controller.signal
1024
1592
  });
1593
+ if (response.redirected) throw new Error(`${description} refused a redirected response.`);
1025
1594
  if (controller.signal.aborted)
1026
1595
  throw Object.assign(new Error("Usage query aborted."), { name: "AbortError" });
1027
1596
  const text = await readBoundedResponse(
1028
1597
  response,
1029
1598
  response.ok ? MAX_SUCCESS_BODY_BYTES : MAX_ERROR_BODY_BYTES,
1030
1599
  !response.ok,
1031
- description
1600
+ description,
1601
+ controller.signal
1032
1602
  );
1033
1603
  if (controller.signal.aborted)
1034
1604
  throw Object.assign(new Error("Usage query aborted."), { name: "AbortError" });
@@ -1059,12 +1629,15 @@ async function fetchProviderJson(url, auth, signal, timeoutMs, description, requ
1059
1629
  signal.removeEventListener("abort", abortFromCaller);
1060
1630
  }
1061
1631
  }
1062
- async function readBoundedResponse(response, maxBytes, truncateOverflow, description) {
1632
+ async function readBoundedResponse(response, maxBytes, truncateOverflow, description, signal) {
1063
1633
  if (!response.body) return "";
1064
1634
  const reader = response.body.getReader();
1065
1635
  const chunks = [];
1066
1636
  let total = 0;
1067
1637
  let truncated = false;
1638
+ const abort = () => void reader.cancel().catch(() => void 0);
1639
+ if (signal.aborted) abort();
1640
+ else signal.addEventListener("abort", abort, { once: true });
1068
1641
  try {
1069
1642
  while (true) {
1070
1643
  const { done, value } = await reader.read();
@@ -1081,6 +1654,7 @@ async function readBoundedResponse(response, maxBytes, truncateOverflow, descrip
1081
1654
  total += value.byteLength;
1082
1655
  }
1083
1656
  } finally {
1657
+ signal.removeEventListener("abort", abort);
1084
1658
  reader.releaseLock();
1085
1659
  }
1086
1660
  if (truncated && !truncateOverflow) {
@@ -1095,6 +1669,59 @@ async function readBoundedResponse(response, maxBytes, truncateOverflow, descrip
1095
1669
  const text = new TextDecoder().decode(body);
1096
1670
  return truncated ? `${text}\u2026` : text;
1097
1671
  }
1672
+ function resolveXaiUsageAuth(auth, model, salt, candidates) {
1673
+ const resolvedAccess = bearerToken(headerValue(auth.headers, "Authorization")) ?? auth.apiKey;
1674
+ if (!resolvedAccess) throw new Error("xAI runtime authentication was incomplete.");
1675
+ let sawOAuth = false;
1676
+ let sawMatchingAccess = false;
1677
+ let sawIncompleteMatch = false;
1678
+ const matches = [];
1679
+ for (const candidate of candidates) {
1680
+ try {
1681
+ const credential = asObject8(candidate);
1682
+ if (credential?.type !== "oauth") continue;
1683
+ sawOAuth = true;
1684
+ if (credential.access !== resolvedAccess) continue;
1685
+ sawMatchingAccess = true;
1686
+ if (typeof credential.access !== "string" || !credential.access || typeof credential.refresh !== "string" || !credential.refresh || typeof credential.expires !== "number" || !Number.isFinite(credential.expires)) {
1687
+ sawIncompleteMatch = true;
1688
+ continue;
1689
+ }
1690
+ matches.push({ access: credential.access, refresh: credential.refresh });
1691
+ } catch {
1692
+ }
1693
+ }
1694
+ if (sawIncompleteMatch) throw new Error("The matching xAI OAuth credential was incomplete.");
1695
+ if (matches.length > 1) {
1696
+ throw new Error("Multiple OAuth credentials match the active xAI runtime account.");
1697
+ }
1698
+ const match = matches[0];
1699
+ if (!match) {
1700
+ if (!sawOAuth) {
1701
+ throw new Error(
1702
+ "xAI consumer usage requires the OAuth subscription account configured through Pi /login; XAI_API_KEY users can review API spend at console.x.ai."
1703
+ );
1704
+ }
1705
+ if (sawMatchingAccess) throw new Error("The matching xAI OAuth credential was incomplete.");
1706
+ throw new Error("The active xAI runtime account does not match Pi's stored OAuth account.");
1707
+ }
1708
+ const authorization = `Bearer ${match.access}`;
1709
+ const headers = { Authorization: authorization };
1710
+ return {
1711
+ apiKey: match.access,
1712
+ headers,
1713
+ fingerprint: fingerprintResolvedAuth({ headers }, salt),
1714
+ secrets: [
1715
+ match.access,
1716
+ match.refresh,
1717
+ resolvedAccess,
1718
+ auth.apiKey,
1719
+ headerValue(auth.headers, "Authorization"),
1720
+ authorization
1721
+ ].filter((value) => Boolean(value)),
1722
+ model
1723
+ };
1724
+ }
1098
1725
  function resolveGitHubCopilotUsageAuth(auth, model, salt, candidates, standaloneFallback) {
1099
1726
  const resolvedAccess = bearerToken(headerValue(auth.headers, "Authorization")) ?? auth.apiKey;
1100
1727
  if (!resolvedAccess) throw new Error("GitHub Copilot OAuth credentials were incomplete.");
@@ -1105,7 +1732,7 @@ function resolveGitHubCopilotUsageAuth(auth, model, salt, candidates, standalone
1105
1732
  const matches = /* @__PURE__ */ new Map();
1106
1733
  for (const candidate of candidates) {
1107
1734
  try {
1108
- const credential = asObject6(candidate);
1735
+ const credential = asObject8(candidate);
1109
1736
  if (credential?.type !== "oauth") continue;
1110
1737
  sawOAuth = true;
1111
1738
  const storedAccess2 = typeof credential.access === "string" && credential.access ? credential.access : void 0;
@@ -1167,7 +1794,7 @@ function bearerToken(authorization) {
1167
1794
  const match = /^Bearer\s+(.+)$/iu.exec(authorization ?? "");
1168
1795
  return match?.[1];
1169
1796
  }
1170
- function asObject6(value) {
1797
+ function asObject8(value) {
1171
1798
  if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
1172
1799
  return value;
1173
1800
  }
@@ -1186,8 +1813,11 @@ function hasOfficialUrlOrigin(value, providerId) {
1186
1813
  try {
1187
1814
  const url = new URL(value);
1188
1815
  if (providerId === "openai-codex") return url.origin === "https://chatgpt.com";
1816
+ if (providerId === "deepseek") return url.origin === "https://api.deepseek.com";
1189
1817
  if (providerId === "openrouter") return url.origin === "https://openrouter.ai";
1190
1818
  if (providerId === "opencode-go") return url.origin === "https://opencode.ai";
1819
+ if (providerId === "kimi-coding") return url.origin === "https://api.kimi.com";
1820
+ if (providerId === "xai") return url.origin === "https://api.x.ai";
1191
1821
  if (providerId === "zai") return url.origin === "https://api.z.ai";
1192
1822
  if (providerId === "zai-coding-cn") return url.origin === "https://open.bigmodel.cn";
1193
1823
  if (providerId === "github-copilot") {
@@ -1207,6 +1837,17 @@ function headerValue(headers, name) {
1207
1837
  function hasHeader(headers, name) {
1208
1838
  return Object.keys(headers).some((key) => key.toLowerCase() === name.toLowerCase());
1209
1839
  }
1840
+ function validatedXaiUserId(value) {
1841
+ if (typeof value !== "string" || !/^[A-Za-z0-9._~-]{1,128}$/u.test(value)) {
1842
+ throw new Error("xAI consumer identity returned an unsafe canonical user ID.");
1843
+ }
1844
+ return value;
1845
+ }
1846
+ function remainingTimeout(timeoutMs, startedAt) {
1847
+ const remaining = timeoutMs - (Date.now() - startedAt);
1848
+ if (remaining <= 0) throw new Error("Timed out while fetching xAI consumer usage.");
1849
+ return remaining;
1850
+ }
1210
1851
  function zaiMonitorUrl(baseUrl) {
1211
1852
  const base = baseUrl?.trim();
1212
1853
  if (!base) throw new Error("Z.AI model base URL is unavailable.");
@@ -1360,7 +2001,7 @@ function normalizeCodexResetCreditsPayload(payload) {
1360
2001
  if (rawCredits !== void 0 && !Array.isArray(rawCredits)) {
1361
2002
  throw new Error("Codex reset credits response returned invalid credits.");
1362
2003
  }
1363
- const options = (rawCredits ?? []).map(asObject7).filter((credit) => Boolean(credit)).filter((credit) => credit.status === "available" && credit.reset_type === "codex_rate_limits").map(normalizeResetOption).sort(
2004
+ const options = (rawCredits ?? []).map(asObject9).filter((credit) => Boolean(credit)).filter((credit) => credit.status === "available" && credit.reset_type === "codex_rate_limits").map(normalizeResetOption).sort(
1364
2005
  (left, right) => (left.expiresAt ?? Number.MAX_SAFE_INTEGER) - (right.expiresAt ?? Number.MAX_SAFE_INTEGER)
1365
2006
  ).slice(0, Math.min(availableCount, MAX_RESET_OPTIONS));
1366
2007
  if (availableCount > 0 && options.length === 0) {
@@ -1375,7 +2016,7 @@ function selectCodexResetCredential(candidates, resolvedAccess, resolvedAccountI
1375
2016
  const matches = /* @__PURE__ */ new Map();
1376
2017
  for (const candidate of candidates) {
1377
2018
  try {
1378
- const credential = asObject7(candidate);
2019
+ const credential = asObject9(candidate);
1379
2020
  if (credential?.type !== "oauth") continue;
1380
2021
  sawOAuth = true;
1381
2022
  const storedAccess = asNonemptyString(credential.access);
@@ -1414,7 +2055,7 @@ function codexAccountIdFromAccessToken(access) {
1414
2055
  const parts = access.split(".");
1415
2056
  if (parts.length !== 3 || !parts[1]) return void 0;
1416
2057
  const payload = JSON.parse(Buffer.from(parts[1], "base64url").toString("utf8"));
1417
- const claims = asObject7(asObject7(payload)?.["https://api.openai.com/auth"]);
2058
+ const claims = asObject9(asObject9(payload)?.["https://api.openai.com/auth"]);
1418
2059
  return validHeaderValue(claims?.chatgpt_account_id);
1419
2060
  } catch {
1420
2061
  return void 0;
@@ -1446,7 +2087,7 @@ function normalizeResetOption(credit) {
1446
2087
  function isCodexResetOutcomeCode(value) {
1447
2088
  return value === "reset" || value === "nothing_to_reset" || value === "no_credit" || value === "already_redeemed";
1448
2089
  }
1449
- function asObject7(value) {
2090
+ function asObject9(value) {
1450
2091
  if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
1451
2092
  return value;
1452
2093
  }
@@ -1487,13 +2128,17 @@ var BAR_SEGMENTS = 20;
1487
2128
  var VALUE_COLUMN = 29;
1488
2129
  function formatUsageReport(report, displayState) {
1489
2130
  const stateLabel = displayState === "current" ? "Current" : "Configured";
1490
- const lines = [`${report.providerName} Usage \xB7 ${stateLabel}`];
2131
+ const title = report.providerId === "deepseek" ? "DeepSeek API Balance" : `${report.providerName} Usage`;
2132
+ const lines = [`${title} \xB7 ${stateLabel}`];
1491
2133
  if (report.accountLabel) lines.push(`Account: ${report.accountLabel}`);
1492
2134
  lines.push(`Semantics: ${report.semantics.label}`, "");
1493
2135
  if (report.providerId === "openai-codex") formatCodexReport(lines, report);
2136
+ else if (report.providerId === "deepseek") formatDeepSeekReport(lines, report);
1494
2137
  else if (report.providerId === "github-copilot") formatGitHubCopilotReport(lines, report);
1495
2138
  else if (report.providerId === "openrouter") formatOpenRouterReport(lines, report);
1496
2139
  else if (report.providerId === "opencode-go") formatOpenCodeZenReport(lines, report);
2140
+ else if (report.providerId === "kimi-coding") formatKimiCodingReport(lines, report);
2141
+ else if (report.providerId === "xai") formatXaiReport(lines, report);
1497
2142
  else if (report.providerId === "zai" || report.providerId === "zai-coding-cn") {
1498
2143
  formatZaiReport(lines, report);
1499
2144
  } else formatGenericReport(lines, report);
@@ -1502,8 +2147,11 @@ function formatUsageReport(report, displayState) {
1502
2147
  }
1503
2148
  return lines.join("\n").trimEnd();
1504
2149
  }
1505
- function formatUsageStatusline(report, model) {
1506
- if (report.providerId === "openai-codex") return formatCodexStatusline(report, model);
2150
+ function formatUsageStatusline(report, model, now = Date.now(), showCodexResetCountdown = true) {
2151
+ if (report.providerId === "openai-codex") {
2152
+ return formatCodexStatusline(report, model, now, showCodexResetCountdown);
2153
+ }
2154
+ if (report.providerId === "deepseek") return formatDeepSeekStatusline(report);
1507
2155
  if (report.providerId === "github-copilot") return formatGitHubCopilotStatusline(report);
1508
2156
  if (report.providerId === "openrouter") {
1509
2157
  const limit = report.buckets.find((bucket) => bucket.id === "key-limit");
@@ -1512,6 +2160,10 @@ function formatUsageStatusline(report, model) {
1512
2160
  if (typeof total?.value === "number") return `openrouter ${formatUsd(total.value)} used`;
1513
2161
  }
1514
2162
  if (report.providerId === "opencode-go") return formatOpenCodeZenStatusline(report);
2163
+ if (report.providerId === "kimi-coding") return formatKimiCodingStatusline(report);
2164
+ if (report.providerId === "zai" || report.providerId === "zai-coding-cn") {
2165
+ return formatZaiStatusline(report);
2166
+ }
1515
2167
  return void 0;
1516
2168
  }
1517
2169
  function formatProviderStates(states) {
@@ -1545,6 +2197,31 @@ function formatCodexReport(lines, report) {
1545
2197
  }
1546
2198
  }
1547
2199
  }
2200
+ function formatDeepSeekReport(lines, report) {
2201
+ const availability = report.metrics.find((metric) => metric.id === "api-availability");
2202
+ lines.push(
2203
+ `${"API calls:".padEnd(VALUE_COLUMN)}${availability?.value === "available" ? "Available" : "Unavailable"}`
2204
+ );
2205
+ for (const currency of ["CNY", "USD"]) {
2206
+ const metrics = report.metrics.filter((metric) => metric.currency === currency);
2207
+ if (metrics.length === 0) continue;
2208
+ lines.push("", `${currency} balance:`);
2209
+ for (const metric of metrics) {
2210
+ lines.push(`${`${metric.label}:`.padEnd(VALUE_COLUMN)}${currency} ${metric.value}`);
2211
+ }
2212
+ }
2213
+ }
2214
+ function formatDeepSeekStatusline(report) {
2215
+ const availability = report.metrics.find((metric) => metric.id === "api-availability");
2216
+ if (availability?.value !== "available") return "deepseek API unavailable";
2217
+ const totals = ["CNY", "USD"].flatMap((currency) => {
2218
+ const metric = report.metrics.find(
2219
+ (candidate) => candidate.id === `${currency.toLowerCase()}-total`
2220
+ );
2221
+ return metric ? [`${currency} ${metric.value}`] : [];
2222
+ });
2223
+ return totals.length > 0 ? `deepseek ${totals.join(" \xB7 ")}` : "deepseek balance unavailable";
2224
+ }
1548
2225
  function formatGitHubCopilotReport(lines, report) {
1549
2226
  const quota = findGitHubCopilotQuota(report);
1550
2227
  if (!quota || quota.limit === void 0 || quota.remaining === void 0) {
@@ -1614,6 +2291,102 @@ function formatOpenCodeZenStatusline(report) {
1614
2291
  }
1615
2292
  return parts.length > 1 ? parts.join(" ") : void 0;
1616
2293
  }
2294
+ function formatKimiCodingReport(lines, report) {
2295
+ for (const bucket of report.buckets) {
2296
+ const reset = bucket.resetsAt ? ` (resets ${formatReset(bucket.resetsAt)})` : "";
2297
+ if (bucket.used === void 0 || bucket.limit === void 0) {
2298
+ lines.push(`${`${bucket.label}:`.padEnd(VALUE_COLUMN)}unavailable${reset}`);
2299
+ continue;
2300
+ }
2301
+ lines.push(
2302
+ `${`${bucket.label}:`.padEnd(VALUE_COLUMN)}${bucket.used} of ${bucket.limit} used \xB7 ${percentRemaining(bucket)}% left${reset}`
2303
+ );
2304
+ }
2305
+ const balance = report.metrics.find((metric) => metric.id === "booster-balance");
2306
+ const total = report.metrics.find((metric) => metric.id === "booster-total");
2307
+ const monthlyUsed = report.metrics.find((metric) => metric.id === "booster-monthly-used");
2308
+ const monthlyLimit = report.metrics.find((metric) => metric.id === "booster-monthly-limit");
2309
+ if (!balance && !monthlyUsed && !monthlyLimit) return;
2310
+ lines.push("", "Extra usage wallet:");
2311
+ if (balance) {
2312
+ const totalSuffix = total ? ` of ${formatCurrencyMetric(total)}` : "";
2313
+ lines.push(`${"Balance:".padEnd(VALUE_COLUMN)}${formatCurrencyMetric(balance)}${totalSuffix}`);
2314
+ }
2315
+ if (monthlyUsed) {
2316
+ lines.push(`${"Used this month:".padEnd(VALUE_COLUMN)}${formatCurrencyMetric(monthlyUsed)}`);
2317
+ }
2318
+ if (monthlyLimit) {
2319
+ lines.push(`${"Monthly limit:".padEnd(VALUE_COLUMN)}${formatCurrencyMetric(monthlyLimit)}`);
2320
+ }
2321
+ }
2322
+ function formatKimiCodingStatusline(report) {
2323
+ const fiveHour = report.buckets.find((bucket) => bucket.id === "five-hour");
2324
+ const weekly = report.buckets.find((bucket) => bucket.id === "weekly");
2325
+ const subWindow = fiveHour ?? report.buckets.find((bucket) => bucket.id !== "weekly");
2326
+ const selected = [subWindow, weekly].filter(
2327
+ (bucket, index, buckets) => bucket !== void 0 && buckets.indexOf(bucket) === index
2328
+ );
2329
+ const parts = ["kimi"];
2330
+ for (const bucket of selected) {
2331
+ if (!bucket.limit || bucket.remaining === void 0) continue;
2332
+ const fallback = bucket.id === "weekly" ? "weekly" : "5h";
2333
+ parts.push(
2334
+ `${percentRemaining(bucket)}% ${formatWindowLabel(bucket.windowMinutes, fallback, true)}`
2335
+ );
2336
+ }
2337
+ return parts.length > 1 ? parts.join(" ") : void 0;
2338
+ }
2339
+ function formatZaiStatusline(report) {
2340
+ const selected = [
2341
+ report.buckets.find((bucket) => bucket.id === "five-hour"),
2342
+ report.buckets.find((bucket) => bucket.id === "weekly")
2343
+ ];
2344
+ const parts = ["zai"];
2345
+ for (const bucket of selected) {
2346
+ if (!bucket?.limit || bucket.remaining === void 0) continue;
2347
+ const fallback = bucket.id === "weekly" ? "weekly" : "5h";
2348
+ parts.push(
2349
+ `${percentRemaining(bucket)}% ${formatWindowLabel(bucket.windowMinutes, fallback, true)}`
2350
+ );
2351
+ }
2352
+ return parts.length > 1 ? parts.join(" ") : void 0;
2353
+ }
2354
+ function formatCurrencyMetric(metric) {
2355
+ if (typeof metric.value !== "number") return String(metric.value);
2356
+ if (!metric.currency) return "unavailable";
2357
+ if (metric.currency === "USD") return `$${metric.value.toFixed(2)}`;
2358
+ if (metric.currency === "CNY") return `\xA5${metric.value.toFixed(2)}`;
2359
+ return `${metric.value.toFixed(2)} ${metric.currency}`;
2360
+ }
2361
+ function formatXaiReport(lines, report) {
2362
+ const included = report.buckets.find((bucket) => bucket.id === "included-allowance");
2363
+ if (included) {
2364
+ let value = "unavailable";
2365
+ if (included.unit === "percent" && included.used !== void 0) {
2366
+ value = `${included.used}% used`;
2367
+ if (included.remaining !== void 0) value += ` \xB7 ${included.remaining}% left`;
2368
+ } else if (included.used !== void 0) {
2369
+ value = `${formatUsd(included.used)} used`;
2370
+ if (included.limit !== void 0) value += ` of ${formatUsd(included.limit)}`;
2371
+ } else if (included.limit !== void 0) {
2372
+ value = `usage unavailable \xB7 ${formatUsd(included.limit)} limit`;
2373
+ }
2374
+ const period = included.period ? ` \xB7 ${included.period}` : "";
2375
+ const reset = included.resetsAt ? ` (resets ${formatReset(included.resetsAt)})` : "";
2376
+ lines.push(`${"Included allowance:".padEnd(VALUE_COLUMN)}${value}${period}${reset}`);
2377
+ }
2378
+ const onDemand = report.buckets.find((bucket) => bucket.id === "on-demand");
2379
+ if (onDemand) {
2380
+ let value = onDemand.used === void 0 ? "usage unavailable" : `${formatUsd(onDemand.used)} used`;
2381
+ if (onDemand.limit !== void 0) value += ` of ${formatUsd(onDemand.limit)} cap`;
2382
+ lines.push(`${"On-demand usage:".padEnd(VALUE_COLUMN)}${value}`);
2383
+ }
2384
+ for (const metric of report.metrics) {
2385
+ lines.push(
2386
+ `${`${metric.label}:`.padEnd(VALUE_COLUMN)}${formatMetricValue(metric.value, metric.unit)}`
2387
+ );
2388
+ }
2389
+ }
1617
2390
  function formatZaiReport(lines, report) {
1618
2391
  for (const bucket of report.buckets) {
1619
2392
  const reset = bucket.resetsAt ? ` (resets ${formatReset(bucket.resetsAt)})` : "";
@@ -1649,7 +2422,7 @@ function formatGenericReport(lines, report) {
1649
2422
  );
1650
2423
  }
1651
2424
  }
1652
- function formatCodexStatusline(report, model) {
2425
+ function formatCodexStatusline(report, model, now = Date.now(), showResetCountdown = true) {
1653
2426
  const group = selectCodexGroup(report, model);
1654
2427
  if (!group) return formatCodexCreditsStatus(report);
1655
2428
  const buckets = report.buckets.filter((bucket) => (bucket.groupId ?? bucket.id) === group);
@@ -1659,10 +2432,15 @@ function formatCodexStatusline(report, model) {
1659
2432
  ];
1660
2433
  for (const bucket of buckets) {
1661
2434
  if (bucket.remaining === void 0) continue;
2435
+ const percent = `${clampPercent4(bucket.remaining).toFixed(0)}%`;
1662
2436
  const fallback = bucket.id.endsWith(":secondary") ? "weekly" : "5h";
1663
- parts.push(
1664
- `${clampPercent4(bucket.remaining).toFixed(0)}% ${formatWindowLabel(bucket.windowMinutes, fallback, true)}`
1665
- );
2437
+ const window = formatWindowLabel(bucket.windowMinutes, fallback, true);
2438
+ if (!showResetCountdown) {
2439
+ parts.push(`${percent} ${window}`);
2440
+ continue;
2441
+ }
2442
+ const reset = formatResetCountdown(bucket.resetsAt, now);
2443
+ parts.push(`${percent} ${reset ? `\u21BB ${reset}` : window}`);
1666
2444
  }
1667
2445
  return parts.length > 1 ? parts.join(" ") : formatCodexCreditsStatus(report);
1668
2446
  }
@@ -1743,6 +2521,23 @@ function formatWindowLabel(minutes, fallback, compact) {
1743
2521
  if (minutes % 60 === 0) return `${minutes / 60}h`;
1744
2522
  return `${minutes}m`;
1745
2523
  }
2524
+ function formatResetCountdown(resetsAt, now) {
2525
+ if (resetsAt === void 0 || !Number.isFinite(resetsAt) || !Number.isFinite(now))
2526
+ return void 0;
2527
+ const totalMinutes = Math.max(0, Math.ceil((resetsAt * 1e3 - now) / 6e4));
2528
+ const days = Math.floor(totalMinutes / 1440);
2529
+ const hours = Math.floor(totalMinutes % 1440 / 60);
2530
+ const minutes = totalMinutes % 60;
2531
+ if (days > 0) {
2532
+ return [
2533
+ `${String(days)}d`,
2534
+ hours > 0 ? `${String(hours)}h` : minutes > 0 ? `${String(minutes)}m` : ""
2535
+ ].filter(Boolean).join("");
2536
+ }
2537
+ if (hours > 0)
2538
+ return [`${String(hours)}h`, minutes > 0 ? `${String(minutes)}m` : ""].filter(Boolean).join("");
2539
+ return `${String(minutes)}m`;
2540
+ }
1746
2541
  function formatMetricValue(value, unit) {
1747
2542
  if (unit === "usd" && typeof value === "number") return formatUsd(value);
1748
2543
  return String(value);
@@ -1774,18 +2569,23 @@ import { getAgentDir } from "@earendil-works/pi-coding-agent";
1774
2569
  var USAGE_SETTINGS_FILE = "pi-usage.json";
1775
2570
  var MAX_USAGE_SETTINGS_BYTES = 64 * 1024;
1776
2571
  var DEFAULT_USAGE_SETTINGS = Object.freeze({
1777
- codexFastMode: false
2572
+ codexFastMode: false,
2573
+ codexStatusResetCountdown: true
1778
2574
  });
1779
2575
  function usageSettingsPath() {
1780
2576
  return join(getAgentDir(), USAGE_SETTINGS_FILE);
1781
2577
  }
1782
2578
  function normalizeUsageSettings(value) {
1783
- if (!isRecord2(value)) return void 0;
2579
+ if (!isRecord3(value)) return void 0;
1784
2580
  if (Object.hasOwn(value, "codexFastMode") && typeof value.codexFastMode !== "boolean") {
1785
2581
  return void 0;
1786
2582
  }
2583
+ if (Object.hasOwn(value, "codexStatusResetCountdown") && typeof value.codexStatusResetCountdown !== "boolean") {
2584
+ return void 0;
2585
+ }
1787
2586
  return {
1788
- codexFastMode: typeof value.codexFastMode === "boolean" ? value.codexFastMode : DEFAULT_USAGE_SETTINGS.codexFastMode
2587
+ codexFastMode: typeof value.codexFastMode === "boolean" ? value.codexFastMode : DEFAULT_USAGE_SETTINGS.codexFastMode,
2588
+ codexStatusResetCountdown: typeof value.codexStatusResetCountdown === "boolean" ? value.codexStatusResetCountdown : DEFAULT_USAGE_SETTINGS.codexStatusResetCountdown
1789
2589
  };
1790
2590
  }
1791
2591
  async function loadUsageSettings(path = usageSettingsPath(), signal) {
@@ -1807,7 +2607,7 @@ async function loadUsageSettings(path = usageSettingsPath(), signal) {
1807
2607
  throwIfAborted(signal);
1808
2608
  const document = JSON.parse(text);
1809
2609
  const settings = normalizeUsageSettings(document);
1810
- if (!settings || !isRecord2(document)) throw new Error("invalid settings shape");
2610
+ if (!settings || !isRecord3(document)) throw new Error("invalid settings shape");
1811
2611
  return { kind: "loaded", path, settings, document };
1812
2612
  } catch (error) {
1813
2613
  if (signal?.aborted) throw error;
@@ -1902,7 +2702,7 @@ async function chmodPrivate(path) {
1902
2702
  function throwIfAborted(signal) {
1903
2703
  if (signal?.aborted) throw new DOMException("Settings operation aborted", "AbortError");
1904
2704
  }
1905
- function isRecord2(value) {
2705
+ function isRecord3(value) {
1906
2706
  return typeof value === "object" && value !== null && !Array.isArray(value);
1907
2707
  }
1908
2708
  function isNodeError(error) {
@@ -2011,7 +2811,7 @@ function registerCodexFastMode(pi, settingsRuntime, refreshStatus) {
2011
2811
  const key = activeRequestKey(ctx);
2012
2812
  if (key && ctx.model) {
2013
2813
  pendingFastRequests.set(key, {
2014
- fastRequested: isRecord3(rewritten) && rewritten.service_tier === "priority",
2814
+ fastRequested: isRecord4(rewritten) && rewritten.service_tier === "priority",
2015
2815
  model: ctx.model
2016
2816
  });
2017
2817
  }
@@ -2051,7 +2851,7 @@ function activeRequestKey(ctx) {
2051
2851
  return model ? `${ctx.sessionManager.getSessionId()}:${model.provider}/${model.id}` : void 0;
2052
2852
  }
2053
2853
  function consumeFastRequest(ctx, message, pending) {
2054
- if (!isRecord3(message) || message.role !== "assistant") return NO_FAST_REQUEST;
2854
+ if (!isRecord4(message) || message.role !== "assistant") return NO_FAST_REQUEST;
2055
2855
  const key = messageRequestKey(ctx, message);
2056
2856
  if (!key) return NO_FAST_REQUEST;
2057
2857
  const request = pending.get(key);
@@ -2062,7 +2862,7 @@ function messageRequestKey(ctx, message) {
2062
2862
  if (typeof message.provider !== "string" || typeof message.model !== "string") return void 0;
2063
2863
  return `${ctx.sessionManager.getSessionId()}:${message.provider}/${message.model}`;
2064
2864
  }
2065
- function isRecord3(value) {
2865
+ function isRecord4(value) {
2066
2866
  return typeof value === "object" && value !== null && !Array.isArray(value);
2067
2867
  }
2068
2868
  function isAbortError2(error) {
@@ -2071,7 +2871,7 @@ function isAbortError2(error) {
2071
2871
 
2072
2872
  // src/usage-helpers.ts
2073
2873
  function configuredAdapters(ctx) {
2074
- return SUPPORTED_ADAPTERS.filter(
2874
+ return usageAdapters().filter(
2075
2875
  (adapter) => adapter.id === ctx.model?.provider || providerIsConfigured(ctx, adapter.id)
2076
2876
  );
2077
2877
  }
@@ -2101,8 +2901,120 @@ function isTimeoutError(error) {
2101
2901
  return error instanceof Error && error.name === "TimeoutError";
2102
2902
  }
2103
2903
 
2904
+ // src/usage-settings-ui.ts
2905
+ import {
2906
+ getSettingsListTheme
2907
+ } from "@earendil-works/pi-coding-agent";
2908
+ import {
2909
+ Container,
2910
+ Key,
2911
+ matchesKey,
2912
+ SettingsList,
2913
+ Text
2914
+ } from "@earendil-works/pi-tui";
2915
+ var OFF = "Off";
2916
+ var ON = "On";
2917
+ async function showUsageSettings(ctx, settingsRuntime, parentSignal, isCurrent, onApplied) {
2918
+ if (ctx.mode !== "tui") {
2919
+ if (ctx.hasUI) ctx.ui.notify(`Edit settings manually: ${settingsRuntime.get().path}`, "info");
2920
+ return false;
2921
+ }
2922
+ if (parentSignal.aborted || !isCurrent()) return false;
2923
+ return ctx.ui.custom((tui, theme, _keybindings, done) => {
2924
+ const localController = new AbortController();
2925
+ const signal = AbortSignal.any([parentSignal, localController.signal]);
2926
+ let changed = false;
2927
+ let closing = false;
2928
+ let saveQueue = Promise.resolve();
2929
+ const state = settingsRuntime.get();
2930
+ const items = [
2931
+ {
2932
+ id: "codexFastMode",
2933
+ label: "Codex Fast mode",
2934
+ description: "Use faster Codex routing at increased plan allowance consumption.",
2935
+ currentValue: state.settings.codexFastMode ? ON : OFF,
2936
+ values: [OFF, ON]
2937
+ },
2938
+ {
2939
+ id: "codexStatusResetCountdown",
2940
+ label: "Codex reset countdown",
2941
+ description: "Show time remaining until each Codex usage limit resets.",
2942
+ currentValue: state.settings.codexStatusResetCountdown ? ON : OFF,
2943
+ values: [OFF, ON]
2944
+ }
2945
+ ];
2946
+ const container = new Container();
2947
+ container.addChild(new Text(theme.fg("accent", theme.bold("pi-usage Settings")), 1, 1));
2948
+ let settingsList;
2949
+ const cancel = () => {
2950
+ if (closing) return;
2951
+ closing = true;
2952
+ localController.abort();
2953
+ done(changed);
2954
+ };
2955
+ settingsList = new SettingsList(
2956
+ items,
2957
+ items.length + 2,
2958
+ getSettingsListTheme(),
2959
+ (id, value) => {
2960
+ if (closing || signal.aborted || !isCurrent()) return;
2961
+ const settingId = id;
2962
+ const requested = value !== OFF;
2963
+ saveQueue = saveQueue.then(async () => {
2964
+ const previous = settingsRuntime.get().settings[settingId];
2965
+ if (settingsRuntime.get().kind === "invalid") {
2966
+ settingsList.updateValue(id, displayValue(previous));
2967
+ if (!signal.aborted && isCurrent()) {
2968
+ ctx.ui.notify("Repair pi-usage.json and reload before changing settings.", "error");
2969
+ tui.requestRender();
2970
+ }
2971
+ return;
2972
+ }
2973
+ try {
2974
+ await settingsRuntime.update({ [settingId]: requested }, signal);
2975
+ } catch (error) {
2976
+ if (signal.aborted || !isCurrent()) return;
2977
+ settingsList.updateValue(id, displayValue(previous));
2978
+ ctx.ui.notify(`Could not save pi-usage.json: ${errorMessage(error)}`, "error");
2979
+ tui.requestRender();
2980
+ return;
2981
+ }
2982
+ if (previous !== requested) {
2983
+ changed = true;
2984
+ onApplied(settingId);
2985
+ }
2986
+ if (signal.aborted || !isCurrent()) return;
2987
+ settingsList.updateValue(id, displayValue(requested));
2988
+ tui.requestRender();
2989
+ });
2990
+ },
2991
+ cancel
2992
+ );
2993
+ container.addChild(settingsList);
2994
+ parentSignal.addEventListener("abort", cancel, { once: true });
2995
+ return {
2996
+ render: (width) => container.render(width),
2997
+ invalidate: () => container.invalidate(),
2998
+ handleInput(data) {
2999
+ if (closing) return;
3000
+ if (matchesKey(data, Key.ctrl("c"))) cancel();
3001
+ else settingsList.handleInput(data);
3002
+ tui.requestRender();
3003
+ },
3004
+ dispose() {
3005
+ localController.abort();
3006
+ parentSignal.removeEventListener("abort", cancel);
3007
+ }
3008
+ };
3009
+ });
3010
+ }
3011
+ function displayValue(enabled) {
3012
+ return enabled ? ON : OFF;
3013
+ }
3014
+
2104
3015
  // src/usage.ts
2105
3016
  var CACHE_TTL_MS = 5 * 60 * 1e3;
3017
+ var STATUS_COUNTDOWN_REFRESH_MS = 60 * 1e3;
2106
3018
  var DEFAULT_TIMEOUT_MS = 15e3;
2107
3019
  var ALL_PROVIDER_CONCURRENCY = 2;
2108
3020
  var FAILURE_BACKOFF_MS = 3e4;
@@ -2112,6 +3024,7 @@ var REFRESH_CURRENT = "Refresh current usage";
2112
3024
  var VIEW_ANOTHER = "View another configured provider\u2026";
2113
3025
  var VIEW_ALL = "View all configured providers\u2026";
2114
3026
  var CLOSE = "Close";
3027
+ var SETTINGS = "Settings";
2115
3028
  var REDEEM_CODEX_RESET = "Redeem usage limit reset\u2026";
2116
3029
  function usageExtension(pi, dependencies = {}) {
2117
3030
  const credentialReader = dependencies.credentialReader;
@@ -2126,13 +3039,23 @@ function usageExtension(pi, dependencies = {}) {
2126
3039
  let activeCurrentIdentity;
2127
3040
  let sessionActive = false;
2128
3041
  let statusGeneration = 0;
3042
+ let sessionGeneration = 0;
2129
3043
  let statusRefreshTimer;
3044
+ let statusCountdownTimer;
2130
3045
  let statusController;
2131
3046
  let fastRuntime;
2132
- const clearStatusTimer = () => {
3047
+ const clearStatusRefreshTimer = () => {
2133
3048
  if (statusRefreshTimer) clearTimeout(statusRefreshTimer);
2134
3049
  statusRefreshTimer = void 0;
2135
3050
  };
3051
+ const clearStatusCountdownTimer = () => {
3052
+ if (statusCountdownTimer) clearTimeout(statusCountdownTimer);
3053
+ statusCountdownTimer = void 0;
3054
+ };
3055
+ const clearStatusTimers = () => {
3056
+ clearStatusRefreshTimer();
3057
+ clearStatusCountdownTimer();
3058
+ };
2136
3059
  const safeSetStatus = (ctx, value) => {
2137
3060
  try {
2138
3061
  ctx.ui.setStatus(STATUS_KEY, value);
@@ -2146,11 +3069,11 @@ function usageExtension(pi, dependencies = {}) {
2146
3069
  statusGeneration += 1;
2147
3070
  statusController?.abort();
2148
3071
  statusController = void 0;
2149
- clearStatusTimer();
3072
+ clearStatusTimers();
2150
3073
  safeSetStatus(ctx, void 0);
2151
3074
  };
2152
3075
  const scheduleStatusRefresh = (ctx, model) => {
2153
- clearStatusTimer();
3076
+ clearStatusRefreshTimer();
2154
3077
  const generation = statusGeneration;
2155
3078
  statusRefreshTimer = setTimeout(() => {
2156
3079
  statusRefreshTimer = void 0;
@@ -2160,13 +3083,14 @@ function usageExtension(pi, dependencies = {}) {
2160
3083
  statusRefreshTimer.unref?.();
2161
3084
  };
2162
3085
  const publishStatus = (ctx, outcome, model, shouldSchedule) => {
3086
+ clearStatusCountdownTimer();
2163
3087
  if (adapterForProvider(model.provider)?.publishesStatusline === false) {
2164
- clearStatusTimer();
3088
+ clearStatusRefreshTimer();
2165
3089
  safeSetStatus(ctx, void 0);
2166
3090
  return;
2167
3091
  }
2168
3092
  if (outcome.state.status === "unsupported") {
2169
- clearStatusTimer();
3093
+ clearStatusRefreshTimer();
2170
3094
  safeSetStatus(ctx, void 0);
2171
3095
  return;
2172
3096
  }
@@ -2179,10 +3103,30 @@ function usageExtension(pi, dependencies = {}) {
2179
3103
  }
2180
3104
  return;
2181
3105
  }
2182
- const rawValue = formatUsageStatusline(outcome.state.report, model);
3106
+ const showCodexResetCountdown = outcome.state.report.providerId === "openai-codex" && settingsRuntime.get().settings.codexStatusResetCountdown;
3107
+ const now = Date.now();
3108
+ const rawValue = formatUsageStatusline(
3109
+ outcome.state.report,
3110
+ model,
3111
+ now,
3112
+ showCodexResetCountdown
3113
+ );
2183
3114
  const value = rawValue ? fastRuntime.decorateStatus(model, rawValue) : void 0;
2184
3115
  if (!safeSetStatus(ctx, value)) return;
2185
3116
  if (shouldSchedule && sessionActive) scheduleStatusRefresh(ctx, model);
3117
+ if (sessionActive && showCodexResetCountdown && outcome.state.report.buckets.some(
3118
+ (bucket) => bucket.resetsAt !== void 0 && Number.isFinite(bucket.resetsAt) && bucket.resetsAt * 1e3 > now
3119
+ )) {
3120
+ const generation = statusGeneration;
3121
+ statusCountdownTimer = setTimeout(() => {
3122
+ statusCountdownTimer = void 0;
3123
+ if (!sessionActive || generation !== statusGeneration || modelIdentity(ctx.model) !== modelIdentity(model)) {
3124
+ return;
3125
+ }
3126
+ publishStatus(ctx, outcome, model, false);
3127
+ }, STATUS_COUNTDOWN_REFRESH_MS);
3128
+ statusCountdownTimer.unref?.();
3129
+ }
2186
3130
  };
2187
3131
  const invalidateProviderState = (providerId) => {
2188
3132
  cache.clearProvider(providerId);
@@ -2204,14 +3148,16 @@ function usageExtension(pi, dependencies = {}) {
2204
3148
  }
2205
3149
  activeCurrentIdentity = nextIdentity;
2206
3150
  };
2207
- const queryAdapterState = async (ctx, adapter, displayState, force, signal) => {
2208
- const startedAt = Date.now();
3151
+ const queryAdapterState = async (ctx, adapter, displayState, force, signal, authRetry = 0, deadlineAt = Date.now() + DEFAULT_TIMEOUT_MS) => {
3152
+ const expectedSessionGeneration = sessionGeneration;
3153
+ const expectedSessionId = ctx.sessionManager.getSessionId();
3154
+ const expectedModelIdentity = modelIdentity(ctx.model);
2209
3155
  let auth;
2210
3156
  try {
2211
3157
  auth = await awaitWithDeadline(
2212
3158
  resolveUsageAuth(ctx, adapter, void 0, credentialReader, credentialCandidates),
2213
3159
  signal,
2214
- DEFAULT_TIMEOUT_MS,
3160
+ Math.max(1, deadlineAt - Date.now()),
2215
3161
  `resolving ${adapter.displayName} runtime auth`
2216
3162
  );
2217
3163
  } catch (error) {
@@ -2229,6 +3175,9 @@ function usageExtension(pi, dependencies = {}) {
2229
3175
  }
2230
3176
  };
2231
3177
  }
3178
+ const requiresRequestBoundaryGuard = adapter.id === "deepseek" || adapter.id === "xai";
3179
+ const requestContextChanged = () => expectedSessionGeneration !== sessionGeneration || ctx.sessionManager.getSessionId() !== expectedSessionId || modelIdentity(ctx.model) !== expectedModelIdentity;
3180
+ if (requiresRequestBoundaryGuard && requestContextChanged()) throw abortError();
2232
3181
  if (!auth) {
2233
3182
  if (displayState === "current") {
2234
3183
  transitionCurrentIdentity(`${adapter.id}:unavailable`, adapter.id);
@@ -2278,9 +3227,28 @@ function usageExtension(pi, dependencies = {}) {
2278
3227
  querySequence += 1;
2279
3228
  const queryId = querySequence;
2280
3229
  setBoundedMap(latestQueries, failureKey, queryId, MAX_ACCOUNT_STATES);
3230
+ let deepSeekAuthChanged = false;
2281
3231
  try {
2282
- const remainingMs = Math.max(1, DEFAULT_TIMEOUT_MS - (Date.now() - startedAt));
2283
- const report = await queryProviderUsage(adapter, auth, signal, remainingMs);
3232
+ const remainingMs = Math.max(1, deadlineAt - Date.now());
3233
+ const guard = requiresRequestBoundaryGuard ? async () => {
3234
+ if (signal.aborted || requestContextChanged()) throw abortError();
3235
+ const revalidated = await awaitWithDeadline(
3236
+ resolveUsageAuth(ctx, adapter, void 0, credentialReader, credentialCandidates),
3237
+ signal,
3238
+ Math.max(1, deadlineAt - Date.now()),
3239
+ `revalidating ${adapter.displayName} runtime auth`
3240
+ );
3241
+ if (signal.aborted || requestContextChanged()) throw abortError();
3242
+ if (revalidated?.fingerprint !== auth.fingerprint) {
3243
+ if (adapter.id === "deepseek") {
3244
+ deepSeekAuthChanged = true;
3245
+ throw new Error("DeepSeek runtime credential changed during the balance query.");
3246
+ }
3247
+ throw abortError();
3248
+ }
3249
+ } : void 0;
3250
+ const report = await queryProviderUsage(adapter, auth, signal, remainingMs, guard);
3251
+ if (guard) await guard();
2284
3252
  if (latestQueries.get(failureKey) === queryId) {
2285
3253
  cache.set(adapter.id, auth.fingerprint, report);
2286
3254
  failureBackoff.delete(failureKey);
@@ -2297,6 +3265,18 @@ function usageExtension(pi, dependencies = {}) {
2297
3265
  };
2298
3266
  } catch (error) {
2299
3267
  if (isStaleExtensionContextError(error) || isAbortError3(error)) throw error;
3268
+ if (deepSeekAuthChanged && authRetry === 0 && !signal.aborted && !requestContextChanged() && Date.now() < deadlineAt) {
3269
+ if (latestQueries.get(failureKey) === queryId) latestQueries.delete(failureKey);
3270
+ return queryAdapterState(
3271
+ ctx,
3272
+ adapter,
3273
+ displayState,
3274
+ true,
3275
+ signal,
3276
+ authRetry + 1,
3277
+ deadlineAt
3278
+ );
3279
+ }
2300
3280
  const message = errorMessage(error);
2301
3281
  const now = Date.now();
2302
3282
  for (const [key, failure] of failureBackoff) {
@@ -2353,6 +3333,7 @@ function usageExtension(pi, dependencies = {}) {
2353
3333
  }
2354
3334
  statusGeneration += 1;
2355
3335
  const generation = statusGeneration;
3336
+ clearStatusCountdownTimer();
2356
3337
  statusController?.abort();
2357
3338
  const controller = new AbortController();
2358
3339
  statusController = controller;
@@ -2462,7 +3443,7 @@ function usageExtension(pi, dependencies = {}) {
2462
3443
  const menuGeneration = statusGeneration;
2463
3444
  statusController?.abort();
2464
3445
  statusController = void 0;
2465
- clearStatusTimer();
3446
+ clearStatusTimers();
2466
3447
  const controller = new AbortController();
2467
3448
  activeControllers.add(controller);
2468
3449
  try {
@@ -2498,6 +3479,7 @@ function usageExtension(pi, dependencies = {}) {
2498
3479
  lines: [...formatProviderStates(visibleStates).split("\n"), ...fastLines],
2499
3480
  items: [
2500
3481
  { id: "refresh", label: REFRESH_CURRENT, action: "refresh" },
3482
+ { id: "settings", label: SETTINGS, action: "settings" },
2501
3483
  ...fastAvailability.kind === "available" ? [
2502
3484
  {
2503
3485
  id: "toggle-fast",
@@ -2590,6 +3572,32 @@ function usageExtension(pi, dependencies = {}) {
2590
3572
  })
2591
3573
  },
2592
3574
  actions: {
3575
+ settings: async () => {
3576
+ await showUsageSettings(
3577
+ ctx,
3578
+ settingsRuntime,
3579
+ controller.signal,
3580
+ () => statusGeneration === menuGeneration && !controller.signal.aborted,
3581
+ (id) => {
3582
+ if (id === "codexStatusResetCountdown" && stableCurrent && statusGeneration === menuGeneration && !controller.signal.aborted) {
3583
+ publishStableCurrent(ctx, stableCurrent);
3584
+ }
3585
+ }
3586
+ );
3587
+ fastState = settingsRuntime.get();
3588
+ const revalidated = await queryStableCurrent(
3589
+ ctx,
3590
+ false,
3591
+ controller,
3592
+ "Applying usage settings\u2026"
3593
+ );
3594
+ if (!revalidated) return { kind: "stay" };
3595
+ stableCurrent = revalidated;
3596
+ current = revalidated.outcome;
3597
+ visibleStates = [current.state];
3598
+ publishStableCurrent(ctx, revalidated);
3599
+ return { kind: "stay" };
3600
+ },
2593
3601
  "toggle-fast": async () => {
2594
3602
  const availability = fastRuntime.availability(ctx.model);
2595
3603
  if (availability.kind !== "available" || fastState.kind === "invalid") {
@@ -2873,8 +3881,9 @@ function usageExtension(pi, dependencies = {}) {
2873
3881
  }
2874
3882
  });
2875
3883
  pi.on("session_start", (_event, ctx) => {
3884
+ sessionGeneration += 1;
2876
3885
  statusGeneration += 1;
2877
- clearStatusTimer();
3886
+ clearStatusTimers();
2878
3887
  for (const controller of activeControllers) controller.abort();
2879
3888
  activeControllers.clear();
2880
3889
  statusController = void 0;
@@ -2892,8 +3901,9 @@ function usageExtension(pi, dependencies = {}) {
2892
3901
  });
2893
3902
  pi.on("session_shutdown", (_event, ctx) => {
2894
3903
  sessionActive = false;
3904
+ sessionGeneration += 1;
2895
3905
  statusGeneration += 1;
2896
- clearStatusTimer();
3906
+ clearStatusTimers();
2897
3907
  for (const controller of activeControllers) controller.abort();
2898
3908
  activeControllers.clear();
2899
3909
  statusController = void 0;
@@ -2916,6 +3926,7 @@ export {
2916
3926
  DEFAULT_USAGE_SETTINGS,
2917
3927
  SUPPORTED_ADAPTERS,
2918
3928
  UsageCache,
3929
+ XAI_ADAPTER,
2919
3930
  abortError,
2920
3931
  adapterForProvider,
2921
3932
  awaitWithDeadline,
@@ -2937,10 +3948,13 @@ export {
2937
3948
  loadUsageSettings,
2938
3949
  normalizeCodexBackendPayload,
2939
3950
  normalizeCodexResetCreditsPayload,
3951
+ normalizeDeepSeekBalancePayload,
2940
3952
  normalizeGitHubCopilotUsagePayload,
3953
+ normalizeKimiCodingUsagePayload,
2941
3954
  normalizeOpenCodeZenPayload,
2942
3955
  normalizeOpenRouterKeyPayload,
2943
3956
  normalizeUsageSettings,
3957
+ normalizeXaiBillingPayload,
2944
3958
  normalizeZaiQuotaPayload,
2945
3959
  providerIsConfigured,
2946
3960
  queryProviderUsage,
@@ -2950,6 +3964,7 @@ export {
2950
3964
  rewriteCodexFastPayload,
2951
3965
  runWithConcurrency,
2952
3966
  sanitizeDisplayText,
3967
+ usageAdapters,
2953
3968
  usageSettingsPath
2954
3969
  };
2955
3970
  //# sourceMappingURL=index.ts.map