@narumitw/pi-usage 0.53.0 → 0.54.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
@@ -550,6 +550,219 @@ function asNonnegativeNumber(value) {
550
550
  return number === void 0 || number < 0 ? void 0 : number;
551
551
  }
552
552
 
553
+ // src/providers/kimi-coding.ts
554
+ var FIVE_HOUR_WINDOW_MINUTES = 300;
555
+ var DAILY_WINDOW_MINUTES = 1440;
556
+ var WEEKLY_WINDOW_MINUTES = 10080;
557
+ var FIXED_POINT_UNITS_PER_CENT = 1e6;
558
+ function normalizeKimiCodingUsagePayload(payload, capturedAt) {
559
+ const root = asObject3(payload);
560
+ if (!root) throw new Error("Kimi Coding usage response was not an object.");
561
+ const candidates = [];
562
+ let omittedWindow = false;
563
+ const summary = parseUsageRow(root.usage, WEEKLY_WINDOW_MINUTES, "Weekly window");
564
+ if (summary) candidates.push(summary);
565
+ else if (root.usage !== void 0) omittedWindow = true;
566
+ if (Array.isArray(root.limits)) {
567
+ for (const raw of root.limits) {
568
+ const item = asObject3(raw);
569
+ const windowMinutes = parseWindowMinutes(item?.window);
570
+ const label = sanitizedLabel(item?.name);
571
+ const bucket = windowMinutes === void 0 ? void 0 : parseUsageRow(item?.detail, windowMinutes, label ?? defaultWindowLabel(windowMinutes));
572
+ if (bucket) candidates.push(bucket);
573
+ else omittedWindow = true;
574
+ }
575
+ } else if (root.limits !== void 0) {
576
+ omittedWindow = true;
577
+ }
578
+ const buckets = [];
579
+ const byWindow = /* @__PURE__ */ new Map();
580
+ for (const bucket of candidates) {
581
+ const windowMinutes = bucket.windowMinutes;
582
+ byWindow.set(windowMinutes, [...byWindow.get(windowMinutes) ?? [], bucket]);
583
+ }
584
+ for (const rows of byWindow.values()) {
585
+ if (rows.length === 1) buckets.push(rows[0]);
586
+ else omittedWindow = true;
587
+ }
588
+ buckets.sort((left, right) => (left.windowMinutes ?? 0) - (right.windowMinutes ?? 0));
589
+ const metrics = parseBoosterWallet(root.boosterWallet);
590
+ if (buckets.length === 0 && metrics.length === 0) {
591
+ throw new Error("Kimi Coding usage endpoint returned no displayable usage data.");
592
+ }
593
+ return {
594
+ providerId: "kimi-coding",
595
+ providerName: "Kimi For Coding",
596
+ capturedAt,
597
+ source: "kimi-managed-usage",
598
+ semantics: { kind: "consumer-subscription", label: "Kimi Coding Plan usage" },
599
+ buckets,
600
+ metrics,
601
+ ...omittedWindow ? { notes: ["Unsupported, malformed, or duplicate plan windows were unavailable."] } : {}
602
+ };
603
+ }
604
+ function parseUsageRow(value, windowMinutes, label) {
605
+ const row = asObject3(value);
606
+ if (!row) return void 0;
607
+ const used = asNonnegativeInteger2(row.used);
608
+ const limit = asNonnegativeInteger2(row.limit);
609
+ if (used === void 0 || limit === void 0 || limit === 0) return void 0;
610
+ const resetsAt = asIsoEpochSeconds(row.resetTime);
611
+ return {
612
+ id: windowId(windowMinutes),
613
+ label,
614
+ used,
615
+ remaining: Math.max(0, limit - used),
616
+ limit,
617
+ unit: "count",
618
+ windowMinutes,
619
+ ...resetsAt !== void 0 ? { resetsAt } : {}
620
+ };
621
+ }
622
+ function parseWindowMinutes(value) {
623
+ const window = asObject3(value);
624
+ if (!window) return void 0;
625
+ const duration = asPositiveInteger(window.duration);
626
+ if (duration === void 0) return void 0;
627
+ 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;
628
+ if (multiplier === void 0) return void 0;
629
+ const minutes = duration * multiplier;
630
+ return Number.isSafeInteger(minutes) ? minutes : void 0;
631
+ }
632
+ function parseBoosterWallet(value) {
633
+ const wallet = asObject3(value);
634
+ const balance = asObject3(wallet?.balance);
635
+ if (!wallet || !balance || balance.type !== "BOOSTER") return [];
636
+ const totalRaw = asPositiveInteger(balance.amount);
637
+ if (totalRaw === void 0) return [];
638
+ const leftRaw = asNonnegativeInteger2(balance.amountLeft) ?? 0;
639
+ const monthlyLimit = parseMoney(wallet.monthlyChargeLimit);
640
+ const monthlyUsed = parseMoney(wallet.monthlyUsed);
641
+ const currencies = new Set(
642
+ [monthlyLimit?.currency, monthlyUsed?.currency].filter(
643
+ (currency2) => currency2 !== void 0
644
+ )
645
+ );
646
+ if (currencies.size !== 1) return [];
647
+ const currency = currencies.values().next().value;
648
+ if (!currency) return [];
649
+ const total = fixedPointToMajor(totalRaw);
650
+ const left = fixedPointToMajor(leftRaw);
651
+ if (total === void 0 || left === void 0) return [];
652
+ const metrics = [
653
+ { id: "booster-balance", label: "Balance", value: left, unit: "currency", currency },
654
+ { id: "booster-total", label: "Total balance", value: total, unit: "currency", currency }
655
+ ];
656
+ if (monthlyUsed) {
657
+ metrics.push({
658
+ id: "booster-monthly-used",
659
+ label: "Used this month",
660
+ value: monthlyUsed.cents / 100,
661
+ unit: "currency",
662
+ currency
663
+ });
664
+ }
665
+ if (wallet.monthlyChargeLimitEnabled === false) {
666
+ metrics.push({
667
+ id: "booster-monthly-limit",
668
+ label: "Monthly limit",
669
+ value: "unlimited",
670
+ unit: "currency",
671
+ currency
672
+ });
673
+ } else if (wallet.monthlyChargeLimitEnabled === true && monthlyLimit) {
674
+ metrics.push({
675
+ id: "booster-monthly-limit",
676
+ label: "Monthly limit",
677
+ value: monthlyLimit.cents / 100,
678
+ unit: "currency",
679
+ currency
680
+ });
681
+ }
682
+ return metrics;
683
+ }
684
+ function parseMoney(value) {
685
+ const money = asObject3(value);
686
+ if (!money) return void 0;
687
+ const cents = asNonnegativeInteger2(money.priceInCents);
688
+ if (cents === void 0) return void 0;
689
+ const currency = asCurrency(money.currency);
690
+ if (!currency) return void 0;
691
+ return { cents, currency };
692
+ }
693
+ function fixedPointToMajor(value) {
694
+ const cents = value / FIXED_POINT_UNITS_PER_CENT;
695
+ const roundedCents = cents > 0 && cents < 1 ? 1 : Math.round(cents);
696
+ const major = roundedCents / 100;
697
+ return Number.isSafeInteger(roundedCents) && Number.isFinite(major) ? major : void 0;
698
+ }
699
+ function asObject3(value) {
700
+ if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
701
+ return value;
702
+ }
703
+ function asNonnegativeInteger2(value) {
704
+ if (typeof value === "string" && !/^\d+$/u.test(value)) return void 0;
705
+ const number = typeof value === "string" ? Number(value) : value;
706
+ if (typeof number !== "number" || !Number.isSafeInteger(number) || number < 0) return void 0;
707
+ return number;
708
+ }
709
+ function asPositiveInteger(value) {
710
+ const number = asNonnegativeInteger2(value);
711
+ return number !== void 0 && number > 0 ? number : void 0;
712
+ }
713
+ function asIsoEpochSeconds(value) {
714
+ if (typeof value !== "string") return void 0;
715
+ const match = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.\d+)?(?:Z|([+-])(\d{2}):(\d{2}))$/u.exec(
716
+ value
717
+ );
718
+ if (!match) return void 0;
719
+ const [
720
+ ,
721
+ yearText,
722
+ monthText,
723
+ dayText,
724
+ hourText,
725
+ minuteText,
726
+ secondText,
727
+ ,
728
+ offsetHour,
729
+ offsetMinute
730
+ ] = match;
731
+ const year = Number(yearText);
732
+ const month = Number(monthText);
733
+ const day = Number(dayText);
734
+ const hour = Number(hourText);
735
+ const minute = Number(minuteText);
736
+ const second = Number(secondText);
737
+ 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) {
738
+ return void 0;
739
+ }
740
+ const millis = Date.parse(value);
741
+ return Number.isFinite(millis) && millis >= 0 ? Math.floor(millis / 1e3) : void 0;
742
+ }
743
+ function sanitizedLabel(value) {
744
+ if (typeof value !== "string") return void 0;
745
+ return sanitizeDisplayText(value, 80) || void 0;
746
+ }
747
+ function asCurrency(value) {
748
+ if (typeof value !== "string") return void 0;
749
+ const currency = sanitizeDisplayText(value, 3).toUpperCase();
750
+ return /^[A-Z]{3}$/u.test(currency) ? currency : void 0;
751
+ }
752
+ function windowId(minutes) {
753
+ if (minutes === FIVE_HOUR_WINDOW_MINUTES) return "five-hour";
754
+ if (minutes === DAILY_WINDOW_MINUTES) return "daily";
755
+ if (minutes === WEEKLY_WINDOW_MINUTES) return "weekly";
756
+ return `window-${minutes}-minutes`;
757
+ }
758
+ function defaultWindowLabel(minutes) {
759
+ if (minutes === WEEKLY_WINDOW_MINUTES) return "Weekly window";
760
+ if (minutes % 10080 === 0) return `${minutes / 10080}w window`;
761
+ if (minutes % 1440 === 0) return `${minutes / 1440}d window`;
762
+ if (minutes % 60 === 0) return `${minutes / 60}h window`;
763
+ return `${minutes}m window`;
764
+ }
765
+
553
766
  // src/providers/opencode-zen.ts
554
767
  var ZEN_WINDOWS = [
555
768
  { key: "rolling", label: "Rolling" },
@@ -557,12 +770,12 @@ var ZEN_WINDOWS = [
557
770
  { key: "monthly", label: "Monthly" }
558
771
  ];
559
772
  function normalizeOpenCodeZenPayload(payload, capturedAt) {
560
- const usage = asObject3(payload.usage);
773
+ const usage = asObject4(payload.usage);
561
774
  if (!usage) throw new Error("OpenCode Zen usage response was not an object.");
562
775
  const buckets = [];
563
776
  const notes = [];
564
777
  for (const window of ZEN_WINDOWS) {
565
- const raw = asObject3(usage[window.key]);
778
+ const raw = asObject4(usage[window.key]);
566
779
  if (!raw) continue;
567
780
  const status = asString3(raw.status);
568
781
  if (status !== "ok" && status !== "rate-limited") {
@@ -599,7 +812,7 @@ function normalizeOpenCodeZenPayload(payload, capturedAt) {
599
812
  ...notes.length > 0 ? { notes } : {}
600
813
  };
601
814
  }
602
- function asObject3(value) {
815
+ function asObject4(value) {
603
816
  if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
604
817
  return value;
605
818
  }
@@ -623,7 +836,7 @@ function clampPercent2(value) {
623
836
 
624
837
  // src/providers/openrouter.ts
625
838
  function normalizeOpenRouterKeyPayload(payload, capturedAt) {
626
- const data = asObject4(payload.data);
839
+ const data = asObject5(payload.data);
627
840
  if (!data) throw new Error("OpenRouter key response data was not an object.");
628
841
  const limit = asNonnegativeNumber3(data.limit);
629
842
  const remaining = asNonnegativeNumber3(data.limit_remaining);
@@ -669,7 +882,7 @@ function addUsageMetric(metrics, id, label, value) {
669
882
  if (amount === void 0) return;
670
883
  metrics.push({ id, label, value: amount, unit: "usd" });
671
884
  }
672
- function asObject4(value) {
885
+ function asObject5(value) {
673
886
  if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
674
887
  return value;
675
888
  }
@@ -682,17 +895,178 @@ function asNonnegativeNumber3(value) {
682
895
  return value;
683
896
  }
684
897
 
898
+ // src/providers/xai.ts
899
+ var MAX_SAFE_CENTS = Number.MAX_SAFE_INTEGER;
900
+ function normalizeXaiBillingPayload(payload, subscriptionTier, capturedAt) {
901
+ const configValue = payload.config;
902
+ if (configValue !== null && configValue !== void 0 && !isRecord2(configValue)) {
903
+ throw new Error("xAI billing response config was not an object or null.");
904
+ }
905
+ const config = isRecord2(configValue) ? configValue : void 0;
906
+ const buckets = [];
907
+ const metrics = [];
908
+ const notes = [];
909
+ if (config) {
910
+ const period = normalizePeriod(
911
+ config.currentPeriod,
912
+ config.billingPeriodStart,
913
+ config.billingPeriodEnd
914
+ );
915
+ const preferredPercent = optionalPercent(config.creditUsagePercent, "creditUsagePercent");
916
+ if (preferredPercent !== void 0) {
917
+ buckets.push({
918
+ id: "included-allowance",
919
+ label: "Included allowance",
920
+ used: preferredPercent,
921
+ remaining: 100 - preferredPercent,
922
+ unit: "percent",
923
+ ...period
924
+ });
925
+ } else {
926
+ const limit = optionalUsd(config.monthlyLimit, "monthlyLimit");
927
+ const used = optionalUsd(config.used, "used");
928
+ if (limit !== void 0 || used !== void 0) {
929
+ buckets.push({
930
+ id: "included-allowance",
931
+ label: "Included allowance",
932
+ ...limit !== void 0 ? { limit } : {},
933
+ ...used !== void 0 ? { used } : {},
934
+ ...limit !== void 0 && used !== void 0 ? { remaining: limit - used } : {},
935
+ unit: "usd",
936
+ ...period
937
+ });
938
+ } else if (period.period || period.resetsAt !== void 0) {
939
+ buckets.push({
940
+ id: "included-allowance",
941
+ label: "Included allowance",
942
+ unit: "percent",
943
+ ...period
944
+ });
945
+ }
946
+ }
947
+ const onDemandCap = optionalUsd(config.onDemandCap, "onDemandCap");
948
+ const onDemandUsed = optionalUsd(config.onDemandUsed, "onDemandUsed");
949
+ if (onDemandCap !== void 0 || onDemandUsed !== void 0) {
950
+ buckets.push({
951
+ id: "on-demand",
952
+ label: "On-demand usage",
953
+ ...onDemandCap !== void 0 ? { limit: onDemandCap } : {},
954
+ ...onDemandUsed !== void 0 ? { used: onDemandUsed } : {},
955
+ ...onDemandCap !== void 0 && onDemandUsed !== void 0 ? { remaining: onDemandCap - onDemandUsed } : {},
956
+ unit: "usd"
957
+ });
958
+ }
959
+ const prepaidBalance = optionalUsd(config.prepaidBalance, "prepaidBalance");
960
+ if (prepaidBalance !== void 0) {
961
+ metrics.push({
962
+ id: "prepaid-balance",
963
+ label: "Prepaid balance",
964
+ value: prepaidBalance,
965
+ unit: "usd"
966
+ });
967
+ }
968
+ }
969
+ const tier = optionalTier(subscriptionTier);
970
+ if (tier) metrics.push({ id: "subscription-tier", label: "Plan tier", value: tier });
971
+ if (!config) notes.push("No xAI consumer billing configuration is available for this account.");
972
+ else if (buckets.length === 0 && metrics.length === 0) {
973
+ notes.push("The xAI consumer billing response contained no displayable usage fields.");
974
+ }
975
+ return {
976
+ providerId: "xai",
977
+ providerName: "xAI",
978
+ capturedAt,
979
+ source: "cli-chat-proxy.grok.com consumer billing",
980
+ semantics: {
981
+ kind: "consumer-subscription",
982
+ label: "xAI consumer subscription usage"
983
+ },
984
+ buckets,
985
+ metrics,
986
+ ...notes.length > 0 ? { notes } : {}
987
+ };
988
+ }
989
+ function normalizePeriod(currentPeriod, legacyStart, legacyEnd) {
990
+ if (currentPeriod !== void 0 && currentPeriod !== null && !isRecord2(currentPeriod)) {
991
+ throw new Error("xAI billing currentPeriod was not an object or null.");
992
+ }
993
+ if (isRecord2(currentPeriod)) {
994
+ const type = optionalString(currentPeriod.type, "currentPeriod.type");
995
+ const start2 = optionalTimestamp(currentPeriod.start, "currentPeriod.start");
996
+ const end2 = optionalTimestamp(currentPeriod.end, "currentPeriod.end");
997
+ return {
998
+ ...periodLabel(type, start2) ? { period: periodLabel(type, start2) } : {},
999
+ ...end2 !== void 0 ? { resetsAt: end2 } : {}
1000
+ };
1001
+ }
1002
+ const start = optionalTimestamp(legacyStart, "billingPeriodStart");
1003
+ const end = optionalTimestamp(legacyEnd, "billingPeriodEnd");
1004
+ return {
1005
+ ...start !== void 0 ? { period: "Monthly" } : {},
1006
+ ...end !== void 0 ? { resetsAt: end } : {}
1007
+ };
1008
+ }
1009
+ function periodLabel(type, start) {
1010
+ if (type === "USAGE_PERIOD_TYPE_WEEKLY") return "Weekly";
1011
+ if (type === "USAGE_PERIOD_TYPE_MONTHLY") return "Monthly";
1012
+ if (type)
1013
+ return sanitizeDisplayText(type.replace(/^USAGE_PERIOD_TYPE_/u, "").replaceAll("_", " "), 40);
1014
+ return start === void 0 ? void 0 : "Current period";
1015
+ }
1016
+ function optionalUsd(value, field) {
1017
+ if (value === void 0 || value === null) return void 0;
1018
+ if (!isRecord2(value)) throw new Error(`xAI billing ${field} was not a cent wrapper.`);
1019
+ const cents = value.val === void 0 ? 0 : value.val;
1020
+ if (!Number.isSafeInteger(cents) || Math.abs(cents) > MAX_SAFE_CENTS) {
1021
+ throw new Error(`xAI billing ${field}.val was not a safe signed integer.`);
1022
+ }
1023
+ return cents / 100;
1024
+ }
1025
+ function optionalPercent(value, field) {
1026
+ if (value === void 0 || value === null) return void 0;
1027
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 0 || value > 100) {
1028
+ throw new Error(`xAI billing ${field} was outside 0\u2013100.`);
1029
+ }
1030
+ return value;
1031
+ }
1032
+ function optionalTimestamp(value, field) {
1033
+ if (value === void 0 || value === null) return void 0;
1034
+ if (typeof value !== "string" || value.length > 80) {
1035
+ throw new Error(`xAI billing ${field} was not a bounded timestamp.`);
1036
+ }
1037
+ const milliseconds = Date.parse(value);
1038
+ if (!Number.isFinite(milliseconds)) throw new Error(`xAI billing ${field} was invalid.`);
1039
+ return Math.floor(milliseconds / 1e3);
1040
+ }
1041
+ function optionalString(value, field) {
1042
+ if (value === void 0 || value === null) return void 0;
1043
+ if (typeof value !== "string" || value.length > 80) {
1044
+ throw new Error(`xAI billing ${field} was not a bounded string.`);
1045
+ }
1046
+ return value;
1047
+ }
1048
+ function optionalTier(value) {
1049
+ if (value === void 0 || value === null) return void 0;
1050
+ if (typeof value !== "string" || value.length > 160) {
1051
+ throw new Error("xAI subscription tier was not a bounded string or null.");
1052
+ }
1053
+ return sanitizeDisplayText(value, 80) || void 0;
1054
+ }
1055
+ function isRecord2(value) {
1056
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1057
+ }
1058
+
685
1059
  // src/providers/zai.ts
686
- var FIVE_HOUR_WINDOW_MINUTES = 300;
687
- var WEEKLY_WINDOW_MINUTES = 10080;
1060
+ var FIVE_HOUR_WINDOW_MINUTES2 = 300;
1061
+ var WEEKLY_WINDOW_MINUTES2 = 10080;
688
1062
  function normalizeZaiQuotaPayload(providerId, providerName, payload, capturedAt) {
689
- const data = asObject5(payload.data);
1063
+ const data = asObject6(payload.data);
690
1064
  if (!data) throw new Error("Z.AI quota response data was not an object.");
691
1065
  const limits = Array.isArray(data.limits) ? data.limits : [];
692
1066
  const buckets = [];
693
1067
  const metrics = [];
694
1068
  for (const raw of limits) {
695
- const limit = asObject5(raw);
1069
+ const limit = asObject6(raw);
696
1070
  if (!limit) continue;
697
1071
  const type = asString5(limit.type);
698
1072
  const unit = asNonnegativeNumber4(limit.unit);
@@ -701,14 +1075,14 @@ function normalizeZaiQuotaPayload(providerId, providerName, payload, capturedAt)
701
1075
  addCountBucket(buckets, limit, "mcp-monthly", "MCP monthly allowance");
702
1076
  addUsageDetailMetrics(metrics, limit.usageDetails);
703
1077
  } else if (isPlanUsage && unit === 3) {
704
- addPercentBucket(buckets, limit, "five-hour", "5h window", FIVE_HOUR_WINDOW_MINUTES);
1078
+ addPercentBucket(buckets, limit, "five-hour", "5h window", FIVE_HOUR_WINDOW_MINUTES2);
705
1079
  } else if (isPlanUsage && unit === 6) {
706
1080
  const used = asNonnegativeNumber4(limit.currentValue);
707
1081
  const quota = asNonnegativeNumber4(limit.usage);
708
1082
  if (used !== void 0 && quota !== void 0) {
709
- addCountBucket(buckets, limit, "weekly", "Weekly window", WEEKLY_WINDOW_MINUTES);
1083
+ addCountBucket(buckets, limit, "weekly", "Weekly window", WEEKLY_WINDOW_MINUTES2);
710
1084
  } else {
711
- addPercentBucket(buckets, limit, "weekly", "Weekly window", WEEKLY_WINDOW_MINUTES);
1085
+ addPercentBucket(buckets, limit, "weekly", "Weekly window", WEEKLY_WINDOW_MINUTES2);
712
1086
  }
713
1087
  }
714
1088
  }
@@ -764,7 +1138,7 @@ function addCountBucket(buckets, limit, id, label, windowMinutes) {
764
1138
  function addUsageDetailMetrics(metrics, value) {
765
1139
  if (!Array.isArray(value)) return;
766
1140
  for (const raw of value) {
767
- const detail = asObject5(raw);
1141
+ const detail = asObject6(raw);
768
1142
  if (!detail) continue;
769
1143
  const label = asString5(detail.modelCode);
770
1144
  const usage = asNonnegativeNumber4(detail.usage);
@@ -772,7 +1146,7 @@ function addUsageDetailMetrics(metrics, value) {
772
1146
  metrics.push({ id: `mcp-${kebabCase(label)}`, label, value: usage, unit: "count" });
773
1147
  }
774
1148
  }
775
- function asObject5(value) {
1149
+ function asObject6(value) {
776
1150
  if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
777
1151
  return value;
778
1152
  }
@@ -801,6 +1175,14 @@ var CODEX_USAGE_URL = "https://chatgpt.com/backend-api/wham/usage";
801
1175
  var GITHUB_COPILOT_USAGE_URL = "https://api.github.com/copilot_internal/user";
802
1176
  var OPENROUTER_KEY_URL = "https://openrouter.ai/api/v1/key";
803
1177
  var OPENCODE_GO_USAGE_URL = "https://opencode.ai/zen/go/v1/usage";
1178
+ var KIMI_CODING_USAGE_URL = "https://api.kimi.com/coding/v1/usages";
1179
+ var XAI_USER_URL = "https://cli-chat-proxy.grok.com/v1/user?include=subscription";
1180
+ var XAI_BILLING_URL = "https://cli-chat-proxy.grok.com/v1/billing?format=credits";
1181
+ var XAI_CLIENT_HEADERS = Object.freeze({
1182
+ "X-XAI-Token-Auth": "xai-grok-cli",
1183
+ "x-grok-client-version": "1.0.10",
1184
+ "x-grok-client-mode": "interactive"
1185
+ });
804
1186
  var MAX_SUCCESS_BODY_BYTES = 64 * 1024;
805
1187
  var MAX_ERROR_BODY_BYTES = 4 * 1024;
806
1188
  var AUTH_FINGERPRINT_SALT = randomBytes(32);
@@ -871,6 +1253,22 @@ var SUPPORTED_ADAPTERS = [
871
1253
  return normalizeOpenCodeZenPayload(payload, Date.now());
872
1254
  }
873
1255
  },
1256
+ {
1257
+ id: "kimi-coding",
1258
+ displayName: "Kimi For Coding",
1259
+ semantics: { kind: "consumer-subscription", label: "Kimi Coding Plan usage" },
1260
+ async query(auth, signal, timeoutMs) {
1261
+ const payload = await fetchProviderJson(
1262
+ KIMI_CODING_USAGE_URL,
1263
+ auth,
1264
+ signal,
1265
+ timeoutMs,
1266
+ "Kimi Coding usage endpoint",
1267
+ { redirect: "error" }
1268
+ );
1269
+ return normalizeKimiCodingUsagePayload(payload, Date.now());
1270
+ }
1271
+ },
874
1272
  {
875
1273
  id: "zai",
876
1274
  displayName: "Z.AI",
@@ -909,8 +1307,55 @@ var SUPPORTED_ADAPTERS = [
909
1307
  }
910
1308
  }
911
1309
  ];
912
- function adapterForProvider(providerId) {
913
- return SUPPORTED_ADAPTERS.find((adapter) => adapter.id === providerId);
1310
+ var XAI_ADAPTER = {
1311
+ id: "xai",
1312
+ displayName: "xAI",
1313
+ semantics: {
1314
+ kind: "consumer-subscription",
1315
+ label: "xAI consumer subscription usage"
1316
+ },
1317
+ publishesStatusline: false,
1318
+ async query(auth, signal, timeoutMs, guard) {
1319
+ if (!guard) throw new Error("xAI usage requires request-boundary revalidation.");
1320
+ const startedAt = Date.now();
1321
+ const clientAuth = {
1322
+ ...auth,
1323
+ headers: { ...auth.headers, ...XAI_CLIENT_HEADERS }
1324
+ };
1325
+ await guard();
1326
+ const userPayload = await fetchProviderJson(
1327
+ XAI_USER_URL,
1328
+ clientAuth,
1329
+ signal,
1330
+ remainingTimeout(timeoutMs, startedAt),
1331
+ "xAI consumer identity endpoint",
1332
+ { redirect: "error", userAgent: false }
1333
+ );
1334
+ await guard();
1335
+ const userId = validatedXaiUserId(userPayload.userId);
1336
+ const billingAuth = {
1337
+ ...clientAuth,
1338
+ headers: { ...clientAuth.headers, "x-userid": userId },
1339
+ secrets: [...clientAuth.secrets, userId]
1340
+ };
1341
+ await guard();
1342
+ const billingPayload = await fetchProviderJson(
1343
+ XAI_BILLING_URL,
1344
+ billingAuth,
1345
+ signal,
1346
+ remainingTimeout(timeoutMs, startedAt),
1347
+ "xAI consumer billing endpoint",
1348
+ { redirect: "error", userAgent: false }
1349
+ );
1350
+ await guard();
1351
+ return normalizeXaiBillingPayload(billingPayload, userPayload.subscriptionTier, Date.now());
1352
+ }
1353
+ };
1354
+ function usageAdapters(xaiUsage = true) {
1355
+ return xaiUsage ? [...SUPPORTED_ADAPTERS, XAI_ADAPTER] : SUPPORTED_ADAPTERS;
1356
+ }
1357
+ function adapterForProvider(providerId, xaiUsage = true) {
1358
+ return usageAdapters(xaiUsage).find((adapter) => adapter.id === providerId);
914
1359
  }
915
1360
  function isStaleExtensionContextError(error) {
916
1361
  return error instanceof Error && error.message.includes("This extension ctx is stale after session replacement or reload");
@@ -956,6 +1401,11 @@ async function resolveUsageAuth(ctx, adapter, salt = AUTH_FINGERPRINT_SALT, cred
956
1401
  offered.offeredCount === 0
957
1402
  );
958
1403
  }
1404
+ if (adapter.id === "xai") {
1405
+ const offered = candidateReader ? candidateReader(ctx, adapter.id) : fallbackOAuthCredentialCandidates(adapter.id, credentialReader);
1406
+ if (!offered.ok) throw new Error("xAI OAuth credential discovery failed closed.");
1407
+ return resolveXaiUsageAuth(auth, model, salt, offered.candidates);
1408
+ }
959
1409
  const authorization = authorizationFrom(auth);
960
1410
  if (!authorization) return void 0;
961
1411
  const headers = { Authorization: authorization };
@@ -970,9 +1420,9 @@ async function resolveUsageAuth(ctx, adapter, salt = AUTH_FINGERPRINT_SALT, cred
970
1420
  model
971
1421
  };
972
1422
  }
973
- async function queryProviderUsage(adapter, auth, signal, timeoutMs) {
1423
+ async function queryProviderUsage(adapter, auth, signal, timeoutMs, guard) {
974
1424
  try {
975
- return await adapter.query(auth, signal, timeoutMs);
1425
+ return await adapter.query(auth, signal, timeoutMs, guard);
976
1426
  } catch (error) {
977
1427
  if (isStaleExtensionContextError(error) || isAbortError(error)) throw error;
978
1428
  throw new Error(redactUsageError(errorMessage(error), auth.secrets));
@@ -1012,7 +1462,9 @@ async function fetchProviderJson(url, auth, signal, timeoutMs, description, requ
1012
1462
  }, timeoutMs);
1013
1463
  try {
1014
1464
  const headers = { ...auth.headers };
1015
- if (!hasHeader(headers, "User-Agent")) headers["User-Agent"] = "pi-usage";
1465
+ if (request.userAgent !== false && !hasHeader(headers, "User-Agent")) {
1466
+ headers["User-Agent"] = "pi-usage";
1467
+ }
1016
1468
  if (request.body && !hasHeader(headers, "Content-Type")) {
1017
1469
  headers["Content-Type"] = "application/json";
1018
1470
  }
@@ -1020,15 +1472,18 @@ async function fetchProviderJson(url, auth, signal, timeoutMs, description, requ
1020
1472
  method: request.method ?? "GET",
1021
1473
  headers,
1022
1474
  ...request.body ? { body: JSON.stringify(request.body) } : {},
1475
+ ...request.redirect ? { redirect: request.redirect } : {},
1023
1476
  signal: controller.signal
1024
1477
  });
1478
+ if (response.redirected) throw new Error(`${description} refused a redirected response.`);
1025
1479
  if (controller.signal.aborted)
1026
1480
  throw Object.assign(new Error("Usage query aborted."), { name: "AbortError" });
1027
1481
  const text = await readBoundedResponse(
1028
1482
  response,
1029
1483
  response.ok ? MAX_SUCCESS_BODY_BYTES : MAX_ERROR_BODY_BYTES,
1030
1484
  !response.ok,
1031
- description
1485
+ description,
1486
+ controller.signal
1032
1487
  );
1033
1488
  if (controller.signal.aborted)
1034
1489
  throw Object.assign(new Error("Usage query aborted."), { name: "AbortError" });
@@ -1059,12 +1514,15 @@ async function fetchProviderJson(url, auth, signal, timeoutMs, description, requ
1059
1514
  signal.removeEventListener("abort", abortFromCaller);
1060
1515
  }
1061
1516
  }
1062
- async function readBoundedResponse(response, maxBytes, truncateOverflow, description) {
1517
+ async function readBoundedResponse(response, maxBytes, truncateOverflow, description, signal) {
1063
1518
  if (!response.body) return "";
1064
1519
  const reader = response.body.getReader();
1065
1520
  const chunks = [];
1066
1521
  let total = 0;
1067
1522
  let truncated = false;
1523
+ const abort = () => void reader.cancel().catch(() => void 0);
1524
+ if (signal.aborted) abort();
1525
+ else signal.addEventListener("abort", abort, { once: true });
1068
1526
  try {
1069
1527
  while (true) {
1070
1528
  const { done, value } = await reader.read();
@@ -1081,6 +1539,7 @@ async function readBoundedResponse(response, maxBytes, truncateOverflow, descrip
1081
1539
  total += value.byteLength;
1082
1540
  }
1083
1541
  } finally {
1542
+ signal.removeEventListener("abort", abort);
1084
1543
  reader.releaseLock();
1085
1544
  }
1086
1545
  if (truncated && !truncateOverflow) {
@@ -1095,6 +1554,59 @@ async function readBoundedResponse(response, maxBytes, truncateOverflow, descrip
1095
1554
  const text = new TextDecoder().decode(body);
1096
1555
  return truncated ? `${text}\u2026` : text;
1097
1556
  }
1557
+ function resolveXaiUsageAuth(auth, model, salt, candidates) {
1558
+ const resolvedAccess = bearerToken(headerValue(auth.headers, "Authorization")) ?? auth.apiKey;
1559
+ if (!resolvedAccess) throw new Error("xAI runtime authentication was incomplete.");
1560
+ let sawOAuth = false;
1561
+ let sawMatchingAccess = false;
1562
+ let sawIncompleteMatch = false;
1563
+ const matches = [];
1564
+ for (const candidate of candidates) {
1565
+ try {
1566
+ const credential = asObject7(candidate);
1567
+ if (credential?.type !== "oauth") continue;
1568
+ sawOAuth = true;
1569
+ if (credential.access !== resolvedAccess) continue;
1570
+ sawMatchingAccess = true;
1571
+ if (typeof credential.access !== "string" || !credential.access || typeof credential.refresh !== "string" || !credential.refresh || typeof credential.expires !== "number" || !Number.isFinite(credential.expires)) {
1572
+ sawIncompleteMatch = true;
1573
+ continue;
1574
+ }
1575
+ matches.push({ access: credential.access, refresh: credential.refresh });
1576
+ } catch {
1577
+ }
1578
+ }
1579
+ if (sawIncompleteMatch) throw new Error("The matching xAI OAuth credential was incomplete.");
1580
+ if (matches.length > 1) {
1581
+ throw new Error("Multiple OAuth credentials match the active xAI runtime account.");
1582
+ }
1583
+ const match = matches[0];
1584
+ if (!match) {
1585
+ if (!sawOAuth) {
1586
+ throw new Error(
1587
+ "xAI consumer usage requires the OAuth subscription account configured through Pi /login; XAI_API_KEY users can review API spend at console.x.ai."
1588
+ );
1589
+ }
1590
+ if (sawMatchingAccess) throw new Error("The matching xAI OAuth credential was incomplete.");
1591
+ throw new Error("The active xAI runtime account does not match Pi's stored OAuth account.");
1592
+ }
1593
+ const authorization = `Bearer ${match.access}`;
1594
+ const headers = { Authorization: authorization };
1595
+ return {
1596
+ apiKey: match.access,
1597
+ headers,
1598
+ fingerprint: fingerprintResolvedAuth({ headers }, salt),
1599
+ secrets: [
1600
+ match.access,
1601
+ match.refresh,
1602
+ resolvedAccess,
1603
+ auth.apiKey,
1604
+ headerValue(auth.headers, "Authorization"),
1605
+ authorization
1606
+ ].filter((value) => Boolean(value)),
1607
+ model
1608
+ };
1609
+ }
1098
1610
  function resolveGitHubCopilotUsageAuth(auth, model, salt, candidates, standaloneFallback) {
1099
1611
  const resolvedAccess = bearerToken(headerValue(auth.headers, "Authorization")) ?? auth.apiKey;
1100
1612
  if (!resolvedAccess) throw new Error("GitHub Copilot OAuth credentials were incomplete.");
@@ -1105,7 +1617,7 @@ function resolveGitHubCopilotUsageAuth(auth, model, salt, candidates, standalone
1105
1617
  const matches = /* @__PURE__ */ new Map();
1106
1618
  for (const candidate of candidates) {
1107
1619
  try {
1108
- const credential = asObject6(candidate);
1620
+ const credential = asObject7(candidate);
1109
1621
  if (credential?.type !== "oauth") continue;
1110
1622
  sawOAuth = true;
1111
1623
  const storedAccess2 = typeof credential.access === "string" && credential.access ? credential.access : void 0;
@@ -1167,7 +1679,7 @@ function bearerToken(authorization) {
1167
1679
  const match = /^Bearer\s+(.+)$/iu.exec(authorization ?? "");
1168
1680
  return match?.[1];
1169
1681
  }
1170
- function asObject6(value) {
1682
+ function asObject7(value) {
1171
1683
  if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
1172
1684
  return value;
1173
1685
  }
@@ -1188,6 +1700,8 @@ function hasOfficialUrlOrigin(value, providerId) {
1188
1700
  if (providerId === "openai-codex") return url.origin === "https://chatgpt.com";
1189
1701
  if (providerId === "openrouter") return url.origin === "https://openrouter.ai";
1190
1702
  if (providerId === "opencode-go") return url.origin === "https://opencode.ai";
1703
+ if (providerId === "kimi-coding") return url.origin === "https://api.kimi.com";
1704
+ if (providerId === "xai") return url.origin === "https://api.x.ai";
1191
1705
  if (providerId === "zai") return url.origin === "https://api.z.ai";
1192
1706
  if (providerId === "zai-coding-cn") return url.origin === "https://open.bigmodel.cn";
1193
1707
  if (providerId === "github-copilot") {
@@ -1207,6 +1721,17 @@ function headerValue(headers, name) {
1207
1721
  function hasHeader(headers, name) {
1208
1722
  return Object.keys(headers).some((key) => key.toLowerCase() === name.toLowerCase());
1209
1723
  }
1724
+ function validatedXaiUserId(value) {
1725
+ if (typeof value !== "string" || !/^[A-Za-z0-9._~-]{1,128}$/u.test(value)) {
1726
+ throw new Error("xAI consumer identity returned an unsafe canonical user ID.");
1727
+ }
1728
+ return value;
1729
+ }
1730
+ function remainingTimeout(timeoutMs, startedAt) {
1731
+ const remaining = timeoutMs - (Date.now() - startedAt);
1732
+ if (remaining <= 0) throw new Error("Timed out while fetching xAI consumer usage.");
1733
+ return remaining;
1734
+ }
1210
1735
  function zaiMonitorUrl(baseUrl) {
1211
1736
  const base = baseUrl?.trim();
1212
1737
  if (!base) throw new Error("Z.AI model base URL is unavailable.");
@@ -1360,7 +1885,7 @@ function normalizeCodexResetCreditsPayload(payload) {
1360
1885
  if (rawCredits !== void 0 && !Array.isArray(rawCredits)) {
1361
1886
  throw new Error("Codex reset credits response returned invalid credits.");
1362
1887
  }
1363
- const options = (rawCredits ?? []).map(asObject7).filter((credit) => Boolean(credit)).filter((credit) => credit.status === "available" && credit.reset_type === "codex_rate_limits").map(normalizeResetOption).sort(
1888
+ const options = (rawCredits ?? []).map(asObject8).filter((credit) => Boolean(credit)).filter((credit) => credit.status === "available" && credit.reset_type === "codex_rate_limits").map(normalizeResetOption).sort(
1364
1889
  (left, right) => (left.expiresAt ?? Number.MAX_SAFE_INTEGER) - (right.expiresAt ?? Number.MAX_SAFE_INTEGER)
1365
1890
  ).slice(0, Math.min(availableCount, MAX_RESET_OPTIONS));
1366
1891
  if (availableCount > 0 && options.length === 0) {
@@ -1375,7 +1900,7 @@ function selectCodexResetCredential(candidates, resolvedAccess, resolvedAccountI
1375
1900
  const matches = /* @__PURE__ */ new Map();
1376
1901
  for (const candidate of candidates) {
1377
1902
  try {
1378
- const credential = asObject7(candidate);
1903
+ const credential = asObject8(candidate);
1379
1904
  if (credential?.type !== "oauth") continue;
1380
1905
  sawOAuth = true;
1381
1906
  const storedAccess = asNonemptyString(credential.access);
@@ -1414,7 +1939,7 @@ function codexAccountIdFromAccessToken(access) {
1414
1939
  const parts = access.split(".");
1415
1940
  if (parts.length !== 3 || !parts[1]) return void 0;
1416
1941
  const payload = JSON.parse(Buffer.from(parts[1], "base64url").toString("utf8"));
1417
- const claims = asObject7(asObject7(payload)?.["https://api.openai.com/auth"]);
1942
+ const claims = asObject8(asObject8(payload)?.["https://api.openai.com/auth"]);
1418
1943
  return validHeaderValue(claims?.chatgpt_account_id);
1419
1944
  } catch {
1420
1945
  return void 0;
@@ -1446,7 +1971,7 @@ function normalizeResetOption(credit) {
1446
1971
  function isCodexResetOutcomeCode(value) {
1447
1972
  return value === "reset" || value === "nothing_to_reset" || value === "no_credit" || value === "already_redeemed";
1448
1973
  }
1449
- function asObject7(value) {
1974
+ function asObject8(value) {
1450
1975
  if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
1451
1976
  return value;
1452
1977
  }
@@ -1494,6 +2019,8 @@ function formatUsageReport(report, displayState) {
1494
2019
  else if (report.providerId === "github-copilot") formatGitHubCopilotReport(lines, report);
1495
2020
  else if (report.providerId === "openrouter") formatOpenRouterReport(lines, report);
1496
2021
  else if (report.providerId === "opencode-go") formatOpenCodeZenReport(lines, report);
2022
+ else if (report.providerId === "kimi-coding") formatKimiCodingReport(lines, report);
2023
+ else if (report.providerId === "xai") formatXaiReport(lines, report);
1497
2024
  else if (report.providerId === "zai" || report.providerId === "zai-coding-cn") {
1498
2025
  formatZaiReport(lines, report);
1499
2026
  } else formatGenericReport(lines, report);
@@ -1512,6 +2039,7 @@ function formatUsageStatusline(report, model) {
1512
2039
  if (typeof total?.value === "number") return `openrouter ${formatUsd(total.value)} used`;
1513
2040
  }
1514
2041
  if (report.providerId === "opencode-go") return formatOpenCodeZenStatusline(report);
2042
+ if (report.providerId === "kimi-coding") return formatKimiCodingStatusline(report);
1515
2043
  return void 0;
1516
2044
  }
1517
2045
  function formatProviderStates(states) {
@@ -1614,6 +2142,87 @@ function formatOpenCodeZenStatusline(report) {
1614
2142
  }
1615
2143
  return parts.length > 1 ? parts.join(" ") : void 0;
1616
2144
  }
2145
+ function formatKimiCodingReport(lines, report) {
2146
+ for (const bucket of report.buckets) {
2147
+ const reset = bucket.resetsAt ? ` (resets ${formatReset(bucket.resetsAt)})` : "";
2148
+ if (bucket.used === void 0 || bucket.limit === void 0) {
2149
+ lines.push(`${`${bucket.label}:`.padEnd(VALUE_COLUMN)}unavailable${reset}`);
2150
+ continue;
2151
+ }
2152
+ lines.push(
2153
+ `${`${bucket.label}:`.padEnd(VALUE_COLUMN)}${bucket.used} of ${bucket.limit} used \xB7 ${percentRemaining(bucket)}% left${reset}`
2154
+ );
2155
+ }
2156
+ const balance = report.metrics.find((metric) => metric.id === "booster-balance");
2157
+ const total = report.metrics.find((metric) => metric.id === "booster-total");
2158
+ const monthlyUsed = report.metrics.find((metric) => metric.id === "booster-monthly-used");
2159
+ const monthlyLimit = report.metrics.find((metric) => metric.id === "booster-monthly-limit");
2160
+ if (!balance && !monthlyUsed && !monthlyLimit) return;
2161
+ lines.push("", "Extra usage wallet:");
2162
+ if (balance) {
2163
+ const totalSuffix = total ? ` of ${formatCurrencyMetric(total)}` : "";
2164
+ lines.push(`${"Balance:".padEnd(VALUE_COLUMN)}${formatCurrencyMetric(balance)}${totalSuffix}`);
2165
+ }
2166
+ if (monthlyUsed) {
2167
+ lines.push(`${"Used this month:".padEnd(VALUE_COLUMN)}${formatCurrencyMetric(monthlyUsed)}`);
2168
+ }
2169
+ if (monthlyLimit) {
2170
+ lines.push(`${"Monthly limit:".padEnd(VALUE_COLUMN)}${formatCurrencyMetric(monthlyLimit)}`);
2171
+ }
2172
+ }
2173
+ function formatKimiCodingStatusline(report) {
2174
+ const fiveHour = report.buckets.find((bucket) => bucket.id === "five-hour");
2175
+ const weekly = report.buckets.find((bucket) => bucket.id === "weekly");
2176
+ const subWindow = fiveHour ?? report.buckets.find((bucket) => bucket.id !== "weekly");
2177
+ const selected = [subWindow, weekly].filter(
2178
+ (bucket, index, buckets) => bucket !== void 0 && buckets.indexOf(bucket) === index
2179
+ );
2180
+ const parts = ["kimi"];
2181
+ for (const bucket of selected) {
2182
+ if (!bucket.limit || bucket.remaining === void 0) continue;
2183
+ const fallback = bucket.id === "weekly" ? "weekly" : "5h";
2184
+ parts.push(
2185
+ `${percentRemaining(bucket)}% ${formatWindowLabel(bucket.windowMinutes, fallback, true)}`
2186
+ );
2187
+ }
2188
+ return parts.length > 1 ? parts.join(" ") : void 0;
2189
+ }
2190
+ function formatCurrencyMetric(metric) {
2191
+ if (typeof metric.value !== "number") return String(metric.value);
2192
+ if (!metric.currency) return "unavailable";
2193
+ if (metric.currency === "USD") return `$${metric.value.toFixed(2)}`;
2194
+ if (metric.currency === "CNY") return `\xA5${metric.value.toFixed(2)}`;
2195
+ return `${metric.value.toFixed(2)} ${metric.currency}`;
2196
+ }
2197
+ function formatXaiReport(lines, report) {
2198
+ const included = report.buckets.find((bucket) => bucket.id === "included-allowance");
2199
+ if (included) {
2200
+ let value = "unavailable";
2201
+ if (included.unit === "percent" && included.used !== void 0) {
2202
+ value = `${included.used}% used`;
2203
+ if (included.remaining !== void 0) value += ` \xB7 ${included.remaining}% left`;
2204
+ } else if (included.used !== void 0) {
2205
+ value = `${formatUsd(included.used)} used`;
2206
+ if (included.limit !== void 0) value += ` of ${formatUsd(included.limit)}`;
2207
+ } else if (included.limit !== void 0) {
2208
+ value = `usage unavailable \xB7 ${formatUsd(included.limit)} limit`;
2209
+ }
2210
+ const period = included.period ? ` \xB7 ${included.period}` : "";
2211
+ const reset = included.resetsAt ? ` (resets ${formatReset(included.resetsAt)})` : "";
2212
+ lines.push(`${"Included allowance:".padEnd(VALUE_COLUMN)}${value}${period}${reset}`);
2213
+ }
2214
+ const onDemand = report.buckets.find((bucket) => bucket.id === "on-demand");
2215
+ if (onDemand) {
2216
+ let value = onDemand.used === void 0 ? "usage unavailable" : `${formatUsd(onDemand.used)} used`;
2217
+ if (onDemand.limit !== void 0) value += ` of ${formatUsd(onDemand.limit)} cap`;
2218
+ lines.push(`${"On-demand usage:".padEnd(VALUE_COLUMN)}${value}`);
2219
+ }
2220
+ for (const metric of report.metrics) {
2221
+ lines.push(
2222
+ `${`${metric.label}:`.padEnd(VALUE_COLUMN)}${formatMetricValue(metric.value, metric.unit)}`
2223
+ );
2224
+ }
2225
+ }
1617
2226
  function formatZaiReport(lines, report) {
1618
2227
  for (const bucket of report.buckets) {
1619
2228
  const reset = bucket.resetsAt ? ` (resets ${formatReset(bucket.resetsAt)})` : "";
@@ -1774,18 +2383,23 @@ import { getAgentDir } from "@earendil-works/pi-coding-agent";
1774
2383
  var USAGE_SETTINGS_FILE = "pi-usage.json";
1775
2384
  var MAX_USAGE_SETTINGS_BYTES = 64 * 1024;
1776
2385
  var DEFAULT_USAGE_SETTINGS = Object.freeze({
1777
- codexFastMode: false
2386
+ codexFastMode: false,
2387
+ xaiUsage: true
1778
2388
  });
1779
2389
  function usageSettingsPath() {
1780
2390
  return join(getAgentDir(), USAGE_SETTINGS_FILE);
1781
2391
  }
1782
2392
  function normalizeUsageSettings(value) {
1783
- if (!isRecord2(value)) return void 0;
2393
+ if (!isRecord3(value)) return void 0;
1784
2394
  if (Object.hasOwn(value, "codexFastMode") && typeof value.codexFastMode !== "boolean") {
1785
2395
  return void 0;
1786
2396
  }
2397
+ if (Object.hasOwn(value, "xaiUsage") && typeof value.xaiUsage !== "boolean") {
2398
+ return void 0;
2399
+ }
1787
2400
  return {
1788
- codexFastMode: typeof value.codexFastMode === "boolean" ? value.codexFastMode : DEFAULT_USAGE_SETTINGS.codexFastMode
2401
+ codexFastMode: typeof value.codexFastMode === "boolean" ? value.codexFastMode : DEFAULT_USAGE_SETTINGS.codexFastMode,
2402
+ xaiUsage: typeof value.xaiUsage === "boolean" ? value.xaiUsage : DEFAULT_USAGE_SETTINGS.xaiUsage
1789
2403
  };
1790
2404
  }
1791
2405
  async function loadUsageSettings(path = usageSettingsPath(), signal) {
@@ -1807,7 +2421,7 @@ async function loadUsageSettings(path = usageSettingsPath(), signal) {
1807
2421
  throwIfAborted(signal);
1808
2422
  const document = JSON.parse(text);
1809
2423
  const settings = normalizeUsageSettings(document);
1810
- if (!settings || !isRecord2(document)) throw new Error("invalid settings shape");
2424
+ if (!settings || !isRecord3(document)) throw new Error("invalid settings shape");
1811
2425
  return { kind: "loaded", path, settings, document };
1812
2426
  } catch (error) {
1813
2427
  if (signal?.aborted) throw error;
@@ -1902,7 +2516,7 @@ async function chmodPrivate(path) {
1902
2516
  function throwIfAborted(signal) {
1903
2517
  if (signal?.aborted) throw new DOMException("Settings operation aborted", "AbortError");
1904
2518
  }
1905
- function isRecord2(value) {
2519
+ function isRecord3(value) {
1906
2520
  return typeof value === "object" && value !== null && !Array.isArray(value);
1907
2521
  }
1908
2522
  function isNodeError(error) {
@@ -2011,7 +2625,7 @@ function registerCodexFastMode(pi, settingsRuntime, refreshStatus) {
2011
2625
  const key = activeRequestKey(ctx);
2012
2626
  if (key && ctx.model) {
2013
2627
  pendingFastRequests.set(key, {
2014
- fastRequested: isRecord3(rewritten) && rewritten.service_tier === "priority",
2628
+ fastRequested: isRecord4(rewritten) && rewritten.service_tier === "priority",
2015
2629
  model: ctx.model
2016
2630
  });
2017
2631
  }
@@ -2051,7 +2665,7 @@ function activeRequestKey(ctx) {
2051
2665
  return model ? `${ctx.sessionManager.getSessionId()}:${model.provider}/${model.id}` : void 0;
2052
2666
  }
2053
2667
  function consumeFastRequest(ctx, message, pending) {
2054
- if (!isRecord3(message) || message.role !== "assistant") return NO_FAST_REQUEST;
2668
+ if (!isRecord4(message) || message.role !== "assistant") return NO_FAST_REQUEST;
2055
2669
  const key = messageRequestKey(ctx, message);
2056
2670
  if (!key) return NO_FAST_REQUEST;
2057
2671
  const request = pending.get(key);
@@ -2062,7 +2676,7 @@ function messageRequestKey(ctx, message) {
2062
2676
  if (typeof message.provider !== "string" || typeof message.model !== "string") return void 0;
2063
2677
  return `${ctx.sessionManager.getSessionId()}:${message.provider}/${message.model}`;
2064
2678
  }
2065
- function isRecord3(value) {
2679
+ function isRecord4(value) {
2066
2680
  return typeof value === "object" && value !== null && !Array.isArray(value);
2067
2681
  }
2068
2682
  function isAbortError2(error) {
@@ -2070,8 +2684,8 @@ function isAbortError2(error) {
2070
2684
  }
2071
2685
 
2072
2686
  // src/usage-helpers.ts
2073
- function configuredAdapters(ctx) {
2074
- return SUPPORTED_ADAPTERS.filter(
2687
+ function configuredAdapters(ctx, xaiUsage = true) {
2688
+ return usageAdapters(xaiUsage).filter(
2075
2689
  (adapter) => adapter.id === ctx.model?.provider || providerIsConfigured(ctx, adapter.id)
2076
2690
  );
2077
2691
  }
@@ -2101,6 +2715,118 @@ function isTimeoutError(error) {
2101
2715
  return error instanceof Error && error.name === "TimeoutError";
2102
2716
  }
2103
2717
 
2718
+ // src/usage-settings-ui.ts
2719
+ import {
2720
+ getSettingsListTheme
2721
+ } from "@earendil-works/pi-coding-agent";
2722
+ import {
2723
+ Container,
2724
+ Key,
2725
+ matchesKey,
2726
+ SettingsList,
2727
+ Text
2728
+ } from "@earendil-works/pi-tui";
2729
+ var OFF = "Off";
2730
+ var ON = "On";
2731
+ async function showUsageSettings(ctx, settingsRuntime, parentSignal, isCurrent, onApplied) {
2732
+ if (ctx.mode !== "tui") {
2733
+ if (ctx.hasUI) ctx.ui.notify(`Edit settings manually: ${settingsRuntime.get().path}`, "info");
2734
+ return false;
2735
+ }
2736
+ if (parentSignal.aborted || !isCurrent()) return false;
2737
+ return ctx.ui.custom((tui, theme, _keybindings, done) => {
2738
+ const localController = new AbortController();
2739
+ const signal = AbortSignal.any([parentSignal, localController.signal]);
2740
+ let changed = false;
2741
+ let closing = false;
2742
+ let saveQueue = Promise.resolve();
2743
+ const state = settingsRuntime.get();
2744
+ const items = [
2745
+ {
2746
+ id: "codexFastMode",
2747
+ label: "Codex Fast mode",
2748
+ description: "Use faster Codex routing at increased plan allowance consumption.",
2749
+ currentValue: state.settings.codexFastMode ? ON : OFF,
2750
+ values: [OFF, ON]
2751
+ },
2752
+ {
2753
+ id: "xaiUsage",
2754
+ label: "xAI usage",
2755
+ description: "Report OAuth subscription allowance and credits.",
2756
+ currentValue: state.kind !== "invalid" && state.settings.xaiUsage ? ON : OFF,
2757
+ values: [OFF, ON]
2758
+ }
2759
+ ];
2760
+ const container = new Container();
2761
+ container.addChild(new Text(theme.fg("accent", theme.bold("pi-usage Settings")), 1, 1));
2762
+ let settingsList;
2763
+ const cancel = () => {
2764
+ if (closing) return;
2765
+ closing = true;
2766
+ localController.abort();
2767
+ done(changed);
2768
+ };
2769
+ settingsList = new SettingsList(
2770
+ items,
2771
+ items.length + 2,
2772
+ getSettingsListTheme(),
2773
+ (id, value) => {
2774
+ if (closing || signal.aborted || !isCurrent()) return;
2775
+ const settingId = id;
2776
+ const requested = value !== OFF;
2777
+ saveQueue = saveQueue.then(async () => {
2778
+ const previous = settingsRuntime.get().settings[settingId];
2779
+ if (settingsRuntime.get().kind === "invalid") {
2780
+ const effectivePrevious = settingId === "xaiUsage" ? false : previous;
2781
+ settingsList.updateValue(id, displayValue(settingId, effectivePrevious));
2782
+ if (!signal.aborted && isCurrent()) {
2783
+ ctx.ui.notify("Repair pi-usage.json and reload before changing settings.", "error");
2784
+ tui.requestRender();
2785
+ }
2786
+ return;
2787
+ }
2788
+ try {
2789
+ await settingsRuntime.update({ [settingId]: requested }, signal);
2790
+ } catch (error) {
2791
+ if (signal.aborted || !isCurrent()) return;
2792
+ settingsList.updateValue(id, displayValue(settingId, previous));
2793
+ ctx.ui.notify(`Could not save pi-usage.json: ${errorMessage(error)}`, "error");
2794
+ tui.requestRender();
2795
+ return;
2796
+ }
2797
+ if (previous !== requested) {
2798
+ changed = true;
2799
+ onApplied(settingId, previous, requested);
2800
+ }
2801
+ if (signal.aborted || !isCurrent()) return;
2802
+ settingsList.updateValue(id, displayValue(settingId, requested));
2803
+ tui.requestRender();
2804
+ });
2805
+ },
2806
+ cancel
2807
+ );
2808
+ container.addChild(settingsList);
2809
+ parentSignal.addEventListener("abort", cancel, { once: true });
2810
+ return {
2811
+ render: (width) => container.render(width),
2812
+ invalidate: () => container.invalidate(),
2813
+ handleInput(data) {
2814
+ if (closing) return;
2815
+ if (matchesKey(data, Key.ctrl("c"))) cancel();
2816
+ else settingsList.handleInput(data);
2817
+ tui.requestRender();
2818
+ },
2819
+ dispose() {
2820
+ localController.abort();
2821
+ parentSignal.removeEventListener("abort", cancel);
2822
+ }
2823
+ };
2824
+ });
2825
+ }
2826
+ function displayValue(_id, enabled) {
2827
+ return enabled ? ON : OFF;
2828
+ }
2829
+
2104
2830
  // src/usage.ts
2105
2831
  var CACHE_TTL_MS = 5 * 60 * 1e3;
2106
2832
  var DEFAULT_TIMEOUT_MS = 15e3;
@@ -2112,6 +2838,7 @@ var REFRESH_CURRENT = "Refresh current usage";
2112
2838
  var VIEW_ANOTHER = "View another configured provider\u2026";
2113
2839
  var VIEW_ALL = "View all configured providers\u2026";
2114
2840
  var CLOSE = "Close";
2841
+ var SETTINGS = "Settings";
2115
2842
  var REDEEM_CODEX_RESET = "Redeem usage limit reset\u2026";
2116
2843
  function usageExtension(pi, dependencies = {}) {
2117
2844
  const credentialReader = dependencies.credentialReader;
@@ -2126,9 +2853,16 @@ function usageExtension(pi, dependencies = {}) {
2126
2853
  let activeCurrentIdentity;
2127
2854
  let sessionActive = false;
2128
2855
  let statusGeneration = 0;
2856
+ let sessionGeneration = 0;
2857
+ let xaiSettingsGeneration = 0;
2129
2858
  let statusRefreshTimer;
2130
2859
  let statusController;
2131
2860
  let fastRuntime;
2861
+ const xaiUsageEnabled = () => {
2862
+ const state = settingsRuntime.get();
2863
+ return state.kind !== "invalid" && state.settings.xaiUsage;
2864
+ };
2865
+ const activeAdapterForProvider = (providerId) => adapterForProvider(providerId, xaiUsageEnabled());
2132
2866
  const clearStatusTimer = () => {
2133
2867
  if (statusRefreshTimer) clearTimeout(statusRefreshTimer);
2134
2868
  statusRefreshTimer = void 0;
@@ -2160,7 +2894,7 @@ function usageExtension(pi, dependencies = {}) {
2160
2894
  statusRefreshTimer.unref?.();
2161
2895
  };
2162
2896
  const publishStatus = (ctx, outcome, model, shouldSchedule) => {
2163
- if (adapterForProvider(model.provider)?.publishesStatusline === false) {
2897
+ if (activeAdapterForProvider(model.provider)?.publishesStatusline === false) {
2164
2898
  clearStatusTimer();
2165
2899
  safeSetStatus(ctx, void 0);
2166
2900
  return;
@@ -2206,6 +2940,10 @@ function usageExtension(pi, dependencies = {}) {
2206
2940
  };
2207
2941
  const queryAdapterState = async (ctx, adapter, displayState, force, signal) => {
2208
2942
  const startedAt = Date.now();
2943
+ const expectedSessionGeneration = sessionGeneration;
2944
+ const expectedXaiSettingsGeneration = xaiSettingsGeneration;
2945
+ const expectedSessionId = ctx.sessionManager.getSessionId();
2946
+ const expectedModelIdentity = modelIdentity(ctx.model);
2209
2947
  let auth;
2210
2948
  try {
2211
2949
  auth = await awaitWithDeadline(
@@ -2229,6 +2967,9 @@ function usageExtension(pi, dependencies = {}) {
2229
2967
  }
2230
2968
  };
2231
2969
  }
2970
+ if (adapter.id === "xai" && (expectedSessionGeneration !== sessionGeneration || expectedXaiSettingsGeneration !== xaiSettingsGeneration || !xaiUsageEnabled() || ctx.sessionManager.getSessionId() !== expectedSessionId || modelIdentity(ctx.model) !== expectedModelIdentity)) {
2971
+ throw abortError();
2972
+ }
2232
2973
  if (!auth) {
2233
2974
  if (displayState === "current") {
2234
2975
  transitionCurrentIdentity(`${adapter.id}:unavailable`, adapter.id);
@@ -2280,7 +3021,22 @@ function usageExtension(pi, dependencies = {}) {
2280
3021
  setBoundedMap(latestQueries, failureKey, queryId, MAX_ACCOUNT_STATES);
2281
3022
  try {
2282
3023
  const remainingMs = Math.max(1, DEFAULT_TIMEOUT_MS - (Date.now() - startedAt));
2283
- const report = await queryProviderUsage(adapter, auth, signal, remainingMs);
3024
+ const guard = adapter.id === "xai" ? async () => {
3025
+ if (signal.aborted || expectedSessionGeneration !== sessionGeneration || expectedXaiSettingsGeneration !== xaiSettingsGeneration || !xaiUsageEnabled() || ctx.sessionManager.getSessionId() !== expectedSessionId || modelIdentity(ctx.model) !== expectedModelIdentity) {
3026
+ throw abortError();
3027
+ }
3028
+ const revalidated = await awaitWithDeadline(
3029
+ resolveUsageAuth(ctx, adapter, void 0, credentialReader, credentialCandidates),
3030
+ signal,
3031
+ Math.max(1, DEFAULT_TIMEOUT_MS - (Date.now() - startedAt)),
3032
+ "revalidating xAI runtime auth"
3033
+ );
3034
+ if (signal.aborted || expectedSessionGeneration !== sessionGeneration || expectedXaiSettingsGeneration !== xaiSettingsGeneration || !xaiUsageEnabled() || ctx.sessionManager.getSessionId() !== expectedSessionId || modelIdentity(ctx.model) !== expectedModelIdentity || revalidated?.fingerprint !== auth.fingerprint) {
3035
+ throw abortError();
3036
+ }
3037
+ } : void 0;
3038
+ const report = await queryProviderUsage(adapter, auth, signal, remainingMs, guard);
3039
+ if (guard) await guard();
2284
3040
  if (latestQueries.get(failureKey) === queryId) {
2285
3041
  cache.set(adapter.id, auth.fingerprint, report);
2286
3042
  failureBackoff.delete(failureKey);
@@ -2323,7 +3079,7 @@ function usageExtension(pi, dependencies = {}) {
2323
3079
  }
2324
3080
  };
2325
3081
  const queryCurrentState = async (ctx, model, force, signal) => {
2326
- const adapter = adapterForProvider(model?.provider);
3082
+ const adapter = activeAdapterForProvider(model?.provider);
2327
3083
  if (!adapter) {
2328
3084
  const providerId = model?.provider ?? "none";
2329
3085
  transitionCurrentIdentity(`unsupported:${providerId}`, providerId);
@@ -2333,14 +3089,14 @@ function usageExtension(pi, dependencies = {}) {
2333
3089
  providerName: providerDisplayName(ctx, providerId),
2334
3090
  displayState: "current",
2335
3091
  status: "unsupported",
2336
- message: model ? `Usage reporting is not supported for ${providerDisplayName(ctx, providerId)}.` : "No model is selected."
3092
+ message: providerId === "xai" && !xaiUsageEnabled() ? "xAI usage is disabled. Open Settings to enable it." : model ? `Usage reporting is not supported for ${providerDisplayName(ctx, providerId)}.` : "No model is selected."
2337
3093
  }
2338
3094
  };
2339
3095
  }
2340
3096
  return queryAdapterState(ctx, adapter, "current", force, signal);
2341
3097
  };
2342
3098
  const refreshCurrentStatus = async (ctx, model, force) => {
2343
- const adapter = adapterForProvider(model?.provider);
3099
+ const adapter = activeAdapterForProvider(model?.provider);
2344
3100
  if (!adapter || !model) {
2345
3101
  const providerId = model?.provider ?? "none";
2346
3102
  transitionCurrentIdentity(`unsupported:${providerId}`, providerId);
@@ -2403,7 +3159,7 @@ function usageExtension(pi, dependencies = {}) {
2403
3159
  if (generation !== statusGeneration || modelIdentity(ctx.model) !== modelIdentity(model)) {
2404
3160
  return false;
2405
3161
  }
2406
- const adapter = adapterForProvider(model?.provider);
3162
+ const adapter = activeAdapterForProvider(model?.provider);
2407
3163
  if (outcome.authState === "unavailable") {
2408
3164
  if (!adapter) return false;
2409
3165
  try {
@@ -2498,6 +3254,7 @@ function usageExtension(pi, dependencies = {}) {
2498
3254
  lines: [...formatProviderStates(visibleStates).split("\n"), ...fastLines],
2499
3255
  items: [
2500
3256
  { id: "refresh", label: REFRESH_CURRENT, action: "refresh" },
3257
+ { id: "settings", label: SETTINGS, action: "settings" },
2501
3258
  ...fastAvailability.kind === "available" ? [
2502
3259
  {
2503
3260
  id: "toggle-fast",
@@ -2526,7 +3283,7 @@ function usageExtension(pi, dependencies = {}) {
2526
3283
  providers: () => ({
2527
3284
  kind: "actions",
2528
3285
  title: "Select a configured provider",
2529
- items: configuredAdapters(ctx).filter((adapter) => adapter.id !== ctx.model?.provider).map((adapter) => ({
3286
+ items: configuredAdapters(ctx, xaiUsageEnabled()).filter((adapter) => adapter.id !== ctx.model?.provider).map((adapter) => ({
2530
3287
  id: adapter.id,
2531
3288
  label: adapter.displayName,
2532
3289
  action: "provider"
@@ -2590,6 +3347,37 @@ function usageExtension(pi, dependencies = {}) {
2590
3347
  })
2591
3348
  },
2592
3349
  actions: {
3350
+ settings: async () => {
3351
+ await showUsageSettings(
3352
+ ctx,
3353
+ settingsRuntime,
3354
+ controller.signal,
3355
+ () => statusGeneration === menuGeneration && !controller.signal.aborted,
3356
+ (id, _previous, next) => {
3357
+ if (id !== "xaiUsage") return;
3358
+ xaiSettingsGeneration += 1;
3359
+ invalidateProviderState("xai");
3360
+ if (!next) {
3361
+ for (const active of activeControllers) {
3362
+ if (active !== controller) active.abort();
3363
+ }
3364
+ }
3365
+ }
3366
+ );
3367
+ fastState = settingsRuntime.get();
3368
+ const revalidated = await queryStableCurrent(
3369
+ ctx,
3370
+ false,
3371
+ controller,
3372
+ "Applying usage settings\u2026"
3373
+ );
3374
+ if (!revalidated) return { kind: "stay" };
3375
+ stableCurrent = revalidated;
3376
+ current = revalidated.outcome;
3377
+ visibleStates = [current.state];
3378
+ publishStableCurrent(ctx, revalidated);
3379
+ return { kind: "stay" };
3380
+ },
2593
3381
  "toggle-fast": async () => {
2594
3382
  const availability = fastRuntime.availability(ctx.model);
2595
3383
  if (availability.kind !== "available" || fastState.kind === "invalid") {
@@ -2758,7 +3546,7 @@ function usageExtension(pi, dependencies = {}) {
2758
3546
  return { kind: "stay" };
2759
3547
  },
2760
3548
  another: async () => {
2761
- const others = configuredAdapters(ctx).filter(
3549
+ const others = configuredAdapters(ctx, xaiUsageEnabled()).filter(
2762
3550
  (adapter) => adapter.id !== ctx.model?.provider
2763
3551
  );
2764
3552
  if (others.length === 0) {
@@ -2768,7 +3556,7 @@ function usageExtension(pi, dependencies = {}) {
2768
3556
  return { kind: "to", screen: "providers" };
2769
3557
  },
2770
3558
  provider: async ({ itemId }) => {
2771
- const adapter = configuredAdapters(ctx).find(
3559
+ const adapter = configuredAdapters(ctx, xaiUsageEnabled()).find(
2772
3560
  (candidate) => candidate.id === itemId && candidate.id !== ctx.model?.provider
2773
3561
  );
2774
3562
  if (!adapter) return { kind: "back" };
@@ -2794,7 +3582,7 @@ function usageExtension(pi, dependencies = {}) {
2794
3582
  return { kind: "back" };
2795
3583
  },
2796
3584
  all: async () => {
2797
- const adapters = configuredAdapters(ctx);
3585
+ const adapters = configuredAdapters(ctx, xaiUsageEnabled());
2798
3586
  const currentProviderId = ctx.model?.provider;
2799
3587
  const settled = await runMenuOperation(
2800
3588
  ctx,
@@ -2873,6 +3661,8 @@ function usageExtension(pi, dependencies = {}) {
2873
3661
  }
2874
3662
  });
2875
3663
  pi.on("session_start", (_event, ctx) => {
3664
+ sessionGeneration += 1;
3665
+ xaiSettingsGeneration += 1;
2876
3666
  statusGeneration += 1;
2877
3667
  clearStatusTimer();
2878
3668
  for (const controller of activeControllers) controller.abort();
@@ -2892,6 +3682,8 @@ function usageExtension(pi, dependencies = {}) {
2892
3682
  });
2893
3683
  pi.on("session_shutdown", (_event, ctx) => {
2894
3684
  sessionActive = false;
3685
+ sessionGeneration += 1;
3686
+ xaiSettingsGeneration += 1;
2895
3687
  statusGeneration += 1;
2896
3688
  clearStatusTimer();
2897
3689
  for (const controller of activeControllers) controller.abort();
@@ -2916,6 +3708,7 @@ export {
2916
3708
  DEFAULT_USAGE_SETTINGS,
2917
3709
  SUPPORTED_ADAPTERS,
2918
3710
  UsageCache,
3711
+ XAI_ADAPTER,
2919
3712
  abortError,
2920
3713
  adapterForProvider,
2921
3714
  awaitWithDeadline,
@@ -2938,9 +3731,11 @@ export {
2938
3731
  normalizeCodexBackendPayload,
2939
3732
  normalizeCodexResetCreditsPayload,
2940
3733
  normalizeGitHubCopilotUsagePayload,
3734
+ normalizeKimiCodingUsagePayload,
2941
3735
  normalizeOpenCodeZenPayload,
2942
3736
  normalizeOpenRouterKeyPayload,
2943
3737
  normalizeUsageSettings,
3738
+ normalizeXaiBillingPayload,
2944
3739
  normalizeZaiQuotaPayload,
2945
3740
  providerIsConfigured,
2946
3741
  queryProviderUsage,
@@ -2950,6 +3745,7 @@ export {
2950
3745
  rewriteCodexFastPayload,
2951
3746
  runWithConcurrency,
2952
3747
  sanitizeDisplayText,
3748
+ usageAdapters,
2953
3749
  usageSettingsPath
2954
3750
  };
2955
3751
  //# sourceMappingURL=index.ts.map