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