@hk_net/pi-usage-bars 0.3.0 → 0.4.1

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.
@@ -2,18 +2,63 @@ import * as fs from "node:fs";
2
2
  import * as os from "node:os";
3
3
  import * as path from "node:path";
4
4
 
5
- export type ProviderKey = "codex" | "claude" | "zai" | "zai-cn";
6
- export type PiProviderId = "openai-codex" | "anthropic" | "zai" | "zai-coding-cn";
5
+ export type ProviderKey =
6
+ | "codex"
7
+ | "claude"
8
+ | "zai"
9
+ | "zai-cn"
10
+ | "kimi"
11
+ | "minimax"
12
+ | "minimax-cn"
13
+ | "openrouter"
14
+ | "deepseek"
15
+ | "moonshot"
16
+ | "moonshot-cn";
17
+ export type PiProviderId =
18
+ | "openai-codex"
19
+ | "anthropic"
20
+ | "zai"
21
+ | "zai-coding-cn"
22
+ | "kimi-coding"
23
+ | "minimax"
24
+ | "minimax-cn"
25
+ | "openrouter"
26
+ | "deepseek"
27
+ | "moonshotai"
28
+ | "moonshotai-cn";
29
+
30
+ export interface AccountBalance {
31
+ amount: number;
32
+ unit: string;
33
+ label: string;
34
+ }
35
+
36
+ export interface AccountSpend {
37
+ unit: string;
38
+ daily?: number;
39
+ weekly?: number;
40
+ monthly?: number;
41
+ lifetime?: number;
42
+ }
7
43
 
8
44
  export interface UsageData {
9
45
  session: number;
10
46
  weekly: number;
47
+ quotaHidden?: boolean;
48
+ accountBalance?: AccountBalance;
49
+ accountBalanceDetails?: AccountBalance[];
50
+ accountSpend?: AccountSpend;
11
51
  sessionResetsIn?: string;
12
52
  weeklyResetsIn?: string;
13
53
  sessionResetsAt?: string;
14
54
  weeklyResetsAt?: string;
15
55
  extraSpend?: number;
16
56
  extraLimit?: number;
57
+ sessionLabel?: string;
58
+ weeklyLabel?: string;
59
+ sessionHidden?: boolean;
60
+ weeklyHidden?: boolean;
61
+ notice?: string;
17
62
  warning?: string;
18
63
  stale?: boolean;
19
64
  fetchedAt?: number;
@@ -26,6 +71,16 @@ export type UsageTokens = Partial<Record<ProviderKey, string>>;
26
71
  export interface UsageEndpoints {
27
72
  zai: string;
28
73
  zaiCn: string;
74
+ kimi: string;
75
+ minimax: string;
76
+ minimaxLegacy: string;
77
+ minimaxCn: string;
78
+ minimaxCnLegacy: string;
79
+ openRouterCredits: string;
80
+ openRouterKey: string;
81
+ deepSeekBalance: string;
82
+ moonshotBalance: string;
83
+ moonshotCnBalance: string;
29
84
  }
30
85
 
31
86
  export interface HeadersLike {
@@ -108,6 +163,16 @@ const CLAUDE_LOCK_STALE_MS = 20_000;
108
163
  export const DEFAULT_USAGE_CACHE_FILE = path.join(os.tmpdir(), "pi", "usage-bars-cache.json");
109
164
  export const DEFAULT_ZAI_USAGE_ENDPOINT = "https://api.z.ai/api/monitor/usage/quota/limit";
110
165
  export const DEFAULT_ZAI_CN_USAGE_ENDPOINT = "https://open.bigmodel.cn/api/monitor/usage/quota/limit";
166
+ export const DEFAULT_KIMI_USAGE_ENDPOINT = "https://api.kimi.com/coding/v1/usages";
167
+ export const DEFAULT_MINIMAX_USAGE_ENDPOINT = "https://api.minimax.io/v1/token_plan/remains";
168
+ export const DEFAULT_MINIMAX_LEGACY_USAGE_ENDPOINT = "https://api.minimax.io/v1/api/openplatform/coding_plan/remains";
169
+ export const DEFAULT_MINIMAX_CN_USAGE_ENDPOINT = "https://api.minimaxi.com/v1/token_plan/remains";
170
+ export const DEFAULT_MINIMAX_CN_LEGACY_USAGE_ENDPOINT = "https://api.minimaxi.com/v1/api/openplatform/coding_plan/remains";
171
+ export const DEFAULT_OPENROUTER_CREDITS_ENDPOINT = "https://openrouter.ai/api/v1/credits";
172
+ export const DEFAULT_OPENROUTER_KEY_ENDPOINT = "https://openrouter.ai/api/v1/key";
173
+ export const DEFAULT_DEEPSEEK_BALANCE_ENDPOINT = "https://api.deepseek.com/user/balance";
174
+ export const DEFAULT_MOONSHOT_BALANCE_ENDPOINT = "https://api.moonshot.ai/v1/users/me/balance";
175
+ export const DEFAULT_MOONSHOT_CN_BALANCE_ENDPOINT = "https://api.moonshot.cn/v1/users/me/balance";
111
176
 
112
177
  export function resolveUsageEndpoints(env: NodeJS.ProcessEnv = process.env): UsageEndpoints {
113
178
  const configured = (value: string | undefined, fallback: string) => {
@@ -118,6 +183,16 @@ export function resolveUsageEndpoints(env: NodeJS.ProcessEnv = process.env): Usa
118
183
  return {
119
184
  zai: configured(env.PI_ZAI_USAGE_ENDPOINT, DEFAULT_ZAI_USAGE_ENDPOINT),
120
185
  zaiCn: configured(env.PI_ZAI_CODING_CN_USAGE_ENDPOINT, DEFAULT_ZAI_CN_USAGE_ENDPOINT),
186
+ kimi: configured(env.PI_KIMI_USAGE_ENDPOINT, DEFAULT_KIMI_USAGE_ENDPOINT),
187
+ minimax: configured(env.PI_MINIMAX_USAGE_ENDPOINT, DEFAULT_MINIMAX_USAGE_ENDPOINT),
188
+ minimaxLegacy: configured(env.PI_MINIMAX_LEGACY_USAGE_ENDPOINT, DEFAULT_MINIMAX_LEGACY_USAGE_ENDPOINT),
189
+ minimaxCn: configured(env.PI_MINIMAX_CN_USAGE_ENDPOINT, DEFAULT_MINIMAX_CN_USAGE_ENDPOINT),
190
+ minimaxCnLegacy: configured(env.PI_MINIMAX_CN_LEGACY_USAGE_ENDPOINT, DEFAULT_MINIMAX_CN_LEGACY_USAGE_ENDPOINT),
191
+ openRouterCredits: configured(env.PI_OPENROUTER_CREDITS_ENDPOINT, DEFAULT_OPENROUTER_CREDITS_ENDPOINT),
192
+ openRouterKey: configured(env.PI_OPENROUTER_KEY_ENDPOINT, DEFAULT_OPENROUTER_KEY_ENDPOINT),
193
+ deepSeekBalance: configured(env.PI_DEEPSEEK_BALANCE_ENDPOINT, DEFAULT_DEEPSEEK_BALANCE_ENDPOINT),
194
+ moonshotBalance: configured(env.PI_MOONSHOT_BALANCE_ENDPOINT, DEFAULT_MOONSHOT_BALANCE_ENDPOINT),
195
+ moonshotCnBalance: configured(env.PI_MOONSHOT_CN_BALANCE_ENDPOINT, DEFAULT_MOONSHOT_CN_BALANCE_ENDPOINT),
121
196
  };
122
197
  }
123
198
 
@@ -384,12 +459,21 @@ function snapshotUsage(usage: UsageData, nowMs = Date.now()): UsageData {
384
459
  return {
385
460
  session: usage.session,
386
461
  weekly: usage.weekly,
462
+ quotaHidden: usage.quotaHidden,
463
+ accountBalance: usage.accountBalance,
464
+ accountBalanceDetails: usage.accountBalanceDetails,
465
+ accountSpend: usage.accountSpend,
387
466
  sessionResetsAt: usage.sessionResetsAt,
388
467
  weeklyResetsAt: usage.weeklyResetsAt,
389
468
  sessionResetsIn: usage.sessionResetsIn,
390
469
  weeklyResetsIn: usage.weeklyResetsIn,
391
470
  extraSpend: usage.extraSpend,
392
471
  extraLimit: usage.extraLimit,
472
+ sessionLabel: usage.sessionLabel,
473
+ weeklyLabel: usage.weeklyLabel,
474
+ sessionHidden: usage.sessionHidden,
475
+ weeklyHidden: usage.weeklyHidden,
476
+ notice: usage.notice,
393
477
  fetchedAt: usage.fetchedAt ?? nowMs,
394
478
  };
395
479
  }
@@ -438,6 +522,41 @@ function readClaudeCacheOutcome(cacheFile = DEFAULT_USAGE_CACHE_FILE, nowMs = Da
438
522
  return null;
439
523
  }
440
524
 
525
+ export function parseCodexRateLimit(data: any): UsageData {
526
+ const rateLimit = data?.rate_limit ?? data?.rate_limits;
527
+ const primary = rateLimit?.primary_window ?? rateLimit?.primary ?? rateLimit?.five_hour;
528
+ const secondary = rateLimit?.secondary_window ?? rateLimit?.secondary ?? rateLimit?.weekly;
529
+
530
+ let sessionWindow: any = null;
531
+ let weeklyWindow: any = null;
532
+ for (const [position, window] of [["primary", primary], ["secondary", secondary]] as const) {
533
+ if (!window || typeof window !== "object") continue;
534
+ const duration = window.limit_window_seconds;
535
+ if (typeof duration === "number" && Number.isFinite(duration)) {
536
+ // Some Codex accounts return their seven-day quota as primary_window
537
+ // and omit secondary_window, so position alone does not identify it.
538
+ if (duration >= 2 * 24 * 60 * 60) weeklyWindow ??= window;
539
+ else sessionWindow ??= window;
540
+ } else if (position === "primary") {
541
+ sessionWindow ??= window;
542
+ } else {
543
+ weeklyWindow ??= window;
544
+ }
545
+ }
546
+
547
+ const reset = (window: any) =>
548
+ typeof window?.reset_after_seconds === "number" ? formatDuration(window.reset_after_seconds) : undefined;
549
+
550
+ return {
551
+ session: readPercentCandidate(sessionWindow?.used_percent) ?? 0,
552
+ weekly: readPercentCandidate(weeklyWindow?.used_percent) ?? 0,
553
+ ...(!sessionWindow ? { sessionHidden: true } : {}),
554
+ ...(!weeklyWindow ? { weeklyHidden: true } : {}),
555
+ sessionResetsIn: reset(sessionWindow),
556
+ weeklyResetsIn: reset(weeklyWindow),
557
+ };
558
+ }
559
+
441
560
  export async function fetchCodexUsage(token: string, config: RequestConfig = {}): Promise<UsageData> {
442
561
  const result = await requestJson(
443
562
  "https://chatgpt.com/backend-api/wham/usage",
@@ -445,16 +564,7 @@ export async function fetchCodexUsage(token: string, config: RequestConfig = {})
445
564
  config,
446
565
  );
447
566
  if (!result.ok) return { session: 0, weekly: 0, error: result.error };
448
-
449
- const data = result.data as any;
450
- const primary = data?.rate_limit?.primary_window;
451
- const secondary = data?.rate_limit?.secondary_window;
452
- return {
453
- session: readPercentCandidate(primary?.used_percent) ?? 0,
454
- weekly: readPercentCandidate(secondary?.used_percent) ?? 0,
455
- sessionResetsIn: typeof primary?.reset_after_seconds === "number" ? formatDuration(primary.reset_after_seconds) : undefined,
456
- weeklyResetsIn: typeof secondary?.reset_after_seconds === "number" ? formatDuration(secondary.reset_after_seconds) : undefined,
457
- };
567
+ return parseCodexRateLimit(result.data);
458
568
  }
459
569
 
460
570
  async function fetchClaudeUsageAttempt(
@@ -545,6 +655,463 @@ export async function fetchClaudeUsageWithFallback(
545
655
  }
546
656
  }
547
657
 
658
+ function readNumber(value: unknown): number | null {
659
+ if (typeof value === "number" && Number.isFinite(value)) return value;
660
+ if (typeof value === "string" && value.trim()) {
661
+ const parsed = Number(value);
662
+ if (Number.isFinite(parsed)) return parsed;
663
+ }
664
+ return null;
665
+ }
666
+
667
+ function usedPercentFromCounts(
668
+ value: Record<string, unknown> | null | undefined,
669
+ options: { remainingPercent?: string; used?: string; total?: string; remaining?: string } = {},
670
+ ): number | null {
671
+ if (!value) return null;
672
+ const remainingPercent = readNumber(value[options.remainingPercent ?? "remaining_percent"]);
673
+ if (remainingPercent !== null) return Math.max(0, Math.min(100, 100 - remainingPercent));
674
+
675
+ const total = readNumber(value[options.total ?? "limit"]);
676
+ const used = readNumber(value[options.used ?? "used"]);
677
+ const remaining = readNumber(value[options.remaining ?? "remaining"]);
678
+ if (total === null || total <= 0) return null;
679
+ if (used !== null) return Math.max(0, Math.min(100, used / total * 100));
680
+ if (remaining !== null) return Math.max(0, Math.min(100, (total - remaining) / total * 100));
681
+ return null;
682
+ }
683
+
684
+ function normalizeIsoDate(value: unknown): string | undefined {
685
+ if (typeof value !== "string" || !value.trim()) return undefined;
686
+ const normalized = value.trim().replace(/(\.\d{3})\d+(?=Z|[+-]\d\d:\d\d$)/, "$1");
687
+ return Number.isFinite(new Date(normalized).getTime()) ? normalized : undefined;
688
+ }
689
+
690
+ function isoFromEpoch(value: unknown): string | undefined {
691
+ const raw = readNumber(value);
692
+ if (raw === null || raw <= 0) return undefined;
693
+ const milliseconds = raw > 1_000_000_000_000 ? raw : raw * 1000;
694
+ const date = new Date(milliseconds);
695
+ return Number.isFinite(date.getTime()) ? date.toISOString() : undefined;
696
+ }
697
+
698
+ function resetFromRemains(value: unknown, nowMs: number): string | undefined {
699
+ const raw = readNumber(value);
700
+ if (raw === null || raw <= 0) return undefined;
701
+ const milliseconds = raw > 1_000_000 ? raw : raw * 1000;
702
+ return new Date(nowMs + milliseconds).toISOString();
703
+ }
704
+
705
+ export function extractKimiUsageFromPayload(payload: unknown, nowMs = Date.now()): UsageData | null {
706
+ const root = asObject(payload);
707
+ if (!root) return null;
708
+ const webUsages = Array.isArray(root.usages) ? root.usages : undefined;
709
+ const codingUsage = webUsages?.map(asObject).find((entry) =>
710
+ String(entry?.scope ?? "").toUpperCase() === "FEATURE_CODING") ?? root;
711
+ const dataRows = Array.isArray(codingUsage.data) ? codingUsage.data.map(asObject).filter(Boolean) : [];
712
+ const usage = asObject(codingUsage.usage) ?? asObject(codingUsage.detail) ??
713
+ dataRows.find((entry) => String(entry?.model_name ?? entry?.modelName ?? "").toLowerCase() === "all");
714
+ const limits = Array.isArray(codingUsage.limits)
715
+ ? codingUsage.limits
716
+ : dataRows.filter((entry) => entry !== usage);
717
+ const sessionLimit = limits.map(asObject).find((entry) => {
718
+ const window = asObject(entry?.window);
719
+ const duration = readNumber(window?.duration);
720
+ const unit = String(window?.timeUnit ?? window?.time_unit ?? "").toUpperCase();
721
+ return duration === 300 && unit.includes("MINUTE");
722
+ }) ?? limits.map(asObject).find((entry) => entry !== null);
723
+ const sessionDetail = asObject(sessionLimit?.detail) ?? sessionLimit;
724
+
725
+ const session = usedPercentFromCounts(sessionDetail);
726
+ const weekly = usedPercentFromCounts(usage);
727
+ if (session === null || weekly === null) return null;
728
+
729
+ const sessionReset = normalizeIsoDate(sessionDetail?.resetTime ?? sessionDetail?.reset_at ?? sessionDetail?.reset_time);
730
+ const weeklyReset = normalizeIsoDate(usage?.resetTime ?? usage?.reset_at ?? usage?.reset_time);
731
+ return hydrateUsageResets({
732
+ ...normalizeUsagePair(session, weekly),
733
+ sessionLabel: "5-hour",
734
+ weeklyLabel: "Weekly",
735
+ sessionResetsAt: sessionReset,
736
+ weeklyResetsAt: weeklyReset,
737
+ }, nowMs);
738
+ }
739
+
740
+ export async function fetchKimiUsage(token: string, config: FetchConfig = {}): Promise<UsageData> {
741
+ const endpoints = config.endpoints ?? resolveUsageEndpoints(config.env);
742
+ const result = await requestJson(endpoints.kimi, {
743
+ headers: {
744
+ Authorization: `Bearer ${token}`,
745
+ "User-Agent": "KimiCLI/1.5",
746
+ },
747
+ }, config);
748
+ if (!result.ok) return { session: 0, weekly: 0, error: result.error };
749
+ return extractKimiUsageFromPayload(result.data) ?? {
750
+ session: 0,
751
+ weekly: 0,
752
+ error: "unrecognized response shape",
753
+ };
754
+ }
755
+
756
+ interface MiniMaxWindow {
757
+ percent: number;
758
+ resetsAt?: string;
759
+ }
760
+
761
+ function pickHighestWindow(windows: MiniMaxWindow[]): MiniMaxWindow | undefined {
762
+ return windows.reduce<MiniMaxWindow | undefined>((highest, window) =>
763
+ !highest || window.percent > highest.percent ? window : highest, undefined);
764
+ }
765
+
766
+ function miniMaxResetAt(value: Record<string, unknown>, prefix: "current" | "weekly", nowMs: number): string | undefined {
767
+ const end = prefix === "current"
768
+ ? value.end_time ?? value.endTime
769
+ : value.weekly_end_time ?? value.weeklyEndTime;
770
+ const remains = prefix === "current"
771
+ ? value.remains_time ?? value.remainsTime
772
+ : value.weekly_remains_time ?? value.weeklyRemainsTime;
773
+ const resetsAt = prefix === "current"
774
+ ? value.current_resets_at ?? value.currentResetsAt
775
+ : value.weekly_resets_at ?? value.weeklyResetsAt;
776
+ return normalizeIsoDate(resetsAt) ?? isoFromEpoch(end) ?? resetFromRemains(remains, nowMs);
777
+ }
778
+
779
+ function extractMiniMaxCreditBalance(payload: unknown): AccountBalance | undefined {
780
+ const root = asObject(payload);
781
+ const data = asObject(root?.data) ?? root;
782
+ if (!data) return undefined;
783
+ const amount = readNumber(
784
+ data.points_balance ?? data.pointsBalance ??
785
+ data.point_balance ?? data.pointBalance ??
786
+ data.credits_balance ?? data.creditsBalance ??
787
+ data.credit_balance ?? data.creditBalance,
788
+ );
789
+ return amount === null ? undefined : { amount, unit: "credits", label: "Credit balance" };
790
+ }
791
+
792
+ export function extractMiniMaxUsageFromPayload(payload: unknown, nowMs = Date.now()): UsageData | null {
793
+ const root = asObject(payload);
794
+ const data = asObject(root?.data) ?? root;
795
+ if (!data) return null;
796
+ const accountBalance = extractMiniMaxCreditBalance(payload);
797
+
798
+ const intervalWindows: MiniMaxWindow[] = [];
799
+ const weeklyWindows: MiniMaxWindow[] = [];
800
+ if (Array.isArray(data.services)) {
801
+ for (const rawService of data.services) {
802
+ const service = asObject(rawService);
803
+ if (!service) continue;
804
+ const directPercent = readPercentCandidate(readNumber(service.percent));
805
+ const percent = directPercent ?? usedPercentFromCounts(service, { total: "limit", used: "usage" });
806
+ if (percent === null) continue;
807
+ const windowType = String(service.window_type ?? service.windowType ?? "").toLowerCase();
808
+ const resetsAt = normalizeIsoDate(service.resets_at ?? service.reset_time ?? service.end_time);
809
+ (windowType.includes("week") ? weeklyWindows : intervalWindows).push({ percent, resetsAt });
810
+ }
811
+ }
812
+
813
+ if (Array.isArray(data.model_remains ?? data.modelRemains)) {
814
+ for (const rawModel of (data.model_remains ?? data.modelRemains) as unknown[]) {
815
+ const raw = asObject(rawModel);
816
+ if (!raw) continue;
817
+ const model: Record<string, unknown> = {
818
+ ...raw,
819
+ current_interval_remaining_percent:
820
+ raw.current_interval_remaining_percent ?? raw.currentIntervalRemainingPercent,
821
+ current_interval_total_count: raw.current_interval_total_count ?? raw.currentIntervalTotalCount,
822
+ current_interval_usage_count: raw.current_interval_usage_count ?? raw.currentIntervalUsageCount,
823
+ current_interval_status: raw.current_interval_status ?? raw.currentIntervalStatus,
824
+ current_weekly_remaining_percent:
825
+ raw.current_weekly_remaining_percent ?? raw.currentWeeklyRemainingPercent,
826
+ current_weekly_total_count: raw.current_weekly_total_count ?? raw.currentWeeklyTotalCount,
827
+ current_weekly_usage_count: raw.current_weekly_usage_count ?? raw.currentWeeklyUsageCount,
828
+ current_weekly_status: raw.current_weekly_status ?? raw.currentWeeklyStatus,
829
+ };
830
+ const unavailable = (prefix: "interval" | "weekly") =>
831
+ readNumber(model[`current_${prefix}_status`]) === 3 &&
832
+ (readNumber(model[`current_${prefix}_remaining_percent`]) ?? 0) >= 100 &&
833
+ (readNumber(model[`current_${prefix}_total_count`]) ?? 0) === 0 &&
834
+ (readNumber(model[`current_${prefix}_usage_count`]) ?? 0) === 0;
835
+ const interval = unavailable("interval") ? null : usedPercentFromCounts(model, {
836
+ remainingPercent: "current_interval_remaining_percent",
837
+ total: "current_interval_total_count",
838
+ remaining: "current_interval_usage_count",
839
+ });
840
+ if (interval !== null) {
841
+ intervalWindows.push({ percent: interval, resetsAt: miniMaxResetAt(model, "current", nowMs) });
842
+ }
843
+ const weekly = unavailable("weekly") ? null : usedPercentFromCounts(model, {
844
+ remainingPercent: "current_weekly_remaining_percent",
845
+ total: "current_weekly_total_count",
846
+ remaining: "current_weekly_usage_count",
847
+ });
848
+ if (weekly !== null) {
849
+ weeklyWindows.push({ percent: weekly, resetsAt: miniMaxResetAt(model, "weekly", nowMs) });
850
+ }
851
+ }
852
+ }
853
+
854
+ const session = pickHighestWindow(intervalWindows);
855
+ const weekly = pickHighestWindow(weeklyWindows);
856
+ if (!session) {
857
+ return accountBalance
858
+ ? { session: 0, weekly: 0, quotaHidden: true, accountBalance }
859
+ : null;
860
+ }
861
+ return hydrateUsageResets({
862
+ session: Number(session.percent.toFixed(2)),
863
+ accountBalance,
864
+ weekly: Number((weekly?.percent ?? 0).toFixed(2)),
865
+ sessionLabel: "Interval",
866
+ weeklyLabel: "Weekly",
867
+ weeklyHidden: !weekly,
868
+ sessionResetsAt: session.resetsAt,
869
+ weeklyResetsAt: weekly?.resetsAt,
870
+ }, nowMs);
871
+ }
872
+
873
+ function miniMaxPayloadStatus(payload: unknown): number | null {
874
+ const root = asObject(payload);
875
+ const data = asObject(root?.data);
876
+ const baseResponse = asObject(data?.base_resp ?? data?.baseResp ?? root?.base_resp ?? root?.baseResp);
877
+ return readNumber(baseResponse?.status_code ?? baseResponse?.statusCode);
878
+ }
879
+
880
+ function miniMaxPayloadError(payload: unknown): string | null {
881
+ const root = asObject(payload);
882
+ const data = asObject(root?.data);
883
+ const baseResponse = asObject(data?.base_resp ?? data?.baseResp ?? root?.base_resp ?? root?.baseResp);
884
+ const status = miniMaxPayloadStatus(payload);
885
+ if (status === null || status === 0) return null;
886
+ const message = baseResponse?.status_msg ?? baseResponse?.statusMessage;
887
+ return typeof message === "string" && message.trim()
888
+ ? `API ${status}: ${message.trim()}`
889
+ : `API ${status}`;
890
+ }
891
+
892
+ export async function fetchMiniMaxUsage(
893
+ token: string,
894
+ provider: "minimax" | "minimax-cn" = "minimax",
895
+ config: FetchConfig = {},
896
+ ): Promise<UsageData> {
897
+ const endpoints = config.endpoints ?? resolveUsageEndpoints(config.env);
898
+ const candidates = provider === "minimax-cn"
899
+ ? [endpoints.minimaxCn, endpoints.minimaxCnLegacy]
900
+ : [endpoints.minimax, endpoints.minimaxLegacy];
901
+ let lastError = "usage request failed";
902
+ let credentialError: string | undefined;
903
+ let noActiveTokenPlan = false;
904
+
905
+ for (const endpoint of [...new Set(candidates)]) {
906
+ const result = await requestJson(endpoint, {
907
+ headers: {
908
+ Authorization: `Bearer ${token}`,
909
+ Accept: "application/json",
910
+ },
911
+ }, config);
912
+ if (!result.ok) {
913
+ lastError = result.error;
914
+ if (result.status === 401 || result.status === 403) credentialError ??= result.error;
915
+ if (config.signal?.aborted) break;
916
+ continue;
917
+ }
918
+ const payloadStatus = miniMaxPayloadStatus(result.data);
919
+ const payloadError = miniMaxPayloadError(result.data);
920
+ const usage = extractMiniMaxUsageFromPayload(result.data);
921
+ if (usage && (!payloadError || usage.quotaHidden)) return usage;
922
+ if (payloadStatus === 2062) {
923
+ noActiveTokenPlan = true;
924
+ continue;
925
+ }
926
+ if (payloadError) {
927
+ lastError = payloadError;
928
+ continue;
929
+ }
930
+ lastError = "unrecognized response shape";
931
+ }
932
+
933
+ if (noActiveTokenPlan) {
934
+ return {
935
+ session: 0,
936
+ weekly: 0,
937
+ quotaHidden: true,
938
+ notice: "No active Token Plan · check Credit balance in the MiniMax console",
939
+ };
940
+ }
941
+ return { session: 0, weekly: 0, error: credentialError ?? lastError };
942
+ }
943
+
944
+ export function extractOpenRouterUsageFromPayloads(
945
+ creditsPayload: unknown,
946
+ keyPayload: unknown,
947
+ ): UsageData | null {
948
+ const credits = asObject(asObject(creditsPayload)?.data) ?? asObject(creditsPayload);
949
+ const key = asObject(asObject(keyPayload)?.data) ?? asObject(keyPayload);
950
+
951
+ const totalCredits = readNumber(credits?.total_credits ?? credits?.totalCredits);
952
+ const totalUsage = readNumber(credits?.total_usage ?? credits?.totalUsage);
953
+ const accountBalance = totalCredits !== null && totalUsage !== null
954
+ ? {
955
+ amount: Number((totalCredits - totalUsage).toFixed(6)),
956
+ unit: "USD",
957
+ label: "Balance",
958
+ }
959
+ : undefined;
960
+
961
+ const spendValues = {
962
+ daily: readNumber(key?.usage_daily ?? key?.usageDaily),
963
+ weekly: readNumber(key?.usage_weekly ?? key?.usageWeekly),
964
+ monthly: readNumber(key?.usage_monthly ?? key?.usageMonthly),
965
+ lifetime: readNumber(key?.usage),
966
+ };
967
+ const accountSpend = Object.values(spendValues).some((value) => value !== null)
968
+ ? {
969
+ unit: "USD",
970
+ daily: spendValues.daily ?? undefined,
971
+ weekly: spendValues.weekly ?? undefined,
972
+ monthly: spendValues.monthly ?? undefined,
973
+ lifetime: spendValues.lifetime ?? undefined,
974
+ }
975
+ : undefined;
976
+
977
+ const limit = readNumber(key?.limit);
978
+ const remaining = readNumber(key?.limit_remaining ?? key?.limitRemaining);
979
+ const limitUsed = limit !== null && limit > 0 && remaining !== null
980
+ ? Math.max(0, Math.min(limit, limit - remaining))
981
+ : null;
982
+ const limitPercent = limitUsed !== null && limit !== null ? limitUsed / limit * 100 : null;
983
+ if (!accountBalance && !accountSpend && limitPercent === null) return null;
984
+
985
+ return {
986
+ session: limitPercent === null ? 0 : Number(limitPercent.toFixed(2)),
987
+ weekly: 0,
988
+ quotaHidden: limitPercent === null,
989
+ weeklyHidden: true,
990
+ sessionLabel: "Key limit",
991
+ accountBalance,
992
+ accountSpend,
993
+ };
994
+ }
995
+
996
+ export async function fetchOpenRouterUsage(token: string, config: FetchConfig = {}): Promise<UsageData> {
997
+ const endpoints = config.endpoints ?? resolveUsageEndpoints(config.env);
998
+ const headers = { Authorization: `Bearer ${token}`, Accept: "application/json" };
999
+ const [creditsResult, keyResult] = await Promise.all([
1000
+ requestJson(endpoints.openRouterCredits, { headers }, config),
1001
+ requestJson(endpoints.openRouterKey, { headers }, config),
1002
+ ]);
1003
+ const usage = extractOpenRouterUsageFromPayloads(
1004
+ creditsResult.ok ? creditsResult.data : undefined,
1005
+ keyResult.ok ? keyResult.data : undefined,
1006
+ );
1007
+ if (usage) return usage;
1008
+
1009
+ const errors = [
1010
+ creditsResult.ok ? undefined : `credits: ${creditsResult.error}`,
1011
+ keyResult.ok ? undefined : `key: ${keyResult.error}`,
1012
+ ].filter((value): value is string => Boolean(value));
1013
+ return {
1014
+ session: 0,
1015
+ weekly: 0,
1016
+ error: errors.length > 0 ? errors.join("; ") : "unrecognized response shape",
1017
+ };
1018
+ }
1019
+
1020
+ export function extractDeepSeekBalanceFromPayload(payload: unknown): UsageData | null {
1021
+ const root = asObject(payload);
1022
+ const rawBalances = Array.isArray(root?.balance_infos) ? root.balance_infos : [];
1023
+ const balances = rawBalances.map(asObject).filter((value): value is Record<string, unknown> => value !== null);
1024
+ if (balances.length === 0) return null;
1025
+
1026
+ const parsed = balances.flatMap((balance) => {
1027
+ const unit = typeof balance.currency === "string" ? balance.currency.toUpperCase() : "USD";
1028
+ const total = readNumber(balance.total_balance ?? balance.totalBalance);
1029
+ if (total === null) return [];
1030
+ return [{
1031
+ total: { amount: total, unit, label: "Total balance" } satisfies AccountBalance,
1032
+ toppedUp: readNumber(balance.topped_up_balance ?? balance.toppedUpBalance),
1033
+ granted: readNumber(balance.granted_balance ?? balance.grantedBalance),
1034
+ }];
1035
+ });
1036
+ const primary = parsed[0];
1037
+ if (!primary) return null;
1038
+
1039
+ const details: AccountBalance[] = [];
1040
+ if (primary.toppedUp !== null) {
1041
+ details.push({ amount: primary.toppedUp, unit: primary.total.unit, label: "Topped up" });
1042
+ }
1043
+ if (primary.granted !== null) {
1044
+ details.push({ amount: primary.granted, unit: primary.total.unit, label: "Granted" });
1045
+ }
1046
+ for (const additional of parsed.slice(1)) details.push(additional.total);
1047
+
1048
+ return {
1049
+ session: 0,
1050
+ weekly: 0,
1051
+ quotaHidden: true,
1052
+ accountBalance: primary.total,
1053
+ accountBalanceDetails: details,
1054
+ warning: root?.is_available === false ? "Balance is not currently available for API use" : undefined,
1055
+ };
1056
+ }
1057
+
1058
+ export async function fetchDeepSeekBalance(token: string, config: FetchConfig = {}): Promise<UsageData> {
1059
+ const endpoints = config.endpoints ?? resolveUsageEndpoints(config.env);
1060
+ const result = await requestJson(endpoints.deepSeekBalance, {
1061
+ headers: { Authorization: `Bearer ${token}`, Accept: "application/json" },
1062
+ }, config);
1063
+ if (!result.ok) return { session: 0, weekly: 0, error: result.error };
1064
+ return extractDeepSeekBalanceFromPayload(result.data) ?? {
1065
+ session: 0,
1066
+ weekly: 0,
1067
+ error: "unrecognized response shape",
1068
+ };
1069
+ }
1070
+
1071
+ export function extractMoonshotBalanceFromPayload(
1072
+ payload: unknown,
1073
+ provider: "moonshot" | "moonshot-cn" = "moonshot",
1074
+ ): UsageData | null {
1075
+ const root = asObject(payload);
1076
+ const data = asObject(root?.data) ?? root;
1077
+ if (!data) return null;
1078
+ const available = readNumber(data.available_balance ?? data.availableBalance);
1079
+ if (available === null) return null;
1080
+ const cash = readNumber(data.cash_balance ?? data.cashBalance);
1081
+ const voucher = readNumber(data.voucher_balance ?? data.voucherBalance);
1082
+ const unit = provider === "moonshot-cn" ? "CNY" : "USD";
1083
+ const details: AccountBalance[] = [];
1084
+ if (cash !== null) details.push({ amount: cash, unit, label: "Cash" });
1085
+ if (voucher !== null) details.push({ amount: voucher, unit, label: "Voucher" });
1086
+
1087
+ return {
1088
+ session: 0,
1089
+ weekly: 0,
1090
+ quotaHidden: true,
1091
+ accountBalance: { amount: available, unit, label: "Available balance" },
1092
+ accountBalanceDetails: details,
1093
+ warning: available <= 0 ? "Balance exhausted; inference requests may be rejected" : undefined,
1094
+ };
1095
+ }
1096
+
1097
+ export async function fetchMoonshotBalance(
1098
+ token: string,
1099
+ provider: "moonshot" | "moonshot-cn" = "moonshot",
1100
+ config: FetchConfig = {},
1101
+ ): Promise<UsageData> {
1102
+ const endpoints = config.endpoints ?? resolveUsageEndpoints(config.env);
1103
+ const endpoint = provider === "moonshot-cn" ? endpoints.moonshotCnBalance : endpoints.moonshotBalance;
1104
+ const result = await requestJson(endpoint, {
1105
+ headers: { Authorization: `Bearer ${token}`, Accept: "application/json" },
1106
+ }, config);
1107
+ if (!result.ok) return { session: 0, weekly: 0, error: result.error };
1108
+ return extractMoonshotBalanceFromPayload(result.data, provider) ?? {
1109
+ session: 0,
1110
+ weekly: 0,
1111
+ error: "unrecognized response shape",
1112
+ };
1113
+ }
1114
+
548
1115
  /** Parse ZAI limits where unit 3 is the five-hour window and unit 6 the weekly window. */
549
1116
  export function extractZaiUsageFromPayload(payload: unknown, nowMs = Date.now()): UsageData | null {
550
1117
  const data = payload as any;
@@ -597,6 +1164,13 @@ export function detectProvider(
597
1164
  case "anthropic": return "claude";
598
1165
  case "zai": return "zai";
599
1166
  case "zai-coding-cn": return "zai-cn";
1167
+ case "kimi-coding": return "kimi";
1168
+ case "minimax": return "minimax";
1169
+ case "minimax-cn": return "minimax-cn";
1170
+ case "openrouter": return "openrouter";
1171
+ case "deepseek": return "deepseek";
1172
+ case "moonshotai": return "moonshot";
1173
+ case "moonshotai-cn": return "moonshot-cn";
600
1174
  default: return null;
601
1175
  }
602
1176
  }
@@ -607,6 +1181,13 @@ export function providerToPiProviderId(provider: ProviderKey): PiProviderId {
607
1181
  case "claude": return "anthropic";
608
1182
  case "zai": return "zai";
609
1183
  case "zai-cn": return "zai-coding-cn";
1184
+ case "kimi": return "kimi-coding";
1185
+ case "minimax": return "minimax";
1186
+ case "minimax-cn": return "minimax-cn";
1187
+ case "openrouter": return "openrouter";
1188
+ case "deepseek": return "deepseek";
1189
+ case "moonshot": return "moonshotai";
1190
+ case "moonshot-cn": return "moonshotai-cn";
610
1191
  }
611
1192
  }
612
1193
 
@@ -626,7 +1207,19 @@ export async function fetchAllUsages(
626
1207
  config: FetchAllUsagesConfig = {},
627
1208
  ): Promise<UsageByProvider> {
628
1209
  const endpoints = config.endpoints ?? resolveUsageEndpoints(config.env);
629
- const results: UsageByProvider = { codex: null, claude: null, zai: null, "zai-cn": null };
1210
+ const results: UsageByProvider = {
1211
+ codex: null,
1212
+ claude: null,
1213
+ zai: null,
1214
+ "zai-cn": null,
1215
+ kimi: null,
1216
+ minimax: null,
1217
+ "minimax-cn": null,
1218
+ openrouter: null,
1219
+ deepseek: null,
1220
+ moonshot: null,
1221
+ "moonshot-cn": null,
1222
+ };
630
1223
  const tasks: Promise<void>[] = [];
631
1224
 
632
1225
  const assign = (provider: ProviderKey, request: Promise<UsageData>) => {
@@ -647,7 +1240,29 @@ export async function fetchAllUsages(
647
1240
  }
648
1241
  if (tokens.zai) assign("zai", fetchZaiUsage(tokens.zai, "zai", { ...config, endpoints }));
649
1242
  if (tokens["zai-cn"]) assign("zai-cn", fetchZaiUsage(tokens["zai-cn"], "zai-cn", { ...config, endpoints }));
1243
+ if (tokens.kimi) assign("kimi", fetchKimiUsage(tokens.kimi, { ...config, endpoints }));
1244
+ if (tokens.minimax) assign("minimax", fetchMiniMaxUsage(tokens.minimax, "minimax", { ...config, endpoints }));
1245
+ if (tokens["minimax-cn"]) {
1246
+ assign("minimax-cn", fetchMiniMaxUsage(tokens["minimax-cn"], "minimax-cn", { ...config, endpoints }));
1247
+ }
1248
+ if (tokens.openrouter) assign("openrouter", fetchOpenRouterUsage(tokens.openrouter, { ...config, endpoints }));
1249
+ if (tokens.deepseek) assign("deepseek", fetchDeepSeekBalance(tokens.deepseek, { ...config, endpoints }));
1250
+ if (tokens.moonshot) assign("moonshot", fetchMoonshotBalance(tokens.moonshot, "moonshot", { ...config, endpoints }));
1251
+ if (tokens["moonshot-cn"]) {
1252
+ assign("moonshot-cn", fetchMoonshotBalance(tokens["moonshot-cn"], "moonshot-cn", { ...config, endpoints }));
1253
+ }
650
1254
 
651
1255
  await Promise.all(tasks);
1256
+
1257
+ // Pi intentionally uses MOONSHOT_API_KEY for both regional providers. When one
1258
+ // key works in only one region, hide the expected regional auth failure from
1259
+ // the all-provider view while preserving active-provider polling behavior.
1260
+ if (tokens.moonshot && tokens.moonshot === tokens["moonshot-cn"]) {
1261
+ if (results.moonshot && !results.moonshot.error && results["moonshot-cn"]?.error) {
1262
+ results["moonshot-cn"] = null;
1263
+ } else if (results["moonshot-cn"] && !results["moonshot-cn"].error && results.moonshot?.error) {
1264
+ results.moonshot = null;
1265
+ }
1266
+ }
652
1267
  return results;
653
1268
  }