@narumitw/pi-usage 0.52.3 → 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,11 +895,294 @@ 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
+
1059
+ // src/providers/zai.ts
1060
+ var FIVE_HOUR_WINDOW_MINUTES2 = 300;
1061
+ var WEEKLY_WINDOW_MINUTES2 = 10080;
1062
+ function normalizeZaiQuotaPayload(providerId, providerName, payload, capturedAt) {
1063
+ const data = asObject6(payload.data);
1064
+ if (!data) throw new Error("Z.AI quota response data was not an object.");
1065
+ const limits = Array.isArray(data.limits) ? data.limits : [];
1066
+ const buckets = [];
1067
+ const metrics = [];
1068
+ for (const raw of limits) {
1069
+ const limit = asObject6(raw);
1070
+ if (!limit) continue;
1071
+ const type = asString5(limit.type);
1072
+ const unit = asNonnegativeNumber4(limit.unit);
1073
+ const isPlanUsage = type === "TOKENS_LIMIT" || type === "CREDIT_LIMIT";
1074
+ if (type === "TIME_LIMIT") {
1075
+ addCountBucket(buckets, limit, "mcp-monthly", "MCP monthly allowance");
1076
+ addUsageDetailMetrics(metrics, limit.usageDetails);
1077
+ } else if (isPlanUsage && unit === 3) {
1078
+ addPercentBucket(buckets, limit, "five-hour", "5h window", FIVE_HOUR_WINDOW_MINUTES2);
1079
+ } else if (isPlanUsage && unit === 6) {
1080
+ const used = asNonnegativeNumber4(limit.currentValue);
1081
+ const quota = asNonnegativeNumber4(limit.usage);
1082
+ if (used !== void 0 && quota !== void 0) {
1083
+ addCountBucket(buckets, limit, "weekly", "Weekly window", WEEKLY_WINDOW_MINUTES2);
1084
+ } else {
1085
+ addPercentBucket(buckets, limit, "weekly", "Weekly window", WEEKLY_WINDOW_MINUTES2);
1086
+ }
1087
+ }
1088
+ }
1089
+ if (buckets.length === 0) {
1090
+ throw new Error("Z.AI quota endpoint returned no displayable usage data.");
1091
+ }
1092
+ const notes = [];
1093
+ const level = asString5(data.level);
1094
+ if (level) notes.push(`Plan: ${level}`);
1095
+ return {
1096
+ providerId,
1097
+ providerName,
1098
+ capturedAt,
1099
+ source: "zai-quota",
1100
+ semantics: { kind: "consumer-subscription", label: "GLM Coding Plan usage" },
1101
+ buckets,
1102
+ metrics,
1103
+ ...notes.length > 0 ? { notes } : {}
1104
+ };
1105
+ }
1106
+ function addPercentBucket(buckets, limit, id, label, windowMinutes) {
1107
+ const used = asNonnegativeNumber4(limit.percentage);
1108
+ if (used === void 0) return;
1109
+ const percent = clampPercent3(used);
1110
+ const resetsAt = asEpochSeconds2(limit.nextResetTime);
1111
+ buckets.push({
1112
+ id,
1113
+ label,
1114
+ used: percent,
1115
+ remaining: 100 - percent,
1116
+ limit: 100,
1117
+ unit: "percent",
1118
+ windowMinutes,
1119
+ ...resetsAt !== void 0 ? { resetsAt } : {}
1120
+ });
1121
+ }
1122
+ function addCountBucket(buckets, limit, id, label, windowMinutes) {
1123
+ const used = asNonnegativeNumber4(limit.currentValue);
1124
+ const quota = asNonnegativeNumber4(limit.usage);
1125
+ if (used === void 0 || quota === void 0) return;
1126
+ const resetsAt = asEpochSeconds2(limit.nextResetTime);
1127
+ buckets.push({
1128
+ id,
1129
+ label,
1130
+ used,
1131
+ remaining: Math.max(0, quota - used),
1132
+ limit: quota,
1133
+ unit: "count",
1134
+ ...windowMinutes !== void 0 ? { windowMinutes } : {},
1135
+ ...resetsAt !== void 0 ? { resetsAt } : {}
1136
+ });
1137
+ }
1138
+ function addUsageDetailMetrics(metrics, value) {
1139
+ if (!Array.isArray(value)) return;
1140
+ for (const raw of value) {
1141
+ const detail = asObject6(raw);
1142
+ if (!detail) continue;
1143
+ const label = asString5(detail.modelCode);
1144
+ const usage = asNonnegativeNumber4(detail.usage);
1145
+ if (!label || usage === void 0) continue;
1146
+ metrics.push({ id: `mcp-${kebabCase(label)}`, label, value: usage, unit: "count" });
1147
+ }
1148
+ }
1149
+ function asObject6(value) {
1150
+ if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
1151
+ return value;
1152
+ }
1153
+ function asString5(value) {
1154
+ if (typeof value !== "string") return void 0;
1155
+ return sanitizeDisplayText(value, 80) || void 0;
1156
+ }
1157
+ function asNonnegativeNumber4(value) {
1158
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 0) return void 0;
1159
+ return value;
1160
+ }
1161
+ function asEpochSeconds2(value) {
1162
+ const millis = asNonnegativeNumber4(value);
1163
+ if (millis === void 0) return void 0;
1164
+ return Math.floor(millis / 1e3);
1165
+ }
1166
+ function kebabCase(label) {
1167
+ return label.toLowerCase().replace(/[^a-z0-9]+/gu, "-").replace(/^-+|-+$/gu, "") || "tool";
1168
+ }
1169
+ function clampPercent3(value) {
1170
+ return Math.min(100, Math.max(0, value));
1171
+ }
1172
+
685
1173
  // src/query.ts
686
1174
  var CODEX_USAGE_URL = "https://chatgpt.com/backend-api/wham/usage";
687
1175
  var GITHUB_COPILOT_USAGE_URL = "https://api.github.com/copilot_internal/user";
688
1176
  var OPENROUTER_KEY_URL = "https://openrouter.ai/api/v1/key";
689
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
+ });
690
1186
  var MAX_SUCCESS_BODY_BYTES = 64 * 1024;
691
1187
  var MAX_ERROR_BODY_BYTES = 4 * 1024;
692
1188
  var AUTH_FINGERPRINT_SALT = randomBytes(32);
@@ -756,10 +1252,110 @@ var SUPPORTED_ADAPTERS = [
756
1252
  );
757
1253
  return normalizeOpenCodeZenPayload(payload, Date.now());
758
1254
  }
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
+ },
1272
+ {
1273
+ id: "zai",
1274
+ displayName: "Z.AI",
1275
+ semantics: { kind: "consumer-subscription", label: "GLM Coding Plan usage" },
1276
+ publishesStatusline: false,
1277
+ async query(auth, signal, timeoutMs) {
1278
+ const payload = await fetchProviderJson(
1279
+ zaiMonitorUrl(auth.model.baseUrl),
1280
+ zaiMonitorAuth(auth),
1281
+ signal,
1282
+ timeoutMs,
1283
+ "Z.AI quota endpoint"
1284
+ );
1285
+ return normalizeZaiQuotaPayload("zai", "Z.AI", payload, Date.now());
1286
+ }
1287
+ },
1288
+ {
1289
+ id: "zai-coding-cn",
1290
+ displayName: "Z.AI Coding CN",
1291
+ semantics: { kind: "consumer-subscription", label: "GLM Coding Plan usage" },
1292
+ publishesStatusline: false,
1293
+ async query(auth, signal, timeoutMs) {
1294
+ const payload = await fetchProviderJson(
1295
+ zaiMonitorUrl(auth.model.baseUrl),
1296
+ zaiMonitorAuth(auth),
1297
+ signal,
1298
+ timeoutMs,
1299
+ "Z.AI Coding CN quota endpoint"
1300
+ );
1301
+ return normalizeZaiQuotaPayload(
1302
+ "zai-coding-cn",
1303
+ "Z.AI Coding CN",
1304
+ payload,
1305
+ Date.now()
1306
+ );
1307
+ }
759
1308
  }
760
1309
  ];
761
- function adapterForProvider(providerId) {
762
- 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);
763
1359
  }
764
1360
  function isStaleExtensionContextError(error) {
765
1361
  return error instanceof Error && error.message.includes("This extension ctx is stale after session replacement or reload");
@@ -805,6 +1401,11 @@ async function resolveUsageAuth(ctx, adapter, salt = AUTH_FINGERPRINT_SALT, cred
805
1401
  offered.offeredCount === 0
806
1402
  );
807
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
+ }
808
1409
  const authorization = authorizationFrom(auth);
809
1410
  if (!authorization) return void 0;
810
1411
  const headers = { Authorization: authorization };
@@ -819,9 +1420,9 @@ async function resolveUsageAuth(ctx, adapter, salt = AUTH_FINGERPRINT_SALT, cred
819
1420
  model
820
1421
  };
821
1422
  }
822
- async function queryProviderUsage(adapter, auth, signal, timeoutMs) {
1423
+ async function queryProviderUsage(adapter, auth, signal, timeoutMs, guard) {
823
1424
  try {
824
- return await adapter.query(auth, signal, timeoutMs);
1425
+ return await adapter.query(auth, signal, timeoutMs, guard);
825
1426
  } catch (error) {
826
1427
  if (isStaleExtensionContextError(error) || isAbortError(error)) throw error;
827
1428
  throw new Error(redactUsageError(errorMessage(error), auth.secrets));
@@ -861,7 +1462,9 @@ async function fetchProviderJson(url, auth, signal, timeoutMs, description, requ
861
1462
  }, timeoutMs);
862
1463
  try {
863
1464
  const headers = { ...auth.headers };
864
- 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
+ }
865
1468
  if (request.body && !hasHeader(headers, "Content-Type")) {
866
1469
  headers["Content-Type"] = "application/json";
867
1470
  }
@@ -869,15 +1472,18 @@ async function fetchProviderJson(url, auth, signal, timeoutMs, description, requ
869
1472
  method: request.method ?? "GET",
870
1473
  headers,
871
1474
  ...request.body ? { body: JSON.stringify(request.body) } : {},
1475
+ ...request.redirect ? { redirect: request.redirect } : {},
872
1476
  signal: controller.signal
873
1477
  });
1478
+ if (response.redirected) throw new Error(`${description} refused a redirected response.`);
874
1479
  if (controller.signal.aborted)
875
1480
  throw Object.assign(new Error("Usage query aborted."), { name: "AbortError" });
876
1481
  const text = await readBoundedResponse(
877
1482
  response,
878
1483
  response.ok ? MAX_SUCCESS_BODY_BYTES : MAX_ERROR_BODY_BYTES,
879
1484
  !response.ok,
880
- description
1485
+ description,
1486
+ controller.signal
881
1487
  );
882
1488
  if (controller.signal.aborted)
883
1489
  throw Object.assign(new Error("Usage query aborted."), { name: "AbortError" });
@@ -908,12 +1514,15 @@ async function fetchProviderJson(url, auth, signal, timeoutMs, description, requ
908
1514
  signal.removeEventListener("abort", abortFromCaller);
909
1515
  }
910
1516
  }
911
- async function readBoundedResponse(response, maxBytes, truncateOverflow, description) {
1517
+ async function readBoundedResponse(response, maxBytes, truncateOverflow, description, signal) {
912
1518
  if (!response.body) return "";
913
1519
  const reader = response.body.getReader();
914
1520
  const chunks = [];
915
1521
  let total = 0;
916
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 });
917
1526
  try {
918
1527
  while (true) {
919
1528
  const { done, value } = await reader.read();
@@ -930,6 +1539,7 @@ async function readBoundedResponse(response, maxBytes, truncateOverflow, descrip
930
1539
  total += value.byteLength;
931
1540
  }
932
1541
  } finally {
1542
+ signal.removeEventListener("abort", abort);
933
1543
  reader.releaseLock();
934
1544
  }
935
1545
  if (truncated && !truncateOverflow) {
@@ -944,6 +1554,59 @@ async function readBoundedResponse(response, maxBytes, truncateOverflow, descrip
944
1554
  const text = new TextDecoder().decode(body);
945
1555
  return truncated ? `${text}\u2026` : text;
946
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
+ }
947
1610
  function resolveGitHubCopilotUsageAuth(auth, model, salt, candidates, standaloneFallback) {
948
1611
  const resolvedAccess = bearerToken(headerValue(auth.headers, "Authorization")) ?? auth.apiKey;
949
1612
  if (!resolvedAccess) throw new Error("GitHub Copilot OAuth credentials were incomplete.");
@@ -954,7 +1617,7 @@ function resolveGitHubCopilotUsageAuth(auth, model, salt, candidates, standalone
954
1617
  const matches = /* @__PURE__ */ new Map();
955
1618
  for (const candidate of candidates) {
956
1619
  try {
957
- const credential = asObject5(candidate);
1620
+ const credential = asObject7(candidate);
958
1621
  if (credential?.type !== "oauth") continue;
959
1622
  sawOAuth = true;
960
1623
  const storedAccess2 = typeof credential.access === "string" && credential.access ? credential.access : void 0;
@@ -1016,7 +1679,7 @@ function bearerToken(authorization) {
1016
1679
  const match = /^Bearer\s+(.+)$/iu.exec(authorization ?? "");
1017
1680
  return match?.[1];
1018
1681
  }
1019
- function asObject5(value) {
1682
+ function asObject7(value) {
1020
1683
  if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
1021
1684
  return value;
1022
1685
  }
@@ -1037,6 +1700,10 @@ function hasOfficialUrlOrigin(value, providerId) {
1037
1700
  if (providerId === "openai-codex") return url.origin === "https://chatgpt.com";
1038
1701
  if (providerId === "openrouter") return url.origin === "https://openrouter.ai";
1039
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";
1705
+ if (providerId === "zai") return url.origin === "https://api.z.ai";
1706
+ if (providerId === "zai-coding-cn") return url.origin === "https://open.bigmodel.cn";
1040
1707
  if (providerId === "github-copilot") {
1041
1708
  return url.protocol === "https:" && /^api\.[a-z0-9-]+\.githubcopilot\.com$/u.test(url.hostname);
1042
1709
  }
@@ -1054,6 +1721,28 @@ function headerValue(headers, name) {
1054
1721
  function hasHeader(headers, name) {
1055
1722
  return Object.keys(headers).some((key) => key.toLowerCase() === name.toLowerCase());
1056
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
+ }
1735
+ function zaiMonitorUrl(baseUrl) {
1736
+ const base = baseUrl?.trim();
1737
+ if (!base) throw new Error("Z.AI model base URL is unavailable.");
1738
+ return `${new URL(base).origin}/api/monitor/usage/quota/limit`;
1739
+ }
1740
+ function zaiMonitorAuth(auth) {
1741
+ const authorization = headerValue(auth.headers, "Authorization");
1742
+ const token = authorization === void 0 ? void 0 : bearerToken(authorization) ?? authorization;
1743
+ if (token === void 0 || token === authorization) return auth;
1744
+ return { ...auth, headers: { ...auth.headers, Authorization: token } };
1745
+ }
1057
1746
  function isAbortError(error) {
1058
1747
  return error instanceof Error && error.name === "AbortError";
1059
1748
  }
@@ -1196,7 +1885,7 @@ function normalizeCodexResetCreditsPayload(payload) {
1196
1885
  if (rawCredits !== void 0 && !Array.isArray(rawCredits)) {
1197
1886
  throw new Error("Codex reset credits response returned invalid credits.");
1198
1887
  }
1199
- const options = (rawCredits ?? []).map(asObject6).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(
1200
1889
  (left, right) => (left.expiresAt ?? Number.MAX_SAFE_INTEGER) - (right.expiresAt ?? Number.MAX_SAFE_INTEGER)
1201
1890
  ).slice(0, Math.min(availableCount, MAX_RESET_OPTIONS));
1202
1891
  if (availableCount > 0 && options.length === 0) {
@@ -1211,7 +1900,7 @@ function selectCodexResetCredential(candidates, resolvedAccess, resolvedAccountI
1211
1900
  const matches = /* @__PURE__ */ new Map();
1212
1901
  for (const candidate of candidates) {
1213
1902
  try {
1214
- const credential = asObject6(candidate);
1903
+ const credential = asObject8(candidate);
1215
1904
  if (credential?.type !== "oauth") continue;
1216
1905
  sawOAuth = true;
1217
1906
  const storedAccess = asNonemptyString(credential.access);
@@ -1250,7 +1939,7 @@ function codexAccountIdFromAccessToken(access) {
1250
1939
  const parts = access.split(".");
1251
1940
  if (parts.length !== 3 || !parts[1]) return void 0;
1252
1941
  const payload = JSON.parse(Buffer.from(parts[1], "base64url").toString("utf8"));
1253
- const claims = asObject6(asObject6(payload)?.["https://api.openai.com/auth"]);
1942
+ const claims = asObject8(asObject8(payload)?.["https://api.openai.com/auth"]);
1254
1943
  return validHeaderValue(claims?.chatgpt_account_id);
1255
1944
  } catch {
1256
1945
  return void 0;
@@ -1282,7 +1971,7 @@ function normalizeResetOption(credit) {
1282
1971
  function isCodexResetOutcomeCode(value) {
1283
1972
  return value === "reset" || value === "nothing_to_reset" || value === "no_credit" || value === "already_redeemed";
1284
1973
  }
1285
- function asObject6(value) {
1974
+ function asObject8(value) {
1286
1975
  if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
1287
1976
  return value;
1288
1977
  }
@@ -1330,7 +2019,11 @@ function formatUsageReport(report, displayState) {
1330
2019
  else if (report.providerId === "github-copilot") formatGitHubCopilotReport(lines, report);
1331
2020
  else if (report.providerId === "openrouter") formatOpenRouterReport(lines, report);
1332
2021
  else if (report.providerId === "opencode-go") formatOpenCodeZenReport(lines, report);
1333
- else formatGenericReport(lines, report);
2022
+ else if (report.providerId === "kimi-coding") formatKimiCodingReport(lines, report);
2023
+ else if (report.providerId === "xai") formatXaiReport(lines, report);
2024
+ else if (report.providerId === "zai" || report.providerId === "zai-coding-cn") {
2025
+ formatZaiReport(lines, report);
2026
+ } else formatGenericReport(lines, report);
1334
2027
  if (report.notes) {
1335
2028
  for (const note of report.notes) lines.push(note);
1336
2029
  }
@@ -1346,6 +2039,7 @@ function formatUsageStatusline(report, model) {
1346
2039
  if (typeof total?.value === "number") return `openrouter ${formatUsd(total.value)} used`;
1347
2040
  }
1348
2041
  if (report.providerId === "opencode-go") return formatOpenCodeZenStatusline(report);
2042
+ if (report.providerId === "kimi-coding") return formatKimiCodingStatusline(report);
1349
2043
  return void 0;
1350
2044
  }
1351
2045
  function formatProviderStates(states) {
@@ -1417,7 +2111,7 @@ function compactGitHubCopilotQuotaKind(bucket) {
1417
2111
  }
1418
2112
  function percentRemaining(bucket) {
1419
2113
  if (!bucket.limit || bucket.remaining === void 0) return 0;
1420
- return Math.round(clampPercent3(bucket.remaining / bucket.limit * 100));
2114
+ return Math.round(clampPercent4(bucket.remaining / bucket.limit * 100));
1421
2115
  }
1422
2116
  function formatOpenRouterReport(lines, report) {
1423
2117
  const limit = report.buckets.find((bucket) => bucket.id === "key-limit");
@@ -1444,10 +2138,114 @@ function formatOpenCodeZenStatusline(report) {
1444
2138
  for (const bucket of report.buckets) {
1445
2139
  if (bucket.used === void 0) continue;
1446
2140
  const compact = bucket.id === "rolling" ? "r" : bucket.id === "weekly" ? "w" : "m";
1447
- parts.push(`${clampPercent3(bucket.used).toFixed(0)}% ${compact}`);
2141
+ parts.push(`${clampPercent4(bucket.used).toFixed(0)}% ${compact}`);
2142
+ }
2143
+ return parts.length > 1 ? parts.join(" ") : void 0;
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
+ );
1448
2187
  }
1449
2188
  return parts.length > 1 ? parts.join(" ") : void 0;
1450
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
+ }
2226
+ function formatZaiReport(lines, report) {
2227
+ for (const bucket of report.buckets) {
2228
+ const reset = bucket.resetsAt ? ` (resets ${formatReset(bucket.resetsAt)})` : "";
2229
+ let value = "unavailable";
2230
+ if (bucket.unit === "percent" && bucket.used !== void 0) {
2231
+ value = `${bucket.used}% used`;
2232
+ if (bucket.remaining !== void 0) value += ` \xB7 ${bucket.remaining}% left`;
2233
+ } else if (bucket.used !== void 0 && bucket.limit !== void 0) {
2234
+ value = `${bucket.used} of ${bucket.limit} used`;
2235
+ if (bucket.remaining !== void 0) value += ` \xB7 ${bucket.remaining} left`;
2236
+ } else if (bucket.used !== void 0) {
2237
+ value = `${bucket.used} used`;
2238
+ } else if (bucket.remaining !== void 0) {
2239
+ value = `${bucket.remaining} left`;
2240
+ }
2241
+ lines.push(`${`${bucket.label}:`.padEnd(VALUE_COLUMN)}${value}${reset}`);
2242
+ }
2243
+ for (const metric of report.metrics) {
2244
+ lines.push(
2245
+ `${`${metric.label}:`.padEnd(VALUE_COLUMN)}${formatMetricValue(metric.value, metric.unit)}`
2246
+ );
2247
+ }
2248
+ }
1451
2249
  function formatGenericReport(lines, report) {
1452
2250
  for (const bucket of report.buckets) {
1453
2251
  lines.push(
@@ -1472,7 +2270,7 @@ function formatCodexStatusline(report, model) {
1472
2270
  if (bucket.remaining === void 0) continue;
1473
2271
  const fallback = bucket.id.endsWith(":secondary") ? "weekly" : "5h";
1474
2272
  parts.push(
1475
- `${clampPercent3(bucket.remaining).toFixed(0)}% ${formatWindowLabel(bucket.windowMinutes, fallback, true)}`
2273
+ `${clampPercent4(bucket.remaining).toFixed(0)}% ${formatWindowLabel(bucket.windowMinutes, fallback, true)}`
1476
2274
  );
1477
2275
  }
1478
2276
  return parts.length > 1 ? parts.join(" ") : formatCodexCreditsStatus(report);
@@ -1539,7 +2337,7 @@ function compactLimitLabel(label) {
1539
2337
  return (suffix || normalized).toLowerCase().replace(/\s+/g, " ");
1540
2338
  }
1541
2339
  function formatPercentBucket(bucket) {
1542
- const remaining = clampPercent3(bucket.remaining ?? 0);
2340
+ const remaining = clampPercent4(bucket.remaining ?? 0);
1543
2341
  const filled = Math.round(remaining / 100 * BAR_SEGMENTS);
1544
2342
  const reset = bucket.resetsAt ? ` (resets ${formatReset(bucket.resetsAt)})` : "";
1545
2343
  return `[${"\u2588".repeat(filled)}${"\u2591".repeat(BAR_SEGMENTS - filled)}] ${remaining.toFixed(0)}% left${reset}`;
@@ -1572,7 +2370,7 @@ function formatReset(epochSeconds) {
1572
2370
  function capitalize(value) {
1573
2371
  return `${value[0]?.toUpperCase() ?? ""}${value.slice(1)}`;
1574
2372
  }
1575
- function clampPercent3(value) {
2373
+ function clampPercent4(value) {
1576
2374
  return Math.min(100, Math.max(0, value));
1577
2375
  }
1578
2376
 
@@ -1585,18 +2383,23 @@ import { getAgentDir } from "@earendil-works/pi-coding-agent";
1585
2383
  var USAGE_SETTINGS_FILE = "pi-usage.json";
1586
2384
  var MAX_USAGE_SETTINGS_BYTES = 64 * 1024;
1587
2385
  var DEFAULT_USAGE_SETTINGS = Object.freeze({
1588
- codexFastMode: false
2386
+ codexFastMode: false,
2387
+ xaiUsage: true
1589
2388
  });
1590
2389
  function usageSettingsPath() {
1591
2390
  return join(getAgentDir(), USAGE_SETTINGS_FILE);
1592
2391
  }
1593
2392
  function normalizeUsageSettings(value) {
1594
- if (!isRecord2(value)) return void 0;
2393
+ if (!isRecord3(value)) return void 0;
1595
2394
  if (Object.hasOwn(value, "codexFastMode") && typeof value.codexFastMode !== "boolean") {
1596
2395
  return void 0;
1597
2396
  }
2397
+ if (Object.hasOwn(value, "xaiUsage") && typeof value.xaiUsage !== "boolean") {
2398
+ return void 0;
2399
+ }
1598
2400
  return {
1599
- 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
1600
2403
  };
1601
2404
  }
1602
2405
  async function loadUsageSettings(path = usageSettingsPath(), signal) {
@@ -1618,7 +2421,7 @@ async function loadUsageSettings(path = usageSettingsPath(), signal) {
1618
2421
  throwIfAborted(signal);
1619
2422
  const document = JSON.parse(text);
1620
2423
  const settings = normalizeUsageSettings(document);
1621
- if (!settings || !isRecord2(document)) throw new Error("invalid settings shape");
2424
+ if (!settings || !isRecord3(document)) throw new Error("invalid settings shape");
1622
2425
  return { kind: "loaded", path, settings, document };
1623
2426
  } catch (error) {
1624
2427
  if (signal?.aborted) throw error;
@@ -1713,7 +2516,7 @@ async function chmodPrivate(path) {
1713
2516
  function throwIfAborted(signal) {
1714
2517
  if (signal?.aborted) throw new DOMException("Settings operation aborted", "AbortError");
1715
2518
  }
1716
- function isRecord2(value) {
2519
+ function isRecord3(value) {
1717
2520
  return typeof value === "object" && value !== null && !Array.isArray(value);
1718
2521
  }
1719
2522
  function isNodeError(error) {
@@ -1822,7 +2625,7 @@ function registerCodexFastMode(pi, settingsRuntime, refreshStatus) {
1822
2625
  const key = activeRequestKey(ctx);
1823
2626
  if (key && ctx.model) {
1824
2627
  pendingFastRequests.set(key, {
1825
- fastRequested: isRecord3(rewritten) && rewritten.service_tier === "priority",
2628
+ fastRequested: isRecord4(rewritten) && rewritten.service_tier === "priority",
1826
2629
  model: ctx.model
1827
2630
  });
1828
2631
  }
@@ -1862,7 +2665,7 @@ function activeRequestKey(ctx) {
1862
2665
  return model ? `${ctx.sessionManager.getSessionId()}:${model.provider}/${model.id}` : void 0;
1863
2666
  }
1864
2667
  function consumeFastRequest(ctx, message, pending) {
1865
- if (!isRecord3(message) || message.role !== "assistant") return NO_FAST_REQUEST;
2668
+ if (!isRecord4(message) || message.role !== "assistant") return NO_FAST_REQUEST;
1866
2669
  const key = messageRequestKey(ctx, message);
1867
2670
  if (!key) return NO_FAST_REQUEST;
1868
2671
  const request = pending.get(key);
@@ -1873,7 +2676,7 @@ function messageRequestKey(ctx, message) {
1873
2676
  if (typeof message.provider !== "string" || typeof message.model !== "string") return void 0;
1874
2677
  return `${ctx.sessionManager.getSessionId()}:${message.provider}/${message.model}`;
1875
2678
  }
1876
- function isRecord3(value) {
2679
+ function isRecord4(value) {
1877
2680
  return typeof value === "object" && value !== null && !Array.isArray(value);
1878
2681
  }
1879
2682
  function isAbortError2(error) {
@@ -1881,8 +2684,8 @@ function isAbortError2(error) {
1881
2684
  }
1882
2685
 
1883
2686
  // src/usage-helpers.ts
1884
- function configuredAdapters(ctx) {
1885
- return SUPPORTED_ADAPTERS.filter(
2687
+ function configuredAdapters(ctx, xaiUsage = true) {
2688
+ return usageAdapters(xaiUsage).filter(
1886
2689
  (adapter) => adapter.id === ctx.model?.provider || providerIsConfigured(ctx, adapter.id)
1887
2690
  );
1888
2691
  }
@@ -1912,6 +2715,118 @@ function isTimeoutError(error) {
1912
2715
  return error instanceof Error && error.name === "TimeoutError";
1913
2716
  }
1914
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
+
1915
2830
  // src/usage.ts
1916
2831
  var CACHE_TTL_MS = 5 * 60 * 1e3;
1917
2832
  var DEFAULT_TIMEOUT_MS = 15e3;
@@ -1923,6 +2838,7 @@ var REFRESH_CURRENT = "Refresh current usage";
1923
2838
  var VIEW_ANOTHER = "View another configured provider\u2026";
1924
2839
  var VIEW_ALL = "View all configured providers\u2026";
1925
2840
  var CLOSE = "Close";
2841
+ var SETTINGS = "Settings";
1926
2842
  var REDEEM_CODEX_RESET = "Redeem usage limit reset\u2026";
1927
2843
  function usageExtension(pi, dependencies = {}) {
1928
2844
  const credentialReader = dependencies.credentialReader;
@@ -1937,9 +2853,16 @@ function usageExtension(pi, dependencies = {}) {
1937
2853
  let activeCurrentIdentity;
1938
2854
  let sessionActive = false;
1939
2855
  let statusGeneration = 0;
2856
+ let sessionGeneration = 0;
2857
+ let xaiSettingsGeneration = 0;
1940
2858
  let statusRefreshTimer;
1941
2859
  let statusController;
1942
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());
1943
2866
  const clearStatusTimer = () => {
1944
2867
  if (statusRefreshTimer) clearTimeout(statusRefreshTimer);
1945
2868
  statusRefreshTimer = void 0;
@@ -1971,6 +2894,11 @@ function usageExtension(pi, dependencies = {}) {
1971
2894
  statusRefreshTimer.unref?.();
1972
2895
  };
1973
2896
  const publishStatus = (ctx, outcome, model, shouldSchedule) => {
2897
+ if (activeAdapterForProvider(model.provider)?.publishesStatusline === false) {
2898
+ clearStatusTimer();
2899
+ safeSetStatus(ctx, void 0);
2900
+ return;
2901
+ }
1974
2902
  if (outcome.state.status === "unsupported") {
1975
2903
  clearStatusTimer();
1976
2904
  safeSetStatus(ctx, void 0);
@@ -2012,6 +2940,10 @@ function usageExtension(pi, dependencies = {}) {
2012
2940
  };
2013
2941
  const queryAdapterState = async (ctx, adapter, displayState, force, signal) => {
2014
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);
2015
2947
  let auth;
2016
2948
  try {
2017
2949
  auth = await awaitWithDeadline(
@@ -2035,6 +2967,9 @@ function usageExtension(pi, dependencies = {}) {
2035
2967
  }
2036
2968
  };
2037
2969
  }
2970
+ if (adapter.id === "xai" && (expectedSessionGeneration !== sessionGeneration || expectedXaiSettingsGeneration !== xaiSettingsGeneration || !xaiUsageEnabled() || ctx.sessionManager.getSessionId() !== expectedSessionId || modelIdentity(ctx.model) !== expectedModelIdentity)) {
2971
+ throw abortError();
2972
+ }
2038
2973
  if (!auth) {
2039
2974
  if (displayState === "current") {
2040
2975
  transitionCurrentIdentity(`${adapter.id}:unavailable`, adapter.id);
@@ -2086,7 +3021,22 @@ function usageExtension(pi, dependencies = {}) {
2086
3021
  setBoundedMap(latestQueries, failureKey, queryId, MAX_ACCOUNT_STATES);
2087
3022
  try {
2088
3023
  const remainingMs = Math.max(1, DEFAULT_TIMEOUT_MS - (Date.now() - startedAt));
2089
- 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();
2090
3040
  if (latestQueries.get(failureKey) === queryId) {
2091
3041
  cache.set(adapter.id, auth.fingerprint, report);
2092
3042
  failureBackoff.delete(failureKey);
@@ -2129,7 +3079,7 @@ function usageExtension(pi, dependencies = {}) {
2129
3079
  }
2130
3080
  };
2131
3081
  const queryCurrentState = async (ctx, model, force, signal) => {
2132
- const adapter = adapterForProvider(model?.provider);
3082
+ const adapter = activeAdapterForProvider(model?.provider);
2133
3083
  if (!adapter) {
2134
3084
  const providerId = model?.provider ?? "none";
2135
3085
  transitionCurrentIdentity(`unsupported:${providerId}`, providerId);
@@ -2139,20 +3089,24 @@ function usageExtension(pi, dependencies = {}) {
2139
3089
  providerName: providerDisplayName(ctx, providerId),
2140
3090
  displayState: "current",
2141
3091
  status: "unsupported",
2142
- 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."
2143
3093
  }
2144
3094
  };
2145
3095
  }
2146
3096
  return queryAdapterState(ctx, adapter, "current", force, signal);
2147
3097
  };
2148
3098
  const refreshCurrentStatus = async (ctx, model, force) => {
2149
- const adapter = adapterForProvider(model?.provider);
3099
+ const adapter = activeAdapterForProvider(model?.provider);
2150
3100
  if (!adapter || !model) {
2151
3101
  const providerId = model?.provider ?? "none";
2152
3102
  transitionCurrentIdentity(`unsupported:${providerId}`, providerId);
2153
3103
  clearStatus(ctx);
2154
3104
  return;
2155
3105
  }
3106
+ if (adapter.publishesStatusline === false) {
3107
+ clearStatus(ctx);
3108
+ return;
3109
+ }
2156
3110
  statusGeneration += 1;
2157
3111
  const generation = statusGeneration;
2158
3112
  statusController?.abort();
@@ -2205,7 +3159,7 @@ function usageExtension(pi, dependencies = {}) {
2205
3159
  if (generation !== statusGeneration || modelIdentity(ctx.model) !== modelIdentity(model)) {
2206
3160
  return false;
2207
3161
  }
2208
- const adapter = adapterForProvider(model?.provider);
3162
+ const adapter = activeAdapterForProvider(model?.provider);
2209
3163
  if (outcome.authState === "unavailable") {
2210
3164
  if (!adapter) return false;
2211
3165
  try {
@@ -2300,6 +3254,7 @@ function usageExtension(pi, dependencies = {}) {
2300
3254
  lines: [...formatProviderStates(visibleStates).split("\n"), ...fastLines],
2301
3255
  items: [
2302
3256
  { id: "refresh", label: REFRESH_CURRENT, action: "refresh" },
3257
+ { id: "settings", label: SETTINGS, action: "settings" },
2303
3258
  ...fastAvailability.kind === "available" ? [
2304
3259
  {
2305
3260
  id: "toggle-fast",
@@ -2328,7 +3283,7 @@ function usageExtension(pi, dependencies = {}) {
2328
3283
  providers: () => ({
2329
3284
  kind: "actions",
2330
3285
  title: "Select a configured provider",
2331
- 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) => ({
2332
3287
  id: adapter.id,
2333
3288
  label: adapter.displayName,
2334
3289
  action: "provider"
@@ -2392,6 +3347,37 @@ function usageExtension(pi, dependencies = {}) {
2392
3347
  })
2393
3348
  },
2394
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
+ },
2395
3381
  "toggle-fast": async () => {
2396
3382
  const availability = fastRuntime.availability(ctx.model);
2397
3383
  if (availability.kind !== "available" || fastState.kind === "invalid") {
@@ -2560,7 +3546,7 @@ function usageExtension(pi, dependencies = {}) {
2560
3546
  return { kind: "stay" };
2561
3547
  },
2562
3548
  another: async () => {
2563
- const others = configuredAdapters(ctx).filter(
3549
+ const others = configuredAdapters(ctx, xaiUsageEnabled()).filter(
2564
3550
  (adapter) => adapter.id !== ctx.model?.provider
2565
3551
  );
2566
3552
  if (others.length === 0) {
@@ -2570,7 +3556,7 @@ function usageExtension(pi, dependencies = {}) {
2570
3556
  return { kind: "to", screen: "providers" };
2571
3557
  },
2572
3558
  provider: async ({ itemId }) => {
2573
- const adapter = configuredAdapters(ctx).find(
3559
+ const adapter = configuredAdapters(ctx, xaiUsageEnabled()).find(
2574
3560
  (candidate) => candidate.id === itemId && candidate.id !== ctx.model?.provider
2575
3561
  );
2576
3562
  if (!adapter) return { kind: "back" };
@@ -2596,7 +3582,7 @@ function usageExtension(pi, dependencies = {}) {
2596
3582
  return { kind: "back" };
2597
3583
  },
2598
3584
  all: async () => {
2599
- const adapters = configuredAdapters(ctx);
3585
+ const adapters = configuredAdapters(ctx, xaiUsageEnabled());
2600
3586
  const currentProviderId = ctx.model?.provider;
2601
3587
  const settled = await runMenuOperation(
2602
3588
  ctx,
@@ -2675,6 +3661,8 @@ function usageExtension(pi, dependencies = {}) {
2675
3661
  }
2676
3662
  });
2677
3663
  pi.on("session_start", (_event, ctx) => {
3664
+ sessionGeneration += 1;
3665
+ xaiSettingsGeneration += 1;
2678
3666
  statusGeneration += 1;
2679
3667
  clearStatusTimer();
2680
3668
  for (const controller of activeControllers) controller.abort();
@@ -2694,6 +3682,8 @@ function usageExtension(pi, dependencies = {}) {
2694
3682
  });
2695
3683
  pi.on("session_shutdown", (_event, ctx) => {
2696
3684
  sessionActive = false;
3685
+ sessionGeneration += 1;
3686
+ xaiSettingsGeneration += 1;
2697
3687
  statusGeneration += 1;
2698
3688
  clearStatusTimer();
2699
3689
  for (const controller of activeControllers) controller.abort();
@@ -2718,6 +3708,7 @@ export {
2718
3708
  DEFAULT_USAGE_SETTINGS,
2719
3709
  SUPPORTED_ADAPTERS,
2720
3710
  UsageCache,
3711
+ XAI_ADAPTER,
2721
3712
  abortError,
2722
3713
  adapterForProvider,
2723
3714
  awaitWithDeadline,
@@ -2740,9 +3731,12 @@ export {
2740
3731
  normalizeCodexBackendPayload,
2741
3732
  normalizeCodexResetCreditsPayload,
2742
3733
  normalizeGitHubCopilotUsagePayload,
3734
+ normalizeKimiCodingUsagePayload,
2743
3735
  normalizeOpenCodeZenPayload,
2744
3736
  normalizeOpenRouterKeyPayload,
2745
3737
  normalizeUsageSettings,
3738
+ normalizeXaiBillingPayload,
3739
+ normalizeZaiQuotaPayload,
2746
3740
  providerIsConfigured,
2747
3741
  queryProviderUsage,
2748
3742
  redactUsageError,
@@ -2751,6 +3745,7 @@ export {
2751
3745
  rewriteCodexFastPayload,
2752
3746
  runWithConcurrency,
2753
3747
  sanitizeDisplayText,
3748
+ usageAdapters,
2754
3749
  usageSettingsPath
2755
3750
  };
2756
3751
  //# sourceMappingURL=index.ts.map