@narumitw/pi-usage 0.60.7 → 0.60.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.ts +91 -339
- package/dist/index.ts.map +2 -2
- package/docs/providers.md +2 -1
- package/package.json +73 -74
- package/src/codex-fast-runtime.ts +148 -184
- package/src/codex-fast.ts +74 -83
- package/src/codex-resets.ts +249 -278
- package/src/core.ts +204 -206
- package/src/format.ts +519 -574
- package/src/index.ts +77 -77
- package/src/oauth-credential-source.ts +57 -65
- package/src/providers/baseten.ts +32 -36
- package/src/providers/codex.ts +109 -120
- package/src/providers/deepseek.ts +59 -64
- package/src/providers/fireworks.ts +219 -238
- package/src/providers/github-copilot.ts +89 -95
- package/src/providers/kimi-coding.ts +202 -213
- package/src/providers/minimax.ts +214 -219
- package/src/providers/moonshot.ts +41 -46
- package/src/providers/opencode-zen.ts +56 -59
- package/src/providers/openrouter.ts +50 -53
- package/src/providers/vercel-ai-gateway.ts +33 -33
- package/src/providers/xai.ts +141 -148
- package/src/providers/zai-errors.ts +75 -75
- package/src/providers/zai.ts +173 -189
- package/src/query.ts +887 -967
- package/src/settings.ts +275 -304
- package/src/types.ts +145 -145
- package/src/usage-helpers.ts +18 -18
- package/src/usage-settings-ui.ts +117 -126
- package/src/usage-targets.ts +98 -114
- package/src/usage.ts +1264 -1426
package/src/format.ts
CHANGED
|
@@ -1,729 +1,674 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
ProviderUsageState,
|
|
3
|
-
UsageBucket,
|
|
4
|
-
UsageDisplayState,
|
|
5
|
-
UsageModel,
|
|
6
|
-
UsageReport,
|
|
7
|
-
} from "./types.js";
|
|
1
|
+
import type { ProviderUsageState, UsageBucket, UsageDisplayState, UsageModel, UsageReport } from "./types.js";
|
|
8
2
|
|
|
9
3
|
const BAR_SEGMENTS = 20;
|
|
10
4
|
const VALUE_COLUMN = 29;
|
|
11
5
|
|
|
12
6
|
export function formatUsageReport(report: UsageReport, displayState: UsageDisplayState): string {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
7
|
+
const stateLabel = displayState === "current" ? "Current" : "Configured";
|
|
8
|
+
const title =
|
|
9
|
+
report.providerId === "baseten"
|
|
10
|
+
? "Baseten Model APIs Spend"
|
|
11
|
+
: report.providerId === "deepseek"
|
|
12
|
+
? "DeepSeek API Balance"
|
|
13
|
+
: report.providerId === "fireworks"
|
|
14
|
+
? "Fireworks API Spend"
|
|
15
|
+
: report.providerId === "vercel-ai-gateway"
|
|
16
|
+
? "Vercel AI Gateway Credits"
|
|
17
|
+
: report.providerId === "moonshotai" || report.providerId === "moonshotai-cn"
|
|
18
|
+
? `${report.providerName} Balance`
|
|
19
|
+
: report.providerId === "minimax" || report.providerId === "minimax-cn"
|
|
20
|
+
? report.source === "minimax-account-balance"
|
|
21
|
+
? `${report.providerName} API Balance`
|
|
22
|
+
: `${report.providerName} Token Plan`
|
|
23
|
+
: `${report.providerName} Usage`;
|
|
24
|
+
const lines = [`${title} · ${stateLabel}`];
|
|
25
|
+
if (report.accountLabel) lines.push(`Account: ${report.accountLabel}`);
|
|
26
|
+
lines.push(`Semantics: ${report.semantics.label}`, "");
|
|
27
|
+
|
|
28
|
+
if (report.providerId === "baseten") formatBasetenReport(lines, report);
|
|
29
|
+
else if (report.providerId === "openai-codex") formatCodexReport(lines, report);
|
|
30
|
+
else if (report.providerId === "deepseek") formatDeepSeekReport(lines, report);
|
|
31
|
+
else if (report.providerId === "fireworks") formatFireworksReport(lines, report);
|
|
32
|
+
else if (report.providerId === "vercel-ai-gateway") formatVercelAIGatewayReport(lines, report);
|
|
33
|
+
else if (report.providerId === "github-copilot") formatGitHubCopilotReport(lines, report);
|
|
34
|
+
else if (report.providerId === "openrouter") formatOpenRouterReport(lines, report);
|
|
35
|
+
else if (report.providerId === "opencode-go") formatOpenCodeZenReport(lines, report);
|
|
36
|
+
else if (report.providerId === "kimi-coding") formatKimiCodingReport(lines, report);
|
|
37
|
+
else if (report.providerId === "moonshotai" || report.providerId === "moonshotai-cn") {
|
|
38
|
+
formatMoonshotReport(lines, report);
|
|
39
|
+
} else if (report.providerId === "minimax" || report.providerId === "minimax-cn") {
|
|
40
|
+
formatMiniMaxReport(lines, report);
|
|
41
|
+
} else if (report.providerId === "xai") formatXaiReport(lines, report);
|
|
42
|
+
else if (report.providerId === "zai" || report.providerId === "zai-coding-cn") {
|
|
43
|
+
formatZaiReport(lines, report);
|
|
44
|
+
} else formatGenericReport(lines, report);
|
|
45
|
+
|
|
46
|
+
if (report.notes) {
|
|
47
|
+
for (const note of report.notes) lines.push(note);
|
|
48
|
+
}
|
|
49
|
+
return lines.join("\n").trimEnd();
|
|
56
50
|
}
|
|
57
51
|
|
|
58
52
|
export function formatUsageStatusline(
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
53
|
+
report: UsageReport,
|
|
54
|
+
model?: UsageModel,
|
|
55
|
+
now = Date.now(),
|
|
56
|
+
showCodexResetCountdown = true,
|
|
63
57
|
): string | undefined {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
58
|
+
if (report.providerId === "baseten") return formatBasetenStatusline(report);
|
|
59
|
+
if (report.providerId === "openai-codex") {
|
|
60
|
+
return formatCodexStatusline(report, model, now, showCodexResetCountdown);
|
|
61
|
+
}
|
|
62
|
+
if (report.providerId === "deepseek") return formatDeepSeekStatusline(report);
|
|
63
|
+
if (report.providerId === "fireworks") return formatFireworksStatusline(report);
|
|
64
|
+
if (report.providerId === "vercel-ai-gateway") return formatVercelAIGatewayStatusline(report);
|
|
65
|
+
if (report.providerId === "github-copilot") return formatGitHubCopilotStatusline(report);
|
|
66
|
+
if (report.providerId === "openrouter") {
|
|
67
|
+
const limit = report.buckets.find((bucket) => bucket.id === "key-limit");
|
|
68
|
+
if (limit?.remaining !== undefined) return `openrouter ${formatUsd(limit.remaining)} left`;
|
|
69
|
+
const total = report.metrics.find((metric) => metric.id === "usage-total");
|
|
70
|
+
if (typeof total?.value === "number") return `openrouter ${formatUsd(total.value)} used`;
|
|
71
|
+
}
|
|
72
|
+
if (report.providerId === "opencode-go") return formatOpenCodeZenStatusline(report);
|
|
73
|
+
if (report.providerId === "kimi-coding") return formatKimiCodingStatusline(report);
|
|
74
|
+
if (report.providerId === "moonshotai" || report.providerId === "moonshotai-cn") {
|
|
75
|
+
return formatMoonshotStatusline(report);
|
|
76
|
+
}
|
|
77
|
+
if (report.providerId === "minimax" || report.providerId === "minimax-cn") {
|
|
78
|
+
return formatMiniMaxStatusline(report, model);
|
|
79
|
+
}
|
|
80
|
+
if (report.providerId === "zai" || report.providerId === "zai-coding-cn") {
|
|
81
|
+
return formatZaiStatusline(report);
|
|
82
|
+
}
|
|
83
|
+
return undefined;
|
|
90
84
|
}
|
|
91
85
|
|
|
92
86
|
export function formatProviderStates(states: readonly ProviderUsageState[]): string {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
87
|
+
return states
|
|
88
|
+
.map((state) => {
|
|
89
|
+
if (state.status === "ready") return formatUsageReport(state.report, state.displayState);
|
|
90
|
+
const label = state.displayState === "current" ? "Current" : "Configured";
|
|
91
|
+
if (state.status === "selection-required") {
|
|
92
|
+
return `${state.providerName} · ${label}\nSelection required: choose this provider's ${state.singularLabel} by viewing it individually.`;
|
|
93
|
+
}
|
|
94
|
+
const status =
|
|
95
|
+
state.status === "auth-unavailable"
|
|
96
|
+
? "Authentication unavailable"
|
|
97
|
+
: state.status === "unsupported"
|
|
98
|
+
? "Unsupported"
|
|
99
|
+
: "Query failed";
|
|
100
|
+
return `${state.providerName} · ${label}\n${status}: ${state.message}`;
|
|
101
|
+
})
|
|
102
|
+
.join("\n\n");
|
|
109
103
|
}
|
|
110
104
|
|
|
111
105
|
function formatBasetenReport(lines: string[], report: UsageReport): void {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
106
|
+
lines.push(`${"Spend window:".padEnd(VALUE_COLUMN)}Last 30 days`);
|
|
107
|
+
for (const metric of report.metrics) {
|
|
108
|
+
lines.push(`${`${metric.label}:`.padEnd(VALUE_COLUMN)}USD ${metric.value}`);
|
|
109
|
+
}
|
|
116
110
|
}
|
|
117
111
|
|
|
118
112
|
function formatBasetenStatusline(report: UsageReport): string {
|
|
119
|
-
|
|
120
|
-
|
|
113
|
+
const subtotal = report.metrics.find((metric) => metric.id === "net-subtotal");
|
|
114
|
+
return subtotal ? `baseten USD ${subtotal.value} net` : "baseten no Model APIs usage";
|
|
121
115
|
}
|
|
122
116
|
|
|
123
117
|
function formatCodexReport(lines: string[], report: UsageReport): void {
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
}
|
|
143
|
-
}
|
|
118
|
+
let previousGroup: string | undefined;
|
|
119
|
+
for (const bucket of report.buckets) {
|
|
120
|
+
const group = bucket.groupId ?? bucket.id;
|
|
121
|
+
if (group !== previousGroup && group !== "codex") {
|
|
122
|
+
lines.push(`${bucket.groupLabel ?? group} limit:`);
|
|
123
|
+
}
|
|
124
|
+
previousGroup = group;
|
|
125
|
+
const fallback = bucket.id.endsWith(":secondary") ? "weekly" : "5h";
|
|
126
|
+
const label = `${formatWindowLabel(bucket.windowMinutes, fallback, false)} limit:`;
|
|
127
|
+
lines.push(`${label.padEnd(VALUE_COLUMN)}${formatPercentBucket(bucket)}`);
|
|
128
|
+
}
|
|
129
|
+
for (const metric of report.metrics) {
|
|
130
|
+
if (metric.id === "reset-credits") {
|
|
131
|
+
lines.push(`${"Usage limit resets:".padEnd(VALUE_COLUMN)}${metric.value} available`);
|
|
132
|
+
} else if (metric.id === "credits") {
|
|
133
|
+
lines.push(`${"Credits:".padEnd(VALUE_COLUMN)}${formatMetricValue(metric.value, metric.unit)}`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
144
136
|
}
|
|
145
137
|
|
|
146
138
|
function formatDeepSeekReport(lines: string[], report: UsageReport): void {
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
139
|
+
const availability = report.metrics.find((metric) => metric.id === "api-availability");
|
|
140
|
+
lines.push(
|
|
141
|
+
`${"API calls:".padEnd(VALUE_COLUMN)}${availability?.value === "available" ? "Available" : "Unavailable"}`,
|
|
142
|
+
);
|
|
143
|
+
for (const currency of ["CNY", "USD"]) {
|
|
144
|
+
const metrics = report.metrics.filter((metric) => metric.currency === currency);
|
|
145
|
+
if (metrics.length === 0) continue;
|
|
146
|
+
lines.push("", `${currency} balance:`);
|
|
147
|
+
for (const metric of metrics) {
|
|
148
|
+
lines.push(`${`${metric.label}:`.padEnd(VALUE_COLUMN)}${currency} ${metric.value}`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
159
151
|
}
|
|
160
152
|
|
|
161
153
|
function formatDeepSeekStatusline(report: UsageReport): string {
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
});
|
|
170
|
-
return totals.length > 0 ? `deepseek ${totals.join(" · ")}` : "deepseek balance unavailable";
|
|
154
|
+
const availability = report.metrics.find((metric) => metric.id === "api-availability");
|
|
155
|
+
if (availability?.value !== "available") return "deepseek API unavailable";
|
|
156
|
+
const totals = ["CNY", "USD"].flatMap((currency) => {
|
|
157
|
+
const metric = report.metrics.find((candidate) => candidate.id === `${currency.toLowerCase()}-total`);
|
|
158
|
+
return metric ? [`${currency} ${metric.value}`] : [];
|
|
159
|
+
});
|
|
160
|
+
return totals.length > 0 ? `deepseek ${totals.join(" · ")}` : "deepseek balance unavailable";
|
|
171
161
|
}
|
|
172
162
|
|
|
173
163
|
function formatFireworksReport(lines: string[], report: UsageReport): void {
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
164
|
+
lines.push(`${"Spend window:".padEnd(VALUE_COLUMN)}Last 30 days (rated)`);
|
|
165
|
+
for (const currency of fireworksCurrencies(report)) {
|
|
166
|
+
lines.push("", `${currency} rated spend:`);
|
|
167
|
+
for (const metric of report.metrics) {
|
|
168
|
+
if (metric.currency !== currency) continue;
|
|
169
|
+
lines.push(`${`${metric.label}:`.padEnd(VALUE_COLUMN)}${currency} ${metric.value}`);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
182
172
|
}
|
|
183
173
|
|
|
184
174
|
function formatFireworksStatusline(report: UsageReport): string {
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
175
|
+
const totals = report.metrics.filter((metric) => metric.id.endsWith("-total"));
|
|
176
|
+
if (totals.length === 0) return "fireworks no rated usage";
|
|
177
|
+
return `fireworks ${totals.map((metric) => `${metric.currency} ${metric.value}`).join(" · ")}`;
|
|
188
178
|
}
|
|
189
179
|
|
|
190
180
|
function fireworksCurrencies(report: UsageReport): string[] {
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
181
|
+
const currencies: string[] = [];
|
|
182
|
+
for (const metric of report.metrics) {
|
|
183
|
+
if (!metric.currency || currencies.includes(metric.currency)) continue;
|
|
184
|
+
currencies.push(metric.currency);
|
|
185
|
+
}
|
|
186
|
+
return currencies;
|
|
197
187
|
}
|
|
198
188
|
|
|
199
189
|
function formatVercelAIGatewayReport(lines: string[], report: UsageReport): void {
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
190
|
+
for (const metric of report.metrics) {
|
|
191
|
+
lines.push(`${`${metric.label}:`.padEnd(VALUE_COLUMN)}USD ${metric.value}`);
|
|
192
|
+
}
|
|
203
193
|
}
|
|
204
194
|
|
|
205
195
|
function formatVercelAIGatewayStatusline(report: UsageReport): string {
|
|
206
|
-
|
|
207
|
-
|
|
196
|
+
const balance = report.metrics.find((metric) => metric.id === "credit-balance");
|
|
197
|
+
return balance ? `vercel USD ${balance.value} left` : "vercel credits unavailable";
|
|
208
198
|
}
|
|
209
199
|
|
|
210
200
|
function formatGitHubCopilotReport(lines: string[], report: UsageReport): void {
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
201
|
+
const quota = findGitHubCopilotQuota(report);
|
|
202
|
+
if (!quota || quota.limit === undefined || quota.remaining === undefined) {
|
|
203
|
+
lines.push(`${`${quota?.label ?? "Copilot quota"}:`.padEnd(VALUE_COLUMN)}unlimited`);
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
const percent = percentRemaining(quota);
|
|
207
|
+
const reset = quota.resetsAt ? ` (resets ${formatReset(quota.resetsAt)})` : "";
|
|
208
|
+
lines.push(
|
|
209
|
+
`${`${quota.label}:`.padEnd(VALUE_COLUMN)}${quota.remaining} of ${quota.limit} left · ${percent}%${reset}`,
|
|
210
|
+
);
|
|
211
|
+
const overage = report.metrics.find((metric) => metric.id === "overage-used");
|
|
212
|
+
if (typeof overage?.value === "number" && overage.value > 0) {
|
|
213
|
+
lines.push(`${"Additional usage:".padEnd(VALUE_COLUMN)}${overage.value} ${quota.label}`);
|
|
214
|
+
}
|
|
225
215
|
}
|
|
226
216
|
|
|
227
217
|
function formatGitHubCopilotStatusline(report: UsageReport): string {
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
return `copilot ${kind === "premium" ? "" : `${kind} `}${quota.remaining}/${quota.limit} ${percentRemaining(quota)}%${overageSuffix}`;
|
|
218
|
+
const quota = findGitHubCopilotQuota(report);
|
|
219
|
+
const kind = compactGitHubCopilotQuotaKind(quota);
|
|
220
|
+
if (!quota || quota.limit === undefined || quota.remaining === undefined) {
|
|
221
|
+
return `copilot ${kind} unlimited`;
|
|
222
|
+
}
|
|
223
|
+
const overage = report.metrics.find((metric) => metric.id === "overage-used");
|
|
224
|
+
const overageSuffix = typeof overage?.value === "number" && overage.value > 0 ? ` +${overage.value} over` : "";
|
|
225
|
+
return `copilot ${kind === "premium" ? "" : `${kind} `}${quota.remaining}/${quota.limit} ${percentRemaining(quota)}%${overageSuffix}`;
|
|
237
226
|
}
|
|
238
227
|
|
|
239
228
|
function findGitHubCopilotQuota(report: UsageReport): UsageBucket | undefined {
|
|
240
|
-
|
|
241
|
-
["ai-credits", "premium-requests", "chat-requests"].includes(bucket.id),
|
|
242
|
-
);
|
|
229
|
+
return report.buckets.find((bucket) => ["ai-credits", "premium-requests", "chat-requests"].includes(bucket.id));
|
|
243
230
|
}
|
|
244
231
|
|
|
245
232
|
function compactGitHubCopilotQuotaKind(bucket: UsageBucket | undefined): string {
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
233
|
+
if (bucket?.id === "ai-credits") return "credits";
|
|
234
|
+
if (bucket?.id === "chat-requests") return "chat";
|
|
235
|
+
return "premium";
|
|
249
236
|
}
|
|
250
237
|
|
|
251
238
|
function percentRemaining(bucket: UsageBucket): number {
|
|
252
|
-
|
|
253
|
-
|
|
239
|
+
if (!bucket.limit || bucket.remaining === undefined) return 0;
|
|
240
|
+
return Math.round(clampPercent((bucket.remaining / bucket.limit) * 100));
|
|
254
241
|
}
|
|
255
242
|
|
|
256
243
|
function formatOpenRouterReport(lines: string[], report: UsageReport): void {
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
);
|
|
270
|
-
}
|
|
244
|
+
const limit = report.buckets.find((bucket) => bucket.id === "key-limit");
|
|
245
|
+
if (limit) {
|
|
246
|
+
const period = limit.period ? ` (${limit.period})` : "";
|
|
247
|
+
const value =
|
|
248
|
+
limit.remaining === undefined
|
|
249
|
+
? `${formatUsd(limit.limit ?? 0)} cap; remaining unavailable`
|
|
250
|
+
: `${formatUsd(limit.remaining)} of ${formatUsd(limit.limit ?? 0)} left`;
|
|
251
|
+
lines.push(`${`Key limit${period}:`.padEnd(VALUE_COLUMN)}${value}`);
|
|
252
|
+
}
|
|
253
|
+
for (const metric of report.metrics) {
|
|
254
|
+
lines.push(`${`${metric.label}:`.padEnd(VALUE_COLUMN)}${formatMetricValue(metric.value, metric.unit)}`);
|
|
255
|
+
}
|
|
271
256
|
}
|
|
272
257
|
|
|
273
258
|
function formatOpenCodeZenReport(lines: string[], report: UsageReport): void {
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
259
|
+
for (const bucket of report.buckets) {
|
|
260
|
+
if (bucket.unit === "percent" && bucket.used !== undefined) {
|
|
261
|
+
lines.push(`${`${bucket.label}:`.padEnd(VALUE_COLUMN)}${formatPercentBucket(bucket)}`);
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
const reset = bucket.resetsAt ? ` (resets ${formatReset(bucket.resetsAt)})` : "";
|
|
265
|
+
const used = bucket.used ?? "unavailable";
|
|
266
|
+
lines.push(`${`${bucket.label}:`.padEnd(VALUE_COLUMN)}${used}% used${reset}`);
|
|
267
|
+
}
|
|
283
268
|
}
|
|
284
269
|
|
|
285
270
|
function formatOpenCodeZenStatusline(report: UsageReport): string | undefined {
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
271
|
+
const parts = ["zen"];
|
|
272
|
+
for (const bucket of report.buckets) {
|
|
273
|
+
if (bucket.used === undefined) continue;
|
|
274
|
+
const compact = bucket.id === "rolling" ? "r" : bucket.id === "weekly" ? "w" : "m";
|
|
275
|
+
parts.push(`${clampPercent(bucket.used).toFixed(0)}% ${compact}`);
|
|
276
|
+
}
|
|
277
|
+
return parts.length > 1 ? parts.join(" ") : undefined;
|
|
293
278
|
}
|
|
294
279
|
|
|
295
280
|
function formatKimiCodingReport(lines: string[], report: UsageReport): void {
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
281
|
+
for (const bucket of report.buckets) {
|
|
282
|
+
const reset = bucket.resetsAt ? ` (resets ${formatReset(bucket.resetsAt)})` : "";
|
|
283
|
+
if (bucket.used === undefined || bucket.limit === undefined) {
|
|
284
|
+
lines.push(`${`${bucket.label}:`.padEnd(VALUE_COLUMN)}unavailable${reset}`);
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
lines.push(
|
|
288
|
+
`${`${bucket.label}:`.padEnd(VALUE_COLUMN)}${bucket.used} of ${bucket.limit} used · ${percentRemaining(bucket)}% left${reset}`,
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
const balance = report.metrics.find((metric) => metric.id === "booster-balance");
|
|
292
|
+
const total = report.metrics.find((metric) => metric.id === "booster-total");
|
|
293
|
+
const monthlyUsed = report.metrics.find((metric) => metric.id === "booster-monthly-used");
|
|
294
|
+
const monthlyLimit = report.metrics.find((metric) => metric.id === "booster-monthly-limit");
|
|
295
|
+
if (!balance && !monthlyUsed && !monthlyLimit) return;
|
|
296
|
+
lines.push("", "Extra usage wallet:");
|
|
297
|
+
if (balance) {
|
|
298
|
+
const totalSuffix = total ? ` of ${formatCurrencyMetric(total)}` : "";
|
|
299
|
+
lines.push(`${"Balance:".padEnd(VALUE_COLUMN)}${formatCurrencyMetric(balance)}${totalSuffix}`);
|
|
300
|
+
}
|
|
301
|
+
if (monthlyUsed) {
|
|
302
|
+
lines.push(`${"Used this month:".padEnd(VALUE_COLUMN)}${formatCurrencyMetric(monthlyUsed)}`);
|
|
303
|
+
}
|
|
304
|
+
if (monthlyLimit) {
|
|
305
|
+
lines.push(`${"Monthly limit:".padEnd(VALUE_COLUMN)}${formatCurrencyMetric(monthlyLimit)}`);
|
|
306
|
+
}
|
|
322
307
|
}
|
|
323
308
|
|
|
324
309
|
function formatKimiCodingStatusline(report: UsageReport): string | undefined {
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
);
|
|
339
|
-
}
|
|
340
|
-
return parts.length > 1 ? parts.join(" ") : undefined;
|
|
310
|
+
const fiveHour = report.buckets.find((bucket) => bucket.id === "five-hour");
|
|
311
|
+
const weekly = report.buckets.find((bucket) => bucket.id === "weekly");
|
|
312
|
+
const subWindow = fiveHour ?? report.buckets.find((bucket) => bucket.id !== "weekly");
|
|
313
|
+
const selected = [subWindow, weekly].filter(
|
|
314
|
+
(bucket, index, buckets): bucket is UsageBucket => bucket !== undefined && buckets.indexOf(bucket) === index,
|
|
315
|
+
);
|
|
316
|
+
const parts = ["kimi"];
|
|
317
|
+
for (const bucket of selected) {
|
|
318
|
+
if (!bucket.limit || bucket.remaining === undefined) continue;
|
|
319
|
+
const fallback = bucket.id === "weekly" ? "weekly" : "5h";
|
|
320
|
+
parts.push(`${percentRemaining(bucket)}% ${formatWindowLabel(bucket.windowMinutes, fallback, true)}`);
|
|
321
|
+
}
|
|
322
|
+
return parts.length > 1 ? parts.join(" ") : undefined;
|
|
341
323
|
}
|
|
342
324
|
|
|
343
325
|
function formatMoonshotReport(lines: string[], report: UsageReport): void {
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
326
|
+
for (const metric of report.metrics) {
|
|
327
|
+
lines.push(`${`${metric.label}:`.padEnd(VALUE_COLUMN)}${metric.currency} ${metric.value}`);
|
|
328
|
+
}
|
|
347
329
|
}
|
|
348
330
|
|
|
349
331
|
function formatMoonshotStatusline(report: UsageReport): string {
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
332
|
+
const available = report.metrics.find((metric) => metric.id === "available-balance");
|
|
333
|
+
if (!available) return "moonshot balance unavailable";
|
|
334
|
+
return `moonshot ${available.currency ?? ""} ${available.value}`.replace(/\s+/gu, " ");
|
|
353
335
|
}
|
|
354
336
|
|
|
355
337
|
function formatMiniMaxReport(lines: string[], report: UsageReport): void {
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
338
|
+
if (report.source === "minimax-account-balance") {
|
|
339
|
+
for (const metric of report.metrics) {
|
|
340
|
+
lines.push(`${`${metric.label}:`.padEnd(VALUE_COLUMN)}${metric.currency} ${metric.value}`);
|
|
341
|
+
}
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
let previousGroup: string | undefined;
|
|
345
|
+
for (const bucket of report.buckets) {
|
|
346
|
+
if (bucket.groupId !== previousGroup) lines.push(`${bucket.groupLabel ?? "Token Plan"}:`);
|
|
347
|
+
previousGroup = bucket.groupId;
|
|
348
|
+
const reset = bucket.resetsAt ? ` (resets ${formatReset(bucket.resetsAt)})` : "";
|
|
349
|
+
const value =
|
|
350
|
+
bucket.period === "unlimited"
|
|
351
|
+
? "unlimited"
|
|
352
|
+
: bucket.unit === "percent" && bucket.remaining !== undefined
|
|
353
|
+
? `${bucket.remaining}% remaining${reset}`
|
|
354
|
+
: bucket.limit !== undefined && bucket.remaining !== undefined
|
|
355
|
+
? `${bucket.remaining} of ${bucket.limit} left · ${percentRemaining(bucket)}%${reset}`
|
|
356
|
+
: "unavailable";
|
|
357
|
+
lines.push(`${`${bucket.label}:`.padEnd(VALUE_COLUMN)}${value}`);
|
|
358
|
+
}
|
|
377
359
|
}
|
|
378
360
|
|
|
379
361
|
function formatMiniMaxStatusline(report: UsageReport, model?: UsageModel): string | undefined {
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
362
|
+
const prefix = report.providerId === "minimax-cn" ? "minimax cn" : "minimax";
|
|
363
|
+
if (report.source === "minimax-account-balance") {
|
|
364
|
+
const available = report.metrics.find((metric) => metric.id === "available-balance");
|
|
365
|
+
return available ? `${prefix} ${available.currency} ${available.value}` : undefined;
|
|
366
|
+
}
|
|
367
|
+
const selectedGroup = selectMiniMaxGroup(report, model);
|
|
368
|
+
if (!selectedGroup) return undefined;
|
|
369
|
+
const selected = report.buckets.filter((bucket) => bucket.groupId === selectedGroup);
|
|
370
|
+
const parts = [prefix];
|
|
371
|
+
for (const bucket of selected) {
|
|
372
|
+
const fallback = bucket.id.endsWith(":weekly") ? "weekly" : "5h";
|
|
373
|
+
const window = formatWindowLabel(bucket.windowMinutes, fallback, true);
|
|
374
|
+
if (bucket.period === "unlimited") {
|
|
375
|
+
parts.push(`unlimited ${window}`);
|
|
376
|
+
continue;
|
|
377
|
+
}
|
|
378
|
+
if (bucket.unit === "percent" && bucket.remaining !== undefined) {
|
|
379
|
+
parts.push(`${bucket.remaining}% ${window}`);
|
|
380
|
+
continue;
|
|
381
|
+
}
|
|
382
|
+
if (bucket.limit === undefined || bucket.remaining === undefined) continue;
|
|
383
|
+
parts.push(`${percentRemaining(bucket)}% ${window}`);
|
|
384
|
+
}
|
|
385
|
+
return parts.length > 1 ? parts.join(" ") : undefined;
|
|
404
386
|
}
|
|
405
387
|
|
|
406
388
|
function selectMiniMaxGroup(report: UsageReport, model?: UsageModel): string | undefined {
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
pattern.includes("*") && modelKeys.some((key) => wildcardKeyMatches(pattern, key)),
|
|
435
|
-
),
|
|
436
|
-
);
|
|
437
|
-
if (wildcard) return wildcard.group;
|
|
438
|
-
}
|
|
439
|
-
// Prefer the Coding Plan catch-all over hiding the chip.
|
|
440
|
-
return groups.find((group) => group === "general");
|
|
389
|
+
const groups = [
|
|
390
|
+
...new Set(report.buckets.map((bucket) => bucket.groupId).filter((group): group is string => group !== undefined)),
|
|
391
|
+
];
|
|
392
|
+
if (groups.length <= 1) return groups[0];
|
|
393
|
+
if (model && model.provider !== report.providerId) return undefined;
|
|
394
|
+
if (model) {
|
|
395
|
+
const modelKeys = [model.id, model.name]
|
|
396
|
+
.map(normalizeMiniMaxModelKey)
|
|
397
|
+
.filter((key): key is string => key !== undefined);
|
|
398
|
+
const candidates = groups.map((group) => {
|
|
399
|
+
const bucket = report.buckets.find((candidate) => candidate.groupId === group);
|
|
400
|
+
const patterns = [bucket?.groupLabel, ...(bucket?.modelKeys ?? []), group]
|
|
401
|
+
.map(normalizeMiniMaxModelKey)
|
|
402
|
+
.filter((key): key is string => key !== undefined);
|
|
403
|
+
return { group, patterns };
|
|
404
|
+
});
|
|
405
|
+
const exact = candidates.find(({ patterns }) =>
|
|
406
|
+
patterns.some((pattern) => !pattern.includes("*") && modelKeys.includes(pattern)),
|
|
407
|
+
);
|
|
408
|
+
if (exact) return exact.group;
|
|
409
|
+
const wildcard = candidates.find(({ patterns }) =>
|
|
410
|
+
patterns.some((pattern) => pattern.includes("*") && modelKeys.some((key) => wildcardKeyMatches(pattern, key))),
|
|
411
|
+
);
|
|
412
|
+
if (wildcard) return wildcard.group;
|
|
413
|
+
}
|
|
414
|
+
// Prefer the Coding Plan catch-all over hiding the chip.
|
|
415
|
+
return groups.find((group) => group === "general");
|
|
441
416
|
}
|
|
442
417
|
|
|
443
418
|
function normalizeMiniMaxModelKey(value: string | undefined): string | undefined {
|
|
444
|
-
|
|
445
|
-
|
|
419
|
+
const key = value?.toLowerCase().replace(/[^a-z0-9*]+/gu, "");
|
|
420
|
+
return key && /[a-z0-9]/u.test(key) ? key : undefined;
|
|
446
421
|
}
|
|
447
422
|
|
|
448
423
|
function wildcardKeyMatches(pattern: string, value: string): boolean {
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
424
|
+
if (!pattern.includes("*")) return pattern === value;
|
|
425
|
+
const segments = pattern.split("*").filter(Boolean);
|
|
426
|
+
let offset = 0;
|
|
427
|
+
for (const [index, segment] of segments.entries()) {
|
|
428
|
+
const found = value.indexOf(segment, offset);
|
|
429
|
+
if (found < 0 || (index === 0 && !pattern.startsWith("*") && found !== 0)) return false;
|
|
430
|
+
offset = found + segment.length;
|
|
431
|
+
}
|
|
432
|
+
const last = segments.at(-1);
|
|
433
|
+
return pattern.endsWith("*") || (last !== undefined && value.endsWith(last));
|
|
459
434
|
}
|
|
460
435
|
|
|
461
436
|
function formatZaiStatusline(report: UsageReport): string | undefined {
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
}
|
|
474
|
-
return parts.length > 1 ? parts.join(" ") : undefined;
|
|
437
|
+
const selected = [
|
|
438
|
+
report.buckets.find((bucket) => bucket.id === "five-hour"),
|
|
439
|
+
report.buckets.find((bucket) => bucket.id === "weekly"),
|
|
440
|
+
];
|
|
441
|
+
const parts = ["zai"];
|
|
442
|
+
for (const bucket of selected) {
|
|
443
|
+
if (!bucket?.limit || bucket.remaining === undefined) continue;
|
|
444
|
+
const fallback = bucket.id === "weekly" ? "weekly" : "5h";
|
|
445
|
+
parts.push(`${percentRemaining(bucket)}% ${formatWindowLabel(bucket.windowMinutes, fallback, true)}`);
|
|
446
|
+
}
|
|
447
|
+
return parts.length > 1 ? parts.join(" ") : undefined;
|
|
475
448
|
}
|
|
476
449
|
|
|
477
450
|
function formatCurrencyMetric(metric: UsageReport["metrics"][number]): string {
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
451
|
+
if (typeof metric.value !== "number") return String(metric.value);
|
|
452
|
+
if (!metric.currency) return "unavailable";
|
|
453
|
+
if (metric.currency === "USD") return `$${metric.value.toFixed(2)}`;
|
|
454
|
+
if (metric.currency === "CNY") return `¥${metric.value.toFixed(2)}`;
|
|
455
|
+
return `${metric.value.toFixed(2)} ${metric.currency}`;
|
|
483
456
|
}
|
|
484
457
|
|
|
485
458
|
function formatXaiReport(lines: string[], report: UsageReport): void {
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
`${`${metric.label}:`.padEnd(VALUE_COLUMN)}${formatMetricValue(metric.value, metric.unit)}`,
|
|
511
|
-
);
|
|
512
|
-
}
|
|
459
|
+
const included = report.buckets.find((bucket) => bucket.id === "included-allowance");
|
|
460
|
+
if (included) {
|
|
461
|
+
let value = "unavailable";
|
|
462
|
+
if (included.unit === "percent" && included.used !== undefined) {
|
|
463
|
+
value = formatPercentBar(included);
|
|
464
|
+
} else if (included.used !== undefined) {
|
|
465
|
+
value = `${formatUsd(included.used)} used`;
|
|
466
|
+
if (included.limit !== undefined) value += ` of ${formatUsd(included.limit)}`;
|
|
467
|
+
} else if (included.limit !== undefined) {
|
|
468
|
+
value = `usage unavailable · ${formatUsd(included.limit)} limit`;
|
|
469
|
+
}
|
|
470
|
+
const period = included.period ? ` · ${included.period}` : "";
|
|
471
|
+
const reset = included.resetsAt ? ` (resets ${formatReset(included.resetsAt)})` : "";
|
|
472
|
+
lines.push(`${"Included allowance:".padEnd(VALUE_COLUMN)}${value}${period}${reset}`);
|
|
473
|
+
}
|
|
474
|
+
const onDemand = report.buckets.find((bucket) => bucket.id === "on-demand");
|
|
475
|
+
if (onDemand) {
|
|
476
|
+
let value = onDemand.used === undefined ? "usage unavailable" : `${formatUsd(onDemand.used)} used`;
|
|
477
|
+
if (onDemand.limit !== undefined) value += ` of ${formatUsd(onDemand.limit)} cap`;
|
|
478
|
+
lines.push(`${"On-demand usage:".padEnd(VALUE_COLUMN)}${value}`);
|
|
479
|
+
}
|
|
480
|
+
for (const metric of report.metrics) {
|
|
481
|
+
lines.push(`${`${metric.label}:`.padEnd(VALUE_COLUMN)}${formatMetricValue(metric.value, metric.unit)}`);
|
|
482
|
+
}
|
|
513
483
|
}
|
|
514
484
|
|
|
515
485
|
function formatZaiReport(lines: string[], report: UsageReport): void {
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
);
|
|
537
|
-
}
|
|
486
|
+
for (const bucket of report.buckets) {
|
|
487
|
+
if (bucket.unit === "percent" && bucket.used !== undefined) {
|
|
488
|
+
lines.push(`${`${bucket.label}:`.padEnd(VALUE_COLUMN)}${formatPercentBucket(bucket)}`);
|
|
489
|
+
continue;
|
|
490
|
+
}
|
|
491
|
+
const reset = bucket.resetsAt ? ` (resets ${formatReset(bucket.resetsAt)})` : "";
|
|
492
|
+
let value = "unavailable";
|
|
493
|
+
if (bucket.used !== undefined && bucket.limit !== undefined) {
|
|
494
|
+
value = `${bucket.used} of ${bucket.limit} used`;
|
|
495
|
+
if (bucket.remaining !== undefined) value += ` · ${bucket.remaining} left`;
|
|
496
|
+
} else if (bucket.used !== undefined) {
|
|
497
|
+
value = `${bucket.used} used`;
|
|
498
|
+
} else if (bucket.remaining !== undefined) {
|
|
499
|
+
value = `${bucket.remaining} left`;
|
|
500
|
+
}
|
|
501
|
+
lines.push(`${`${bucket.label}:`.padEnd(VALUE_COLUMN)}${value}${reset}`);
|
|
502
|
+
}
|
|
503
|
+
for (const metric of report.metrics) {
|
|
504
|
+
lines.push(`${`${metric.label}:`.padEnd(VALUE_COLUMN)}${formatMetricValue(metric.value, metric.unit)}`);
|
|
505
|
+
}
|
|
538
506
|
}
|
|
539
507
|
|
|
540
508
|
function formatGenericReport(lines: string[], report: UsageReport): void {
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
);
|
|
550
|
-
}
|
|
509
|
+
for (const bucket of report.buckets) {
|
|
510
|
+
lines.push(
|
|
511
|
+
`${`${bucket.label}:`.padEnd(VALUE_COLUMN)}${formatMetricValue(bucket.remaining ?? bucket.used ?? "unavailable", bucket.unit)}`,
|
|
512
|
+
);
|
|
513
|
+
}
|
|
514
|
+
for (const metric of report.metrics) {
|
|
515
|
+
lines.push(`${`${metric.label}:`.padEnd(VALUE_COLUMN)}${formatMetricValue(metric.value, metric.unit)}`);
|
|
516
|
+
}
|
|
551
517
|
}
|
|
552
518
|
|
|
553
519
|
function formatCodexStatusline(
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
520
|
+
report: UsageReport,
|
|
521
|
+
model?: UsageModel,
|
|
522
|
+
now = Date.now(),
|
|
523
|
+
showResetCountdown = true,
|
|
558
524
|
): string | undefined {
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
}
|
|
578
|
-
return parts.length > 1 ? parts.join(" ") : formatCodexCreditsStatus(report);
|
|
525
|
+
const group = selectCodexGroup(report, model);
|
|
526
|
+
if (!group) return formatCodexCreditsStatus(report);
|
|
527
|
+
const buckets = report.buckets.filter((bucket) => (bucket.groupId ?? bucket.id) === group);
|
|
528
|
+
const labelBucket = buckets[0];
|
|
529
|
+
const parts = [group === "codex" ? "codex" : `codex ${compactLimitLabel(labelBucket?.groupLabel ?? group)}`];
|
|
530
|
+
for (const bucket of buckets) {
|
|
531
|
+
if (bucket.remaining === undefined) continue;
|
|
532
|
+
const percent = `${clampPercent(bucket.remaining).toFixed(0)}%`;
|
|
533
|
+
const fallback = bucket.id.endsWith(":secondary") ? "weekly" : "5h";
|
|
534
|
+
const window = formatWindowLabel(bucket.windowMinutes, fallback, true);
|
|
535
|
+
if (!showResetCountdown) {
|
|
536
|
+
parts.push(`${percent} ${window}`);
|
|
537
|
+
continue;
|
|
538
|
+
}
|
|
539
|
+
const reset = formatResetCountdown(bucket.resetsAt, now);
|
|
540
|
+
parts.push(`${percent} ${reset ? `↻ ${reset}` : window}`);
|
|
541
|
+
}
|
|
542
|
+
return parts.length > 1 ? parts.join(" ") : formatCodexCreditsStatus(report);
|
|
579
543
|
}
|
|
580
544
|
|
|
581
545
|
function formatCodexCreditsStatus(report: UsageReport): string {
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
546
|
+
const credits = report.metrics.find((metric) => metric.id === "credits");
|
|
547
|
+
if (!credits) return "codex usage unavailable";
|
|
548
|
+
if (credits.value === "none") return "codex no credits";
|
|
549
|
+
if (credits.value === "available") return "codex credits available";
|
|
550
|
+
if (credits.value === "unlimited") return "codex credits unlimited";
|
|
551
|
+
return `codex ${formatMetricValue(credits.value, "count")} credits`;
|
|
588
552
|
}
|
|
589
553
|
|
|
590
554
|
function selectCodexGroup(report: UsageReport, model?: UsageModel): string | undefined {
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
}
|
|
616
|
-
return groups.includes("codex") ? "codex" : groups[0];
|
|
555
|
+
const groups = [...new Set(report.buckets.map((bucket) => bucket.groupId ?? bucket.id))];
|
|
556
|
+
if (model?.provider !== "openai-codex") {
|
|
557
|
+
return groups.includes("codex") ? "codex" : groups[0];
|
|
558
|
+
}
|
|
559
|
+
const modelKeys = normalizedModelKeys(model);
|
|
560
|
+
for (const group of groups) {
|
|
561
|
+
const bucket = report.buckets.find((candidate) => (candidate.groupId ?? candidate.id) === group);
|
|
562
|
+
const keys = [group, bucket?.groupLabel, ...(bucket?.modelKeys ?? [])]
|
|
563
|
+
.map(normalizeKey)
|
|
564
|
+
.filter((key): key is string => key !== undefined);
|
|
565
|
+
if (keys.some((key) => modelKeys.has(key))) return group;
|
|
566
|
+
}
|
|
567
|
+
const variants = [...modelKeys]
|
|
568
|
+
.map((key) => key.match(/(?:^|-)codex-(.+)$/)?.[1])
|
|
569
|
+
.filter((value): value is string => Boolean(value));
|
|
570
|
+
for (const variant of variants) {
|
|
571
|
+
const matches = groups.filter((group) => {
|
|
572
|
+
if (group === "codex") return false;
|
|
573
|
+
const key = normalizeKey(group);
|
|
574
|
+
return key ? normalizedKeyHasToken(key, variant) : false;
|
|
575
|
+
});
|
|
576
|
+
if (matches.length === 1) return matches[0];
|
|
577
|
+
}
|
|
578
|
+
return groups.includes("codex") ? "codex" : groups[0];
|
|
617
579
|
}
|
|
618
580
|
|
|
619
581
|
function normalizedModelKeys(model: UsageModel): Set<string> {
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
582
|
+
const keys = new Set<string>();
|
|
583
|
+
for (const value of [model.id, model.name]) {
|
|
584
|
+
const key = normalizeKey(value);
|
|
585
|
+
if (!key) continue;
|
|
586
|
+
keys.add(key);
|
|
587
|
+
const index = key.indexOf("codex");
|
|
588
|
+
if (index >= 0) keys.add(key.slice(index));
|
|
589
|
+
}
|
|
590
|
+
return keys;
|
|
629
591
|
}
|
|
630
592
|
|
|
631
593
|
function normalizeKey(value: string | undefined): string | undefined {
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
594
|
+
const separated = value?.toLowerCase().replace(/[^a-z0-9]+/g, "-");
|
|
595
|
+
if (!separated) return undefined;
|
|
596
|
+
let start = 0;
|
|
597
|
+
let end = separated.length;
|
|
598
|
+
while (separated[start] === "-") start += 1;
|
|
599
|
+
while (end > start && separated[end - 1] === "-") end -= 1;
|
|
600
|
+
return separated.slice(start, end) || undefined;
|
|
639
601
|
}
|
|
640
602
|
|
|
641
603
|
function normalizedKeyHasToken(key: string, token: string): boolean {
|
|
642
|
-
|
|
643
|
-
key === token ||
|
|
644
|
-
key.startsWith(`${token}-`) ||
|
|
645
|
-
key.endsWith(`-${token}`) ||
|
|
646
|
-
key.includes(`-${token}-`)
|
|
647
|
-
);
|
|
604
|
+
return key === token || key.startsWith(`${token}-`) || key.endsWith(`-${token}`) || key.includes(`-${token}-`);
|
|
648
605
|
}
|
|
649
606
|
|
|
650
607
|
function compactLimitLabel(label: string): string {
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
608
|
+
const normalized = label.replace(/[_-]+/g, " ").trim();
|
|
609
|
+
const codex = /\bcodex\s/iu.exec(normalized);
|
|
610
|
+
const suffix = codex ? normalized.slice(codex.index + codex[0].length).trim() : "";
|
|
611
|
+
return (suffix || normalized).toLowerCase().replace(/\s+/g, " ");
|
|
655
612
|
}
|
|
656
613
|
|
|
657
614
|
function formatPercentBucket(bucket: UsageBucket): string {
|
|
658
|
-
|
|
615
|
+
return `${formatPercentBar(bucket)}${bucket.resetsAt ? ` (resets ${formatReset(bucket.resetsAt)})` : ""}`;
|
|
659
616
|
}
|
|
660
617
|
|
|
661
618
|
function formatPercentBar(bucket: UsageBucket): string {
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
}
|
|
666
|
-
|
|
667
|
-
function formatWindowLabel(
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
):
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
if (minutes % 10_080 === 0) return `${minutes / 10_080}w`;
|
|
677
|
-
if (minutes % 1_440 === 0) return `${minutes / 1_440}d`;
|
|
678
|
-
if (minutes % 60 === 0) return `${minutes / 60}h`;
|
|
679
|
-
return `${minutes}m`;
|
|
619
|
+
const remaining = clampPercent(bucket.remaining ?? 0);
|
|
620
|
+
const filled = Math.round((remaining / 100) * BAR_SEGMENTS);
|
|
621
|
+
return `[${"█".repeat(filled)}${"░".repeat(BAR_SEGMENTS - filled)}] ${remaining.toFixed(0)}% left`;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
function formatWindowLabel(minutes: number | undefined, fallback: "5h" | "weekly", compact: boolean): string {
|
|
625
|
+
if (!minutes || !Number.isFinite(minutes) || minutes <= 0) {
|
|
626
|
+
return compact && fallback === "weekly" ? "wk" : capitalize(fallback);
|
|
627
|
+
}
|
|
628
|
+
if (minutes === 10_080) return compact ? "wk" : "Weekly";
|
|
629
|
+
if (minutes % 10_080 === 0) return `${minutes / 10_080}w`;
|
|
630
|
+
if (minutes % 1_440 === 0) return `${minutes / 1_440}d`;
|
|
631
|
+
if (minutes % 60 === 0) return `${minutes / 60}h`;
|
|
632
|
+
return `${minutes}m`;
|
|
680
633
|
}
|
|
681
634
|
|
|
682
635
|
function formatResetCountdown(resetsAt: number | undefined, now: number): string | undefined {
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
.join("");
|
|
696
|
-
}
|
|
697
|
-
if (hours > 0)
|
|
698
|
-
return [`${String(hours)}h`, minutes > 0 ? `${String(minutes)}m` : ""].filter(Boolean).join("");
|
|
699
|
-
return `${String(minutes)}m`;
|
|
636
|
+
if (resetsAt === undefined || !Number.isFinite(resetsAt) || !Number.isFinite(now)) return undefined;
|
|
637
|
+
const totalMinutes = Math.max(0, Math.ceil((resetsAt * 1_000 - now) / 60_000));
|
|
638
|
+
const days = Math.floor(totalMinutes / 1_440);
|
|
639
|
+
const hours = Math.floor((totalMinutes % 1_440) / 60);
|
|
640
|
+
const minutes = totalMinutes % 60;
|
|
641
|
+
if (days > 0) {
|
|
642
|
+
return [`${String(days)}d`, hours > 0 ? `${String(hours)}h` : minutes > 0 ? `${String(minutes)}m` : ""]
|
|
643
|
+
.filter(Boolean)
|
|
644
|
+
.join("");
|
|
645
|
+
}
|
|
646
|
+
if (hours > 0) return [`${String(hours)}h`, minutes > 0 ? `${String(minutes)}m` : ""].filter(Boolean).join("");
|
|
647
|
+
return `${String(minutes)}m`;
|
|
700
648
|
}
|
|
701
649
|
|
|
702
650
|
function formatMetricValue(value: number | string, unit: UsageBucket["unit"] | undefined): string {
|
|
703
|
-
|
|
704
|
-
|
|
651
|
+
if (unit === "usd" && typeof value === "number") return formatUsd(value);
|
|
652
|
+
return String(value);
|
|
705
653
|
}
|
|
706
654
|
|
|
707
655
|
function formatUsd(value: number): string {
|
|
708
|
-
|
|
656
|
+
return `$${value.toFixed(2)}`;
|
|
709
657
|
}
|
|
710
658
|
|
|
711
659
|
function formatReset(epochSeconds: number): string {
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
const now = new Date();
|
|
719
|
-
if (reset.toDateString() === now.toDateString()) return time;
|
|
720
|
-
return `${time} on ${reset.getDate()} ${reset.toLocaleDateString(undefined, { month: "short" })}`;
|
|
660
|
+
const reset = new Date(epochSeconds * 1000);
|
|
661
|
+
if (Number.isNaN(reset.getTime())) return "at an unknown time";
|
|
662
|
+
const time = `${reset.getHours().toString().padStart(2, "0")}:${reset.getMinutes().toString().padStart(2, "0")}`;
|
|
663
|
+
const now = new Date();
|
|
664
|
+
if (reset.toDateString() === now.toDateString()) return time;
|
|
665
|
+
return `${time} on ${reset.getDate()} ${reset.toLocaleDateString(undefined, { month: "short" })}`;
|
|
721
666
|
}
|
|
722
667
|
|
|
723
668
|
function capitalize(value: string): string {
|
|
724
|
-
|
|
669
|
+
return `${value[0]?.toUpperCase() ?? ""}${value.slice(1)}`;
|
|
725
670
|
}
|
|
726
671
|
|
|
727
672
|
function clampPercent(value: number): number {
|
|
728
|
-
|
|
673
|
+
return Math.min(100, Math.max(0, value));
|
|
729
674
|
}
|