@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/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
- const stateLabel = displayState === "current" ? "Current" : "Configured";
14
- const title =
15
- report.providerId === "baseten"
16
- ? "Baseten Model APIs Spend"
17
- : report.providerId === "deepseek"
18
- ? "DeepSeek API Balance"
19
- : report.providerId === "fireworks"
20
- ? "Fireworks API Spend"
21
- : report.providerId === "vercel-ai-gateway"
22
- ? "Vercel AI Gateway Credits"
23
- : report.providerId === "moonshotai" || report.providerId === "moonshotai-cn"
24
- ? `${report.providerName} Balance`
25
- : report.providerId === "minimax" || report.providerId === "minimax-cn"
26
- ? report.source === "minimax-account-balance"
27
- ? `${report.providerName} API Balance`
28
- : `${report.providerName} Token Plan`
29
- : `${report.providerName} Usage`;
30
- const lines = [`${title} · ${stateLabel}`];
31
- if (report.accountLabel) lines.push(`Account: ${report.accountLabel}`);
32
- lines.push(`Semantics: ${report.semantics.label}`, "");
33
-
34
- if (report.providerId === "baseten") formatBasetenReport(lines, report);
35
- else if (report.providerId === "openai-codex") formatCodexReport(lines, report);
36
- else if (report.providerId === "deepseek") formatDeepSeekReport(lines, report);
37
- else if (report.providerId === "fireworks") formatFireworksReport(lines, report);
38
- else if (report.providerId === "vercel-ai-gateway") formatVercelAIGatewayReport(lines, report);
39
- else if (report.providerId === "github-copilot") formatGitHubCopilotReport(lines, report);
40
- else if (report.providerId === "openrouter") formatOpenRouterReport(lines, report);
41
- else if (report.providerId === "opencode-go") formatOpenCodeZenReport(lines, report);
42
- else if (report.providerId === "kimi-coding") formatKimiCodingReport(lines, report);
43
- else if (report.providerId === "moonshotai" || report.providerId === "moonshotai-cn") {
44
- formatMoonshotReport(lines, report);
45
- } else if (report.providerId === "minimax" || report.providerId === "minimax-cn") {
46
- formatMiniMaxReport(lines, report);
47
- } else if (report.providerId === "xai") formatXaiReport(lines, report);
48
- else if (report.providerId === "zai" || report.providerId === "zai-coding-cn") {
49
- formatZaiReport(lines, report);
50
- } else formatGenericReport(lines, report);
51
-
52
- if (report.notes) {
53
- for (const note of report.notes) lines.push(note);
54
- }
55
- return lines.join("\n").trimEnd();
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
- report: UsageReport,
60
- model?: UsageModel,
61
- now = Date.now(),
62
- showCodexResetCountdown = true,
53
+ report: UsageReport,
54
+ model?: UsageModel,
55
+ now = Date.now(),
56
+ showCodexResetCountdown = true,
63
57
  ): string | undefined {
64
- if (report.providerId === "baseten") return formatBasetenStatusline(report);
65
- if (report.providerId === "openai-codex") {
66
- return formatCodexStatusline(report, model, now, showCodexResetCountdown);
67
- }
68
- if (report.providerId === "deepseek") return formatDeepSeekStatusline(report);
69
- if (report.providerId === "fireworks") return formatFireworksStatusline(report);
70
- if (report.providerId === "vercel-ai-gateway") return formatVercelAIGatewayStatusline(report);
71
- if (report.providerId === "github-copilot") return formatGitHubCopilotStatusline(report);
72
- if (report.providerId === "openrouter") {
73
- const limit = report.buckets.find((bucket) => bucket.id === "key-limit");
74
- if (limit?.remaining !== undefined) return `openrouter ${formatUsd(limit.remaining)} left`;
75
- const total = report.metrics.find((metric) => metric.id === "usage-total");
76
- if (typeof total?.value === "number") return `openrouter ${formatUsd(total.value)} used`;
77
- }
78
- if (report.providerId === "opencode-go") return formatOpenCodeZenStatusline(report);
79
- if (report.providerId === "kimi-coding") return formatKimiCodingStatusline(report);
80
- if (report.providerId === "moonshotai" || report.providerId === "moonshotai-cn") {
81
- return formatMoonshotStatusline(report);
82
- }
83
- if (report.providerId === "minimax" || report.providerId === "minimax-cn") {
84
- return formatMiniMaxStatusline(report, model);
85
- }
86
- if (report.providerId === "zai" || report.providerId === "zai-coding-cn") {
87
- return formatZaiStatusline(report);
88
- }
89
- return undefined;
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
- return states
94
- .map((state) => {
95
- if (state.status === "ready") return formatUsageReport(state.report, state.displayState);
96
- const label = state.displayState === "current" ? "Current" : "Configured";
97
- if (state.status === "selection-required") {
98
- return `${state.providerName} · ${label}\nSelection required: choose this provider's ${state.singularLabel} by viewing it individually.`;
99
- }
100
- const status =
101
- state.status === "auth-unavailable"
102
- ? "Authentication unavailable"
103
- : state.status === "unsupported"
104
- ? "Unsupported"
105
- : "Query failed";
106
- return `${state.providerName} · ${label}\n${status}: ${state.message}`;
107
- })
108
- .join("\n\n");
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
- lines.push(`${"Spend window:".padEnd(VALUE_COLUMN)}Last 30 days`);
113
- for (const metric of report.metrics) {
114
- lines.push(`${`${metric.label}:`.padEnd(VALUE_COLUMN)}USD ${metric.value}`);
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
- const subtotal = report.metrics.find((metric) => metric.id === "net-subtotal");
120
- return subtotal ? `baseten USD ${subtotal.value} net` : "baseten no Model APIs usage";
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
- let previousGroup: string | undefined;
125
- for (const bucket of report.buckets) {
126
- const group = bucket.groupId ?? bucket.id;
127
- if (group !== previousGroup && group !== "codex") {
128
- lines.push(`${bucket.groupLabel ?? group} limit:`);
129
- }
130
- previousGroup = group;
131
- const fallback = bucket.id.endsWith(":secondary") ? "weekly" : "5h";
132
- const label = `${formatWindowLabel(bucket.windowMinutes, fallback, false)} limit:`;
133
- lines.push(`${label.padEnd(VALUE_COLUMN)}${formatPercentBucket(bucket)}`);
134
- }
135
- for (const metric of report.metrics) {
136
- if (metric.id === "reset-credits") {
137
- lines.push(`${"Usage limit resets:".padEnd(VALUE_COLUMN)}${metric.value} available`);
138
- } else if (metric.id === "credits") {
139
- lines.push(
140
- `${"Credits:".padEnd(VALUE_COLUMN)}${formatMetricValue(metric.value, metric.unit)}`,
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
- const availability = report.metrics.find((metric) => metric.id === "api-availability");
148
- lines.push(
149
- `${"API calls:".padEnd(VALUE_COLUMN)}${availability?.value === "available" ? "Available" : "Unavailable"}`,
150
- );
151
- for (const currency of ["CNY", "USD"]) {
152
- const metrics = report.metrics.filter((metric) => metric.currency === currency);
153
- if (metrics.length === 0) continue;
154
- lines.push("", `${currency} balance:`);
155
- for (const metric of metrics) {
156
- lines.push(`${`${metric.label}:`.padEnd(VALUE_COLUMN)}${currency} ${metric.value}`);
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
- const availability = report.metrics.find((metric) => metric.id === "api-availability");
163
- if (availability?.value !== "available") return "deepseek API unavailable";
164
- const totals = ["CNY", "USD"].flatMap((currency) => {
165
- const metric = report.metrics.find(
166
- (candidate) => candidate.id === `${currency.toLowerCase()}-total`,
167
- );
168
- return metric ? [`${currency} ${metric.value}`] : [];
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
- lines.push(`${"Spend window:".padEnd(VALUE_COLUMN)}Last 30 days (rated)`);
175
- for (const currency of fireworksCurrencies(report)) {
176
- lines.push("", `${currency} rated spend:`);
177
- for (const metric of report.metrics) {
178
- if (metric.currency !== currency) continue;
179
- lines.push(`${`${metric.label}:`.padEnd(VALUE_COLUMN)}${currency} ${metric.value}`);
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
- const totals = report.metrics.filter((metric) => metric.id.endsWith("-total"));
186
- if (totals.length === 0) return "fireworks no rated usage";
187
- return `fireworks ${totals.map((metric) => `${metric.currency} ${metric.value}`).join(" · ")}`;
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
- const currencies: string[] = [];
192
- for (const metric of report.metrics) {
193
- if (!metric.currency || currencies.includes(metric.currency)) continue;
194
- currencies.push(metric.currency);
195
- }
196
- return currencies;
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
- for (const metric of report.metrics) {
201
- lines.push(`${`${metric.label}:`.padEnd(VALUE_COLUMN)}USD ${metric.value}`);
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
- const balance = report.metrics.find((metric) => metric.id === "credit-balance");
207
- return balance ? `vercel USD ${balance.value} left` : "vercel credits unavailable";
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
- const quota = findGitHubCopilotQuota(report);
212
- if (!quota || quota.limit === undefined || quota.remaining === undefined) {
213
- lines.push(`${`${quota?.label ?? "Copilot quota"}:`.padEnd(VALUE_COLUMN)}unlimited`);
214
- return;
215
- }
216
- const percent = percentRemaining(quota);
217
- const reset = quota.resetsAt ? ` (resets ${formatReset(quota.resetsAt)})` : "";
218
- lines.push(
219
- `${`${quota.label}:`.padEnd(VALUE_COLUMN)}${quota.remaining} of ${quota.limit} left · ${percent}%${reset}`,
220
- );
221
- const overage = report.metrics.find((metric) => metric.id === "overage-used");
222
- if (typeof overage?.value === "number" && overage.value > 0) {
223
- lines.push(`${"Additional usage:".padEnd(VALUE_COLUMN)}${overage.value} ${quota.label}`);
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
- const quota = findGitHubCopilotQuota(report);
229
- const kind = compactGitHubCopilotQuotaKind(quota);
230
- if (!quota || quota.limit === undefined || quota.remaining === undefined) {
231
- return `copilot ${kind} unlimited`;
232
- }
233
- const overage = report.metrics.find((metric) => metric.id === "overage-used");
234
- const overageSuffix =
235
- typeof overage?.value === "number" && overage.value > 0 ? ` +${overage.value} over` : "";
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
- return report.buckets.find((bucket) =>
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
- if (bucket?.id === "ai-credits") return "credits";
247
- if (bucket?.id === "chat-requests") return "chat";
248
- return "premium";
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
- if (!bucket.limit || bucket.remaining === undefined) return 0;
253
- return Math.round(clampPercent((bucket.remaining / bucket.limit) * 100));
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
- const limit = report.buckets.find((bucket) => bucket.id === "key-limit");
258
- if (limit) {
259
- const period = limit.period ? ` (${limit.period})` : "";
260
- const value =
261
- limit.remaining === undefined
262
- ? `${formatUsd(limit.limit ?? 0)} cap; remaining unavailable`
263
- : `${formatUsd(limit.remaining)} of ${formatUsd(limit.limit ?? 0)} left`;
264
- lines.push(`${`Key limit${period}:`.padEnd(VALUE_COLUMN)}${value}`);
265
- }
266
- for (const metric of report.metrics) {
267
- lines.push(
268
- `${`${metric.label}:`.padEnd(VALUE_COLUMN)}${formatMetricValue(metric.value, metric.unit)}`,
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
- for (const bucket of report.buckets) {
275
- if (bucket.unit === "percent" && bucket.used !== undefined) {
276
- lines.push(`${`${bucket.label}:`.padEnd(VALUE_COLUMN)}${formatPercentBucket(bucket)}`);
277
- continue;
278
- }
279
- const reset = bucket.resetsAt ? ` (resets ${formatReset(bucket.resetsAt)})` : "";
280
- const used = bucket.used ?? "unavailable";
281
- lines.push(`${`${bucket.label}:`.padEnd(VALUE_COLUMN)}${used}% used${reset}`);
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
- const parts = ["zen"];
287
- for (const bucket of report.buckets) {
288
- if (bucket.used === undefined) continue;
289
- const compact = bucket.id === "rolling" ? "r" : bucket.id === "weekly" ? "w" : "m";
290
- parts.push(`${clampPercent(bucket.used).toFixed(0)}% ${compact}`);
291
- }
292
- return parts.length > 1 ? parts.join(" ") : undefined;
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
- for (const bucket of report.buckets) {
297
- const reset = bucket.resetsAt ? ` (resets ${formatReset(bucket.resetsAt)})` : "";
298
- if (bucket.used === undefined || bucket.limit === undefined) {
299
- lines.push(`${`${bucket.label}:`.padEnd(VALUE_COLUMN)}unavailable${reset}`);
300
- continue;
301
- }
302
- lines.push(
303
- `${`${bucket.label}:`.padEnd(VALUE_COLUMN)}${bucket.used} of ${bucket.limit} used · ${percentRemaining(bucket)}% left${reset}`,
304
- );
305
- }
306
- const balance = report.metrics.find((metric) => metric.id === "booster-balance");
307
- const total = report.metrics.find((metric) => metric.id === "booster-total");
308
- const monthlyUsed = report.metrics.find((metric) => metric.id === "booster-monthly-used");
309
- const monthlyLimit = report.metrics.find((metric) => metric.id === "booster-monthly-limit");
310
- if (!balance && !monthlyUsed && !monthlyLimit) return;
311
- lines.push("", "Extra usage wallet:");
312
- if (balance) {
313
- const totalSuffix = total ? ` of ${formatCurrencyMetric(total)}` : "";
314
- lines.push(`${"Balance:".padEnd(VALUE_COLUMN)}${formatCurrencyMetric(balance)}${totalSuffix}`);
315
- }
316
- if (monthlyUsed) {
317
- lines.push(`${"Used this month:".padEnd(VALUE_COLUMN)}${formatCurrencyMetric(monthlyUsed)}`);
318
- }
319
- if (monthlyLimit) {
320
- lines.push(`${"Monthly limit:".padEnd(VALUE_COLUMN)}${formatCurrencyMetric(monthlyLimit)}`);
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
- const fiveHour = report.buckets.find((bucket) => bucket.id === "five-hour");
326
- const weekly = report.buckets.find((bucket) => bucket.id === "weekly");
327
- const subWindow = fiveHour ?? report.buckets.find((bucket) => bucket.id !== "weekly");
328
- const selected = [subWindow, weekly].filter(
329
- (bucket, index, buckets): bucket is UsageBucket =>
330
- bucket !== undefined && buckets.indexOf(bucket) === index,
331
- );
332
- const parts = ["kimi"];
333
- for (const bucket of selected) {
334
- if (!bucket.limit || bucket.remaining === undefined) continue;
335
- const fallback = bucket.id === "weekly" ? "weekly" : "5h";
336
- parts.push(
337
- `${percentRemaining(bucket)}% ${formatWindowLabel(bucket.windowMinutes, fallback, true)}`,
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
- for (const metric of report.metrics) {
345
- lines.push(`${`${metric.label}:`.padEnd(VALUE_COLUMN)}${metric.currency} ${metric.value}`);
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
- const available = report.metrics.find((metric) => metric.id === "available-balance");
351
- if (!available) return "moonshot balance unavailable";
352
- return `moonshot ${available.currency ?? ""} ${available.value}`.replace(/\s+/gu, " ");
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
- if (report.source === "minimax-account-balance") {
357
- for (const metric of report.metrics) {
358
- lines.push(`${`${metric.label}:`.padEnd(VALUE_COLUMN)}${metric.currency} ${metric.value}`);
359
- }
360
- return;
361
- }
362
- let previousGroup: string | undefined;
363
- for (const bucket of report.buckets) {
364
- if (bucket.groupId !== previousGroup) lines.push(`${bucket.groupLabel ?? "Token Plan"}:`);
365
- previousGroup = bucket.groupId;
366
- const reset = bucket.resetsAt ? ` (resets ${formatReset(bucket.resetsAt)})` : "";
367
- const value =
368
- bucket.period === "unlimited"
369
- ? "unlimited"
370
- : bucket.unit === "percent" && bucket.remaining !== undefined
371
- ? `${bucket.remaining}% remaining${reset}`
372
- : bucket.limit !== undefined && bucket.remaining !== undefined
373
- ? `${bucket.remaining} of ${bucket.limit} left · ${percentRemaining(bucket)}%${reset}`
374
- : "unavailable";
375
- lines.push(`${`${bucket.label}:`.padEnd(VALUE_COLUMN)}${value}`);
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
- const prefix = report.providerId === "minimax-cn" ? "minimax cn" : "minimax";
381
- if (report.source === "minimax-account-balance") {
382
- const available = report.metrics.find((metric) => metric.id === "available-balance");
383
- return available ? `${prefix} ${available.currency} ${available.value}` : undefined;
384
- }
385
- const selectedGroup = selectMiniMaxGroup(report, model);
386
- if (!selectedGroup) return undefined;
387
- const selected = report.buckets.filter((bucket) => bucket.groupId === selectedGroup);
388
- const parts = [prefix];
389
- for (const bucket of selected) {
390
- const fallback = bucket.id.endsWith(":weekly") ? "weekly" : "5h";
391
- const window = formatWindowLabel(bucket.windowMinutes, fallback, true);
392
- if (bucket.period === "unlimited") {
393
- parts.push(`unlimited ${window}`);
394
- continue;
395
- }
396
- if (bucket.unit === "percent" && bucket.remaining !== undefined) {
397
- parts.push(`${bucket.remaining}% ${window}`);
398
- continue;
399
- }
400
- if (bucket.limit === undefined || bucket.remaining === undefined) continue;
401
- parts.push(`${percentRemaining(bucket)}% ${window}`);
402
- }
403
- return parts.length > 1 ? parts.join(" ") : undefined;
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
- const groups = [
408
- ...new Set(
409
- report.buckets
410
- .map((bucket) => bucket.groupId)
411
- .filter((group): group is string => group !== undefined),
412
- ),
413
- ];
414
- if (groups.length <= 1) return groups[0];
415
- if (model && model.provider !== report.providerId) return undefined;
416
- if (model) {
417
- const modelKeys = [model.id, model.name]
418
- .map(normalizeMiniMaxModelKey)
419
- .filter((key): key is string => key !== undefined);
420
- const candidates = groups.map((group) => {
421
- const bucket = report.buckets.find((candidate) => candidate.groupId === group);
422
- const patterns = [bucket?.groupLabel, ...(bucket?.modelKeys ?? []), group]
423
- .map(normalizeMiniMaxModelKey)
424
- .filter((key): key is string => key !== undefined);
425
- return { group, patterns };
426
- });
427
- const exact = candidates.find(({ patterns }) =>
428
- patterns.some((pattern) => !pattern.includes("*") && modelKeys.includes(pattern)),
429
- );
430
- if (exact) return exact.group;
431
- const wildcard = candidates.find(({ patterns }) =>
432
- patterns.some(
433
- (pattern) =>
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
- const key = value?.toLowerCase().replace(/[^a-z0-9*]+/gu, "");
445
- return key && /[a-z0-9]/u.test(key) ? key : undefined;
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
- if (!pattern.includes("*")) return pattern === value;
450
- const segments = pattern.split("*").filter(Boolean);
451
- let offset = 0;
452
- for (const [index, segment] of segments.entries()) {
453
- const found = value.indexOf(segment, offset);
454
- if (found < 0 || (index === 0 && !pattern.startsWith("*") && found !== 0)) return false;
455
- offset = found + segment.length;
456
- }
457
- const last = segments.at(-1);
458
- return pattern.endsWith("*") || (last !== undefined && value.endsWith(last));
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
- const selected = [
463
- report.buckets.find((bucket) => bucket.id === "five-hour"),
464
- report.buckets.find((bucket) => bucket.id === "weekly"),
465
- ];
466
- const parts = ["zai"];
467
- for (const bucket of selected) {
468
- if (!bucket?.limit || bucket.remaining === undefined) continue;
469
- const fallback = bucket.id === "weekly" ? "weekly" : "5h";
470
- parts.push(
471
- `${percentRemaining(bucket)}% ${formatWindowLabel(bucket.windowMinutes, fallback, true)}`,
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
- if (typeof metric.value !== "number") return String(metric.value);
479
- if (!metric.currency) return "unavailable";
480
- if (metric.currency === "USD") return `$${metric.value.toFixed(2)}`;
481
- if (metric.currency === "CNY") return `¥${metric.value.toFixed(2)}`;
482
- return `${metric.value.toFixed(2)} ${metric.currency}`;
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
- const included = report.buckets.find((bucket) => bucket.id === "included-allowance");
487
- if (included) {
488
- let value = "unavailable";
489
- if (included.unit === "percent" && included.used !== undefined) {
490
- value = formatPercentBar(included);
491
- } else if (included.used !== undefined) {
492
- value = `${formatUsd(included.used)} used`;
493
- if (included.limit !== undefined) value += ` of ${formatUsd(included.limit)}`;
494
- } else if (included.limit !== undefined) {
495
- value = `usage unavailable · ${formatUsd(included.limit)} limit`;
496
- }
497
- const period = included.period ? ` · ${included.period}` : "";
498
- const reset = included.resetsAt ? ` (resets ${formatReset(included.resetsAt)})` : "";
499
- lines.push(`${"Included allowance:".padEnd(VALUE_COLUMN)}${value}${period}${reset}`);
500
- }
501
- const onDemand = report.buckets.find((bucket) => bucket.id === "on-demand");
502
- if (onDemand) {
503
- let value =
504
- onDemand.used === undefined ? "usage unavailable" : `${formatUsd(onDemand.used)} used`;
505
- if (onDemand.limit !== undefined) value += ` of ${formatUsd(onDemand.limit)} cap`;
506
- lines.push(`${"On-demand usage:".padEnd(VALUE_COLUMN)}${value}`);
507
- }
508
- for (const metric of report.metrics) {
509
- lines.push(
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
- for (const bucket of report.buckets) {
517
- if (bucket.unit === "percent" && bucket.used !== undefined) {
518
- lines.push(`${`${bucket.label}:`.padEnd(VALUE_COLUMN)}${formatPercentBucket(bucket)}`);
519
- continue;
520
- }
521
- const reset = bucket.resetsAt ? ` (resets ${formatReset(bucket.resetsAt)})` : "";
522
- let value = "unavailable";
523
- if (bucket.used !== undefined && bucket.limit !== undefined) {
524
- value = `${bucket.used} of ${bucket.limit} used`;
525
- if (bucket.remaining !== undefined) value += ` · ${bucket.remaining} left`;
526
- } else if (bucket.used !== undefined) {
527
- value = `${bucket.used} used`;
528
- } else if (bucket.remaining !== undefined) {
529
- value = `${bucket.remaining} left`;
530
- }
531
- lines.push(`${`${bucket.label}:`.padEnd(VALUE_COLUMN)}${value}${reset}`);
532
- }
533
- for (const metric of report.metrics) {
534
- lines.push(
535
- `${`${metric.label}:`.padEnd(VALUE_COLUMN)}${formatMetricValue(metric.value, metric.unit)}`,
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
- for (const bucket of report.buckets) {
542
- lines.push(
543
- `${`${bucket.label}:`.padEnd(VALUE_COLUMN)}${formatMetricValue(bucket.remaining ?? bucket.used ?? "unavailable", bucket.unit)}`,
544
- );
545
- }
546
- for (const metric of report.metrics) {
547
- lines.push(
548
- `${`${metric.label}:`.padEnd(VALUE_COLUMN)}${formatMetricValue(metric.value, metric.unit)}`,
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
- report: UsageReport,
555
- model?: UsageModel,
556
- now = Date.now(),
557
- showResetCountdown = true,
520
+ report: UsageReport,
521
+ model?: UsageModel,
522
+ now = Date.now(),
523
+ showResetCountdown = true,
558
524
  ): string | undefined {
559
- const group = selectCodexGroup(report, model);
560
- if (!group) return formatCodexCreditsStatus(report);
561
- const buckets = report.buckets.filter((bucket) => (bucket.groupId ?? bucket.id) === group);
562
- const labelBucket = buckets[0];
563
- const parts = [
564
- group === "codex" ? "codex" : `codex ${compactLimitLabel(labelBucket?.groupLabel ?? group)}`,
565
- ];
566
- for (const bucket of buckets) {
567
- if (bucket.remaining === undefined) continue;
568
- const percent = `${clampPercent(bucket.remaining).toFixed(0)}%`;
569
- const fallback = bucket.id.endsWith(":secondary") ? "weekly" : "5h";
570
- const window = formatWindowLabel(bucket.windowMinutes, fallback, true);
571
- if (!showResetCountdown) {
572
- parts.push(`${percent} ${window}`);
573
- continue;
574
- }
575
- const reset = formatResetCountdown(bucket.resetsAt, now);
576
- parts.push(`${percent} ${reset ? `↻ ${reset}` : window}`);
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
- const credits = report.metrics.find((metric) => metric.id === "credits");
583
- if (!credits) return "codex usage unavailable";
584
- if (credits.value === "none") return "codex no credits";
585
- if (credits.value === "available") return "codex credits available";
586
- if (credits.value === "unlimited") return "codex credits unlimited";
587
- return `codex ${formatMetricValue(credits.value, "count")} credits`;
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
- const groups = [...new Set(report.buckets.map((bucket) => bucket.groupId ?? bucket.id))];
592
- if (model?.provider !== "openai-codex") {
593
- return groups.includes("codex") ? "codex" : groups[0];
594
- }
595
- const modelKeys = normalizedModelKeys(model);
596
- for (const group of groups) {
597
- const bucket = report.buckets.find(
598
- (candidate) => (candidate.groupId ?? candidate.id) === group,
599
- );
600
- const keys = [group, bucket?.groupLabel, ...(bucket?.modelKeys ?? [])]
601
- .map(normalizeKey)
602
- .filter((key): key is string => key !== undefined);
603
- if (keys.some((key) => modelKeys.has(key))) return group;
604
- }
605
- const variants = [...modelKeys]
606
- .map((key) => key.match(/(?:^|-)codex-(.+)$/)?.[1])
607
- .filter((value): value is string => Boolean(value));
608
- for (const variant of variants) {
609
- const matches = groups.filter((group) => {
610
- if (group === "codex") return false;
611
- const key = normalizeKey(group);
612
- return key ? normalizedKeyHasToken(key, variant) : false;
613
- });
614
- if (matches.length === 1) return matches[0];
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
- const keys = new Set<string>();
621
- for (const value of [model.id, model.name]) {
622
- const key = normalizeKey(value);
623
- if (!key) continue;
624
- keys.add(key);
625
- const index = key.indexOf("codex");
626
- if (index >= 0) keys.add(key.slice(index));
627
- }
628
- return keys;
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
- const separated = value?.toLowerCase().replace(/[^a-z0-9]+/g, "-");
633
- if (!separated) return undefined;
634
- let start = 0;
635
- let end = separated.length;
636
- while (separated[start] === "-") start += 1;
637
- while (end > start && separated[end - 1] === "-") end -= 1;
638
- return separated.slice(start, end) || undefined;
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
- return (
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
- const normalized = label.replace(/[_-]+/g, " ").trim();
652
- const codex = /\bcodex\s/iu.exec(normalized);
653
- const suffix = codex ? normalized.slice(codex.index + codex[0].length).trim() : "";
654
- return (suffix || normalized).toLowerCase().replace(/\s+/g, " ");
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
- return `${formatPercentBar(bucket)}${bucket.resetsAt ? ` (resets ${formatReset(bucket.resetsAt)})` : ""}`;
615
+ return `${formatPercentBar(bucket)}${bucket.resetsAt ? ` (resets ${formatReset(bucket.resetsAt)})` : ""}`;
659
616
  }
660
617
 
661
618
  function formatPercentBar(bucket: UsageBucket): string {
662
- const remaining = clampPercent(bucket.remaining ?? 0);
663
- const filled = Math.round((remaining / 100) * BAR_SEGMENTS);
664
- return `[${"█".repeat(filled)}${"░".repeat(BAR_SEGMENTS - filled)}] ${remaining.toFixed(0)}% left`;
665
- }
666
-
667
- function formatWindowLabel(
668
- minutes: number | undefined,
669
- fallback: "5h" | "weekly",
670
- compact: boolean,
671
- ): string {
672
- if (!minutes || !Number.isFinite(minutes) || minutes <= 0) {
673
- return compact && fallback === "weekly" ? "wk" : capitalize(fallback);
674
- }
675
- if (minutes === 10_080) return compact ? "wk" : "Weekly";
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
- if (resetsAt === undefined || !Number.isFinite(resetsAt) || !Number.isFinite(now))
684
- return undefined;
685
- const totalMinutes = Math.max(0, Math.ceil((resetsAt * 1_000 - now) / 60_000));
686
- const days = Math.floor(totalMinutes / 1_440);
687
- const hours = Math.floor((totalMinutes % 1_440) / 60);
688
- const minutes = totalMinutes % 60;
689
- if (days > 0) {
690
- return [
691
- `${String(days)}d`,
692
- hours > 0 ? `${String(hours)}h` : minutes > 0 ? `${String(minutes)}m` : "",
693
- ]
694
- .filter(Boolean)
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
- if (unit === "usd" && typeof value === "number") return formatUsd(value);
704
- return String(value);
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
- return `$${value.toFixed(2)}`;
656
+ return `$${value.toFixed(2)}`;
709
657
  }
710
658
 
711
659
  function formatReset(epochSeconds: number): string {
712
- const reset = new Date(epochSeconds * 1000);
713
- if (Number.isNaN(reset.getTime())) return "at an unknown time";
714
- const time = `${reset.getHours().toString().padStart(2, "0")}:${reset
715
- .getMinutes()
716
- .toString()
717
- .padStart(2, "0")}`;
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
- return `${value[0]?.toUpperCase() ?? ""}${value.slice(1)}`;
669
+ return `${value[0]?.toUpperCase() ?? ""}${value.slice(1)}`;
725
670
  }
726
671
 
727
672
  function clampPercent(value: number): number {
728
- return Math.min(100, Math.max(0, value));
673
+ return Math.min(100, Math.max(0, value));
729
674
  }