@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/README.md +94 -7
- package/dist/index.ts +843 -47
- package/dist/index.ts.map +4 -4
- package/package.json +10 -4
- package/src/format.ts +90 -0
- package/src/index.ts +7 -0
- package/src/providers/kimi-coding.ts +276 -0
- package/src/providers/xai.ts +186 -0
- package/src/query.ts +196 -3
- package/src/settings.ts +7 -0
- package/src/types.ts +23 -2
- package/src/usage-helpers.ts +3 -3
- package/src/usage-settings-ui.ts +128 -0
- package/src/usage.ts +119 -13
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 =
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
687
|
-
var
|
|
1060
|
+
var FIVE_HOUR_WINDOW_MINUTES2 = 300;
|
|
1061
|
+
var WEEKLY_WINDOW_MINUTES2 = 10080;
|
|
688
1062
|
function normalizeZaiQuotaPayload(providerId, providerName, payload, capturedAt) {
|
|
689
|
-
const 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 =
|
|
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",
|
|
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",
|
|
1083
|
+
addCountBucket(buckets, limit, "weekly", "Weekly window", WEEKLY_WINDOW_MINUTES2);
|
|
710
1084
|
} else {
|
|
711
|
-
addPercentBucket(buckets, limit, "weekly", "Weekly window",
|
|
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 =
|
|
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
|
|
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
|
-
|
|
913
|
-
|
|
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"))
|
|
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 =
|
|
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
|
|
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(
|
|
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 =
|
|
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 =
|
|
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
|
|
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 (!
|
|
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 || !
|
|
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
|
|
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:
|
|
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 (!
|
|
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
|
|
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
|
|
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 (
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|