pi-myusage 0.1.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.
@@ -0,0 +1,52 @@
1
+ import { fetchJson, HttpError, safeError, urlOnDomain } from "../http.ts";
2
+ import { snapshot, type UsageAdapter, type UsageSnapshot } from "../types.ts";
3
+
4
+ const MODELS_URL = "https://generativelanguage.googleapis.com/v1beta/models";
5
+
6
+ interface ModelsResponse {
7
+ models?: unknown[];
8
+ }
9
+
10
+ export const geminiAdapter: UsageAdapter = {
11
+ id: "gemini",
12
+ label: "Gemini API",
13
+ canHandle(target) {
14
+ if (["gemini", "google-ai-studio", "aistudio"].includes(target.providerId.toLowerCase())) return true;
15
+ return urlOnDomain(target.baseUrl, "generativelanguage.googleapis.com");
16
+ },
17
+ async fetch({ target, signal }): Promise<UsageSnapshot> {
18
+ try {
19
+ const apiKey = target.credentials?.apiKey;
20
+ if (!apiKey) return snapshot(this, target, "unauthorized", { error: "No API key" });
21
+
22
+ let payload: ModelsResponse;
23
+ try {
24
+ payload = await fetchJson<ModelsResponse>(
25
+ MODELS_URL,
26
+ { headers: { "x-goog-api-key": apiKey, Accept: "application/json" } },
27
+ signal,
28
+ );
29
+ } catch (error) {
30
+ if (error instanceof HttpError && (error.status === 401 || error.status === 403)) {
31
+ return snapshot(this, target, "unauthorized", { error: safeError(error) });
32
+ }
33
+ throw error;
34
+ }
35
+
36
+ const count = Array.isArray(payload.models) ? payload.models.length : 0;
37
+ return snapshot(this, target, "ok", {
38
+ accounts: [
39
+ {
40
+ id: "gemini-key",
41
+ label: "Gemini API",
42
+ metrics: [{ kind: "status", id: "gemini-key", label: "Key", value: `valid · ${count} models` }],
43
+ },
44
+ ],
45
+ summary: `Gemini key ok · ${count} models`,
46
+ });
47
+ } catch (error) {
48
+ if (signal.aborted) return snapshot(this, target, "unavailable", { error: "aborted" });
49
+ return snapshot(this, target, "unavailable", { error: safeError(error) });
50
+ }
51
+ },
52
+ };
@@ -0,0 +1,137 @@
1
+ import { shortReset } from "../format.ts";
2
+ import { fetchJson, HttpError, safeError } from "../http.ts";
3
+ import { snapshot, type Metric, type UsageAdapter, type UsageSnapshot } from "../types.ts";
4
+
5
+ const USER_URL = "https://api.github.com/copilot_internal/user";
6
+
7
+ interface CopilotUserPayload {
8
+ login?: string;
9
+ copilot_plan?: string;
10
+ access_type_sku?: string;
11
+ features?: string[];
12
+ quota_reset_date_utc?: string;
13
+ quota_reset_date?: string;
14
+ limited_user_reset_date?: string;
15
+ quota_snapshots?: {
16
+ premium_interactions?: {
17
+ unlimited?: boolean;
18
+ token_based_billing?: boolean;
19
+ entitlement?: number;
20
+ remaining?: number;
21
+ quota_remaining?: number;
22
+ credits_used?: number;
23
+ overage_count?: number;
24
+ } | null;
25
+ } | null;
26
+ limited_user_quotas?: { chat?: number } | null;
27
+ monthly_quotas?: { chat?: number } | null;
28
+ }
29
+
30
+ function resetIso(payload: CopilotUserPayload): string | undefined {
31
+ const raw = payload.quota_reset_date_utc ?? payload.quota_reset_date ?? payload.limited_user_reset_date;
32
+ if (!raw) return undefined;
33
+ const ms = Date.parse(raw);
34
+ return Number.isFinite(ms) ? new Date(ms).toISOString() : undefined;
35
+ }
36
+
37
+ function parseMetrics(payload: CopilotUserPayload): Metric[] {
38
+ const metrics: Metric[] = [];
39
+ const reset = resetIso(payload);
40
+ const resetDetail = reset ? `resets ${shortReset(reset)}` : undefined;
41
+
42
+ const premium = payload.quota_snapshots?.premium_interactions;
43
+ if (premium) {
44
+ if (premium.unlimited === true) {
45
+ metrics.push({ kind: "status", id: "copilot-premium", label: "Premium requests", value: "Unlimited" });
46
+ } else {
47
+ const entitlement = premium.entitlement;
48
+ const remaining = premium.remaining ?? premium.quota_remaining;
49
+ if (typeof entitlement === "number" && typeof remaining === "number") {
50
+ const overage = Math.max(premium.overage_count ?? 0, Math.max(0, -remaining));
51
+ const used = premium.credits_used ?? Math.max(0, entitlement - remaining);
52
+ metrics.push({
53
+ kind: "usage-limit",
54
+ id: premium.token_based_billing === true ? "copilot-credits" : "copilot-premium",
55
+ label: premium.token_based_billing === true ? "AI credits" : "Premium requests",
56
+ used,
57
+ limit: entitlement,
58
+ unit: "requests",
59
+ ...(resetDetail ? { detail: resetDetail } : {}),
60
+ });
61
+ if (overage > 0) {
62
+ metrics.push({
63
+ kind: "status",
64
+ id: "copilot-overage",
65
+ label: "Additional usage",
66
+ value: `${overage}`,
67
+ });
68
+ }
69
+ }
70
+ }
71
+ } else {
72
+ const remaining = payload.limited_user_quotas?.chat;
73
+ const entitlement = payload.monthly_quotas?.chat;
74
+ if (typeof remaining === "number" && typeof entitlement === "number") {
75
+ metrics.push({
76
+ kind: "usage-limit",
77
+ id: "copilot-chat",
78
+ label: "Chat requests",
79
+ used: Math.max(0, entitlement - remaining),
80
+ limit: entitlement,
81
+ unit: "requests",
82
+ ...(resetDetail ? { detail: resetDetail } : {}),
83
+ });
84
+ }
85
+ }
86
+
87
+ const plan = payload.copilot_plan ?? payload.access_type_sku;
88
+ if (plan) metrics.push({ kind: "status", id: "copilot-plan", label: "Plan", value: plan });
89
+
90
+ if (payload.features?.length) {
91
+ const trimmed = payload.features.slice(0, 4).join(", ");
92
+ metrics.push({
93
+ kind: "status",
94
+ id: "copilot-features",
95
+ label: "Features",
96
+ value: trimmed + (payload.features.length > 4 ? ", …" : ""),
97
+ });
98
+ }
99
+
100
+ return metrics;
101
+ }
102
+
103
+ export const githubCopilotAdapter: UsageAdapter = {
104
+ id: "github-copilot",
105
+ label: "GitHub Copilot",
106
+ canHandle(target) {
107
+ const pid = target.providerId.toLowerCase();
108
+ return pid === "github-copilot" || pid === "copilot";
109
+ },
110
+ async fetch({ target, signal }): Promise<UsageSnapshot> {
111
+ try {
112
+ const apiKey = target.credentials?.apiKey;
113
+ if (!apiKey) return snapshot(this, target, "unauthorized", { error: "No API key" });
114
+
115
+ const payload = await fetchJson<CopilotUserPayload>(
116
+ USER_URL,
117
+ { headers: { Authorization: `Bearer ${apiKey}`, Accept: "application/vnd.github+json" } },
118
+ signal,
119
+ );
120
+
121
+ const metrics = parseMetrics(payload);
122
+ const plan = payload.copilot_plan ?? payload.access_type_sku;
123
+ if (!metrics.length || !plan) return snapshot(this, target, "empty");
124
+
125
+ return snapshot(this, target, "ok", {
126
+ accounts: [{ id: payload.login ?? "github-copilot", label: payload.login ?? "Copilot", metrics }],
127
+ summary: `Copilot · ${plan}`,
128
+ });
129
+ } catch (error) {
130
+ if (signal.aborted) return snapshot(this, target, "unavailable", { error: "aborted" });
131
+ if (error instanceof HttpError && (error.status === 401 || error.status === 403)) {
132
+ return snapshot(this, target, "unauthorized", { error: `HTTP ${error.status}` });
133
+ }
134
+ return snapshot(this, target, "unavailable", { error: safeError(error) });
135
+ }
136
+ },
137
+ };
@@ -0,0 +1,120 @@
1
+ import { fetchJson, HttpError, safeError, urlOnDomain } from "../http.ts";
2
+ import { windowsSummary, type QuotaWindowMetric } from "../format.ts";
3
+ import { snapshot, type Metric, type UsageAdapter, type UsageSnapshot } from "../types.ts";
4
+
5
+ const BIGMODEL_ORIGIN = "https://open.bigmodel.cn";
6
+ const ZAI_ORIGIN = "https://api.z.ai";
7
+ const QUOTA_PATH = "/api/monitor/usage/quota/limit";
8
+
9
+ const GLM_IDS = new Set(["glm", "zhipu", "bigmodel", "zai", "zai-coding-cn", "glm-v2max"]);
10
+
11
+ interface QuotaLimit {
12
+ type?: string;
13
+ percentage?: number;
14
+ unit?: number;
15
+ number?: number;
16
+ nextResetTime?: number;
17
+ currentValue?: number;
18
+ usage?: number;
19
+ remaining?: number;
20
+ }
21
+
22
+ interface QuotaLimitResponse {
23
+ success?: boolean;
24
+ data?: { level?: string; limits?: QuotaLimit[] };
25
+ }
26
+
27
+ export function glmWindowLabel(unit: number | undefined, quantity: number | undefined): { label: string; order: number } {
28
+ const q = quantity ?? 1;
29
+ if (unit === 3) return q === 5 ? { label: "5h", order: 0 } : { label: `${q}h`, order: 2 };
30
+ if (unit === 6) return q === 1 ? { label: "7d", order: 1 } : { label: `${q}w`, order: 3 };
31
+ return { label: `u${unit ?? "?"}·${q}`, order: 4 };
32
+ }
33
+
34
+ export function resetIso(nextResetTime: number | undefined): string | undefined {
35
+ if (nextResetTime === undefined || !Number.isFinite(nextResetTime)) return undefined;
36
+ const millis = nextResetTime < 10_000_000_000 ? nextResetTime * 1000 : nextResetTime;
37
+ const date = new Date(millis);
38
+ return Number.isFinite(date.getTime()) ? date.toISOString() : undefined;
39
+ }
40
+
41
+ export const glmAdapter: UsageAdapter = {
42
+ id: "glm",
43
+ label: "GLM / Zhipu",
44
+
45
+ canHandle(target) {
46
+ const pid = target.providerId.toLowerCase();
47
+ return (
48
+ GLM_IDS.has(pid) ||
49
+ (!target.baseUrl && pid.startsWith("glm-")) ||
50
+ urlOnDomain(target.baseUrl, "bigmodel.cn") ||
51
+ urlOnDomain(target.baseUrl, "z.ai")
52
+ );
53
+ },
54
+
55
+ async fetch({ target, signal }): Promise<UsageSnapshot> {
56
+ try {
57
+ const apiKey = target.credentials?.apiKey;
58
+ if (!apiKey) return snapshot(this, target, "unauthorized", { error: "No API key" });
59
+
60
+ const origin = urlOnDomain(target.baseUrl, "z.ai") ? ZAI_ORIGIN : BIGMODEL_ORIGIN;
61
+ const payload = await fetchJson<QuotaLimitResponse>(
62
+ `${origin}${QUOTA_PATH}`,
63
+ { headers: { Authorization: apiKey, Accept: "application/json" } },
64
+ signal,
65
+ );
66
+
67
+ const limits = payload.success ? (payload.data?.limits ?? []) : [];
68
+ if (!limits.length) return snapshot(this, target, "empty");
69
+
70
+ const metrics: Metric[] = [];
71
+ const planLevel = payload.data?.level ? payload.data.level.toUpperCase() : "Coding Plan";
72
+
73
+ const classified = limits
74
+ .filter((limit) => limit.type === "TOKENS_LIMIT")
75
+ .map((limit) => ({ limit, ...glmWindowLabel(limit.unit, limit.number) }))
76
+ .sort((a, b) => a.order - b.order);
77
+ for (const { limit, label } of classified) {
78
+ const used = Math.max(0, Math.min(100, limit.percentage ?? 0));
79
+ const resetAt = resetIso(limit.nextResetTime);
80
+ metrics.push({
81
+ kind: "quota-window",
82
+ id: `glm-${label}`,
83
+ label: `GLM ${label}`,
84
+ remainingFraction: (100 - used) / 100,
85
+ ...(resetAt ? { resetAt } : {}),
86
+ });
87
+ }
88
+
89
+ const mcp = limits.find((limit) => limit.type === "TIME_LIMIT");
90
+ if (mcp && mcp.usage !== undefined && mcp.usage > 0) {
91
+ const remaining = mcp.remaining ?? mcp.usage - (mcp.currentValue ?? 0);
92
+ metrics.push({
93
+ kind: "quota-window",
94
+ id: "glm-mcp",
95
+ label: "MCP Monthly",
96
+ remainingFraction: Math.max(0, Math.min(1, remaining / mcp.usage)),
97
+ detail: `${mcp.currentValue ?? 0}/${mcp.usage}`,
98
+ });
99
+ }
100
+
101
+ if (!metrics.length) return snapshot(this, target, "empty");
102
+ metrics.push({ kind: "status", id: "glm-plan-level", label: "Plan", value: planLevel });
103
+
104
+ const windows = metrics.filter(
105
+ (metric): metric is QuotaWindowMetric => metric.kind === "quota-window" && metric.id !== "glm-mcp",
106
+ );
107
+ const summary = windowsSummary(windows) ?? `GLM · ${planLevel}`;
108
+ return snapshot(this, target, "ok", {
109
+ accounts: [{ id: "glm-coding-plan", label: `GLM ${planLevel}`, metrics }],
110
+ summary,
111
+ });
112
+ } catch (error) {
113
+ if (signal.aborted) return snapshot(this, target, "unavailable", { error: "aborted" });
114
+ if (error instanceof HttpError && (error.status === 401 || error.status === 403)) {
115
+ return snapshot(this, target, "unauthorized", { error: `HTTP ${error.status}` });
116
+ }
117
+ return snapshot(this, target, "unavailable", { error: safeError(error) });
118
+ }
119
+ },
120
+ };
@@ -0,0 +1,183 @@
1
+ import { windowsSummary, type QuotaWindowMetric } from "../format.ts";
2
+ import { fetchJson, HttpError, safeError, urlOrigin, urlOnDomain } from "../http.ts";
3
+ import { snapshot, type Metric, type UsageAdapter, type UsageSnapshot } from "../types.ts";
4
+
5
+ const CODING_USAGES_URL = "https://api.kimi.com/coding/v1/usages";
6
+
7
+ const KIMI_IDS = new Set(["kimi", "kimi-coding", "moonshotai", "moonshotai-cn", "moonshot"]);
8
+
9
+ interface KimiQuotaDetail {
10
+ limit?: string | number;
11
+ used?: string | number;
12
+ remaining?: string | number;
13
+ resetTime?: string;
14
+ }
15
+
16
+ interface KimiUsagesResponse {
17
+ usage?: KimiQuotaDetail;
18
+ limits?: Array<{ window?: { duration?: number; timeUnit?: string }; detail?: KimiQuotaDetail }>;
19
+ }
20
+
21
+ interface MoonshotBalanceResponse {
22
+ balance_infos?: Array<{ currency?: string; total_balance?: string }>;
23
+ }
24
+
25
+ function clamp01(value: number): number {
26
+ return Math.max(0, Math.min(1, value));
27
+ }
28
+
29
+ function parseFraction(detail: KimiQuotaDetail | undefined): number | undefined {
30
+ if (!detail) return undefined;
31
+ const lim = Number(detail.limit);
32
+ const rem = Number(detail.remaining);
33
+ if (Number.isFinite(rem) && Number.isFinite(lim) && lim > 0) return clamp01(rem / lim);
34
+ const used = Number(detail.used);
35
+ if (Number.isFinite(used) && Number.isFinite(lim) && lim > 0) return clamp01((lim - used) / lim);
36
+ return undefined;
37
+ }
38
+
39
+ function isoTime(value: string | undefined): string | undefined {
40
+ if (!value) return undefined;
41
+ const millis = Date.parse(value);
42
+ return Number.isFinite(millis) ? new Date(millis).toISOString() : undefined;
43
+ }
44
+
45
+ function quotaExhaustedMessage(body: string): string | undefined {
46
+ try {
47
+ const data = JSON.parse(body) as {
48
+ code?: string;
49
+ message?: string;
50
+ details?: Array<{ debug?: { reason?: string; localizedMessage?: { message?: string } } }>;
51
+ };
52
+ const detail = data.details?.[0]?.debug;
53
+ if (data.code === "resource_exhausted" || detail?.reason === "REASON_QUOTA_EXCEEDED") {
54
+ return detail?.localizedMessage?.message ?? data.message ?? "Quota exhausted";
55
+ }
56
+ } catch {
57
+ return undefined;
58
+ }
59
+ return undefined;
60
+ }
61
+
62
+ export const kimiAdapter: UsageAdapter = {
63
+ id: "kimi",
64
+ label: "Kimi / Moonshot",
65
+
66
+ canHandle(target) {
67
+ const pid = target.providerId.toLowerCase();
68
+ return (
69
+ KIMI_IDS.has(pid) ||
70
+ urlOnDomain(target.baseUrl, "api.kimi.com") ||
71
+ urlOnDomain(target.baseUrl, "moonshot.cn") ||
72
+ urlOnDomain(target.baseUrl, "moonshot.ai")
73
+ );
74
+ },
75
+
76
+ async fetch({ target, signal }): Promise<UsageSnapshot> {
77
+ try {
78
+ const apiKey = target.credentials?.apiKey;
79
+ if (!apiKey) return snapshot(this, target, "unauthorized", { error: "No API key" });
80
+
81
+ const pid = target.providerId.toLowerCase();
82
+ const codingPlan =
83
+ urlOnDomain(target.baseUrl, "api.kimi.com") || pid.includes("coding") || apiKey.startsWith("eyJ");
84
+
85
+ if (codingPlan) {
86
+ const payload = await fetchJson<KimiUsagesResponse>(
87
+ CODING_USAGES_URL,
88
+ { headers: { Authorization: `Bearer ${apiKey}`, Accept: "application/json" } },
89
+ signal,
90
+ );
91
+
92
+ const metrics: Metric[] = [];
93
+ const fiveHour = payload.limits?.find(
94
+ (limit) => limit.window?.duration === 300 && limit.window?.timeUnit === "TIME_UNIT_MINUTE",
95
+ )?.detail;
96
+ const fiveHourFraction = parseFraction(fiveHour);
97
+ const fiveHourReset = isoTime(fiveHour?.resetTime);
98
+ if (fiveHourFraction !== undefined) {
99
+ metrics.push({
100
+ kind: "quota-window",
101
+ id: "kimi-5h",
102
+ label: "Kimi 5h",
103
+ remainingFraction: fiveHourFraction,
104
+ ...(fiveHourReset ? { resetAt: fiveHourReset } : {}),
105
+ });
106
+ }
107
+ const weeklyFraction = parseFraction(payload.usage);
108
+ const weeklyReset = isoTime(payload.usage?.resetTime);
109
+ if (weeklyFraction !== undefined) {
110
+ metrics.push({
111
+ kind: "quota-window",
112
+ id: "kimi-weekly",
113
+ label: "Kimi Weekly",
114
+ remainingFraction: weeklyFraction,
115
+ ...(weeklyReset ? { resetAt: weeklyReset } : {}),
116
+ });
117
+ }
118
+ if (!metrics.length) return snapshot(this, target, "empty");
119
+ const windows = metrics.filter(
120
+ (metric): metric is QuotaWindowMetric => metric.kind === "quota-window",
121
+ );
122
+ return snapshot(this, target, "ok", {
123
+ accounts: [{ id: "kimi-coding", label: "Kimi Coding", metrics }],
124
+ summary: windowsSummary(windows),
125
+ });
126
+ }
127
+
128
+ const origin =
129
+ urlOrigin(target.baseUrl) ?? (pid.endsWith("-cn") ? "https://api.moonshot.cn" : "https://api.moonshot.ai");
130
+ const payload = await fetchJson<MoonshotBalanceResponse>(
131
+ `${origin}/v1/users/me/balance`,
132
+ { headers: { Authorization: `Bearer ${apiKey}`, Accept: "application/json" } },
133
+ signal,
134
+ );
135
+
136
+ const accounts = (payload.balance_infos ?? []).map((info, index) => ({
137
+ id: `kimi-${info.currency ?? index}`,
138
+ label: info.currency === "CNY" ? "Account (CNY)" : `Account (${info.currency ?? "?"})`,
139
+ metrics:
140
+ info.total_balance !== undefined
141
+ ? [
142
+ {
143
+ kind: "balance" as const,
144
+ id: "total",
145
+ label: "Balance",
146
+ amount: Number(info.total_balance) || 0,
147
+ currency: info.currency ?? "CNY",
148
+ },
149
+ ]
150
+ : [],
151
+ }));
152
+ const primary = accounts[0]?.metrics[0];
153
+ if (!primary) return snapshot(this, target, "empty");
154
+ return snapshot(this, target, "ok", {
155
+ accounts,
156
+ summary:
157
+ primary.kind === "balance"
158
+ ? `${primary.currency === "CNY" ? "¥" : "$"}${primary.amount.toFixed(2)} · Kimi`
159
+ : undefined,
160
+ });
161
+ } catch (error) {
162
+ if (signal.aborted) return snapshot(this, target, "unavailable", { error: "aborted" });
163
+ if (error instanceof HttpError && (error.status === 401 || error.status === 403)) {
164
+ return snapshot(this, target, "unauthorized", { error: `HTTP ${error.status}` });
165
+ }
166
+ if (error instanceof HttpError && error.status === 429) {
167
+ const message = quotaExhaustedMessage(error.body);
168
+ if (message) {
169
+ return snapshot(this, target, "empty", {
170
+ accounts: [
171
+ {
172
+ id: "kimi-coding",
173
+ label: "Kimi Coding",
174
+ metrics: [{ kind: "status", id: "kimi-quota", label: "Quota", value: message }],
175
+ },
176
+ ],
177
+ });
178
+ }
179
+ }
180
+ return snapshot(this, target, "unavailable", { error: safeError(error) });
181
+ }
182
+ },
183
+ };
@@ -0,0 +1,185 @@
1
+ import { windowsSummary, type QuotaWindowMetric } from "../format.ts";
2
+ import { fetchJson, HttpError, safeError, urlOnDomain } from "../http.ts";
3
+ import { snapshot, type Metric, type UsageAccount, type UsageAdapter, type UsageSnapshot } from "../types.ts";
4
+
5
+ interface MiniMaxRemainsResponse {
6
+ base_resp?: { status_code?: number };
7
+ model_remains?: unknown[];
8
+ }
9
+
10
+ interface MiniMaxBalanceResponse {
11
+ base_resp?: { status_code?: number };
12
+ available_amount?: string;
13
+ cash_balance?: string;
14
+ voucher_balance?: string;
15
+ credit_balance?: string;
16
+ owed_amount?: string;
17
+ }
18
+
19
+ type Row = Record<string, unknown>;
20
+
21
+ const DECIMAL_AMOUNT = /^-?(?:0|[1-9]\d*)(?:\.\d+)?$/;
22
+
23
+ const WINDOW_FIELDS = [
24
+ {
25
+ id: "interval",
26
+ label: "Rolling",
27
+ count: "current_interval_usage_count",
28
+ total: "current_interval_total_count",
29
+ percent: "current_interval_remaining_percent",
30
+ status: "current_interval_status",
31
+ end: "end_time",
32
+ },
33
+ {
34
+ id: "weekly",
35
+ label: "Weekly",
36
+ count: "current_weekly_usage_count",
37
+ total: "current_weekly_total_count",
38
+ percent: "current_weekly_remaining_percent",
39
+ status: "current_weekly_status",
40
+ end: "weekly_end_time",
41
+ },
42
+ ] as const;
43
+
44
+ function intVal(value: unknown): number | undefined {
45
+ if (value === undefined || value === null) return undefined;
46
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : undefined;
47
+ }
48
+
49
+ function percentVal(value: unknown): number | undefined {
50
+ if (value === undefined || value === null) return undefined;
51
+ return typeof value === "number" && Number.isFinite(value) && value >= 0 && value <= 100 ? value : undefined;
52
+ }
53
+
54
+ export function resolveRemaining(reported: number, total: number, percent: number | undefined): number | undefined {
55
+ if (total <= 0 || reported > total) return undefined;
56
+ let remaining = reported;
57
+ if (percent !== undefined) {
58
+ const asRemaining = (reported / total) * 100;
59
+ const asUsed = ((total - reported) / total) * 100;
60
+ const remainingDistance = Math.abs(asRemaining - percent);
61
+ const usedDistance = Math.abs(asUsed - percent);
62
+ if (Math.min(remainingDistance, usedDistance) > 1) return undefined;
63
+ if (usedDistance < remainingDistance) remaining = total - reported;
64
+ }
65
+ return remaining;
66
+ }
67
+
68
+ function windowMetric(
69
+ row: Row,
70
+ fields: (typeof WINDOW_FIELDS)[number],
71
+ id: string,
72
+ label: string,
73
+ ): Metric | undefined {
74
+ const status = intVal(row[fields.status]);
75
+ if (status !== undefined && ![1, 2, 3].includes(status)) return undefined;
76
+ const end = intVal(row[fields.end]);
77
+ if (end === undefined || end <= 0) return undefined;
78
+ const resetAt = new Date(end).toISOString();
79
+ if (status === 3) return { kind: "quota-window", id, label, remainingFraction: 1, resetAt };
80
+ const percent = percentVal(row[fields.percent]);
81
+ const total = intVal(row[fields.total]) ?? 0;
82
+ const count = intVal(row[fields.count]);
83
+ if (total === 0) {
84
+ if (percent === undefined || (count !== undefined && count !== 0)) return undefined;
85
+ return { kind: "quota-window", id, label, remainingFraction: Math.min(1, percent / 100), resetAt };
86
+ }
87
+ if (count === undefined) return undefined;
88
+ const remaining = resolveRemaining(count, total, percent);
89
+ if (remaining === undefined) return undefined;
90
+ return { kind: "quota-window", id, label, remainingFraction: Math.max(0, Math.min(1, remaining / total)), resetAt };
91
+ }
92
+
93
+ function balanceMetric(id: string, label: string, value: string | undefined, currency: string): Metric | undefined {
94
+ if (value === undefined || !DECIMAL_AMOUNT.test(value)) return undefined;
95
+ return { kind: "balance", id, label, amount: Number(value), currency };
96
+ }
97
+
98
+ export const minimaxAdapter: UsageAdapter = {
99
+ id: "minimax",
100
+ label: "MiniMax",
101
+
102
+ canHandle(target) {
103
+ const pid = target.providerId.toLowerCase();
104
+ return (
105
+ pid === "minimax" ||
106
+ pid === "minimax-cn" ||
107
+ urlOnDomain(target.baseUrl, "minimax.io") ||
108
+ urlOnDomain(target.baseUrl, "minimaxi.com")
109
+ );
110
+ },
111
+
112
+ async fetch({ target, signal }): Promise<UsageSnapshot> {
113
+ try {
114
+ const apiKey = target.credentials?.apiKey;
115
+ if (!apiKey) return snapshot(this, target, "unauthorized", { error: "No API key" });
116
+
117
+ const cn = urlOnDomain(target.baseUrl, "minimaxi.com") || target.providerId.toLowerCase() === "minimax-cn";
118
+ const root = cn ? "https://api.minimaxi.com" : "https://api.minimax.io";
119
+ const currency = cn ? "CNY" : "USD";
120
+
121
+ if (apiKey.startsWith("eyJ")) {
122
+ const payload = await fetchJson<MiniMaxRemainsResponse>(
123
+ `${root}/v1/token_plan/remains`,
124
+ { headers: { Authorization: `Bearer ${apiKey}`, Accept: "application/json" } },
125
+ signal,
126
+ );
127
+ if (payload.base_resp?.status_code !== 0) {
128
+ throw new Error("MiniMax usage response did not report success");
129
+ }
130
+
131
+ const accounts: UsageAccount[] = [];
132
+ for (const [index, raw] of (payload.model_remains ?? []).entries()) {
133
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) continue;
134
+ const row = raw as Row;
135
+ const modelName =
136
+ typeof row.model_name === "string" && row.model_name ? row.model_name : `Quota ${index + 1}`;
137
+ const metrics = WINDOW_FIELDS.map((fields) =>
138
+ windowMetric(row, fields, `minimax-${index}-${fields.id}`, `${modelName} ${fields.label}`),
139
+ ).filter((metric): metric is Metric => metric !== undefined);
140
+ if (metrics.length) accounts.push({ id: `minimax-${index}`, label: modelName, metrics });
141
+ }
142
+ if (!accounts.length) return snapshot(this, target, "empty");
143
+ const windows = accounts[0].metrics.filter(
144
+ (metric): metric is QuotaWindowMetric => metric.kind === "quota-window",
145
+ );
146
+ return snapshot(this, target, "ok", {
147
+ accounts,
148
+ summary: windowsSummary(windows),
149
+ });
150
+ }
151
+
152
+ const payload = await fetchJson<MiniMaxBalanceResponse>(
153
+ `${root}/account/query_balance`,
154
+ { headers: { Authorization: `Bearer ${apiKey}`, Accept: "application/json" } },
155
+ signal,
156
+ );
157
+ if (payload.base_resp?.status_code !== 0) {
158
+ throw new Error("MiniMax usage response did not report success");
159
+ }
160
+
161
+ const metrics = [
162
+ balanceMetric("available", "Available balance", payload.available_amount, currency),
163
+ balanceMetric("cash", "Cash balance", payload.cash_balance, currency),
164
+ balanceMetric("voucher", "Voucher balance", payload.voucher_balance, currency),
165
+ balanceMetric("credit", "Credit balance", payload.credit_balance, currency),
166
+ balanceMetric("owed", "Owed amount", payload.owed_amount, currency),
167
+ ].filter((metric): metric is Metric => metric !== undefined);
168
+ if (!metrics.length) return snapshot(this, target, "empty");
169
+ const primary = metrics[0];
170
+ return snapshot(this, target, "ok", {
171
+ accounts: [{ id: "minimax", label: `MiniMax (${currency})`, metrics }],
172
+ summary:
173
+ primary.kind === "balance"
174
+ ? `${currency === "CNY" ? "¥" : "$"}${primary.amount.toFixed(2)} · MiniMax`
175
+ : undefined,
176
+ });
177
+ } catch (error) {
178
+ if (signal.aborted) return snapshot(this, target, "unavailable", { error: "aborted" });
179
+ if (error instanceof HttpError && (error.status === 401 || error.status === 403)) {
180
+ return snapshot(this, target, "unauthorized", { error: `HTTP ${error.status}` });
181
+ }
182
+ return snapshot(this, target, "unavailable", { error: safeError(error) });
183
+ }
184
+ },
185
+ };