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