@narumitw/pi-usage 0.65.0 → 0.65.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -1
- package/dist/index.ts +67 -39
- package/dist/index.ts.map +2 -2
- package/docs/providers.md +7 -7
- package/package.json +1 -1
- package/src/format.ts +96 -34
- package/src/usage.ts +14 -12
package/docs/providers.md
CHANGED
|
@@ -81,7 +81,7 @@ Contract fixtures derive from [Specode's pinned implementation](https://github.c
|
|
|
81
81
|
- Source: the Codex usage and earned-reset endpoints using Pi's resolved runtime authorization
|
|
82
82
|
- Displayed data: returned duration-based windows, resets, credits, earned usage-limit resets, and additional model buckets
|
|
83
83
|
- Reset mutation: `POST /wham/rate-limit-reset-credits/consume` with a unique redemption request ID and, when available, the selected opaque credit ID
|
|
84
|
-
- Statusline examples: `codex 59% ↻ 2h30m 61% ↻ 2d15m`, `codex fast 59% ↻ 2h30m`, or `codex spark 100% ↻ 2h30m`. Set `codexStatusPercentage` to `"used"` to show consumed percentages, or set `codexStatusResetCountdown` to `false` for
|
|
84
|
+
- Statusline examples: `codex 59% ↻ 2h30m · 61% ↻ 2d15m`, `codex fast 59% ↻ 2h30m`, or `codex spark 100% ↻ 2h30m`. Set `codexStatusPercentage` to `"used"` to show consumed percentages, or set `codexStatusResetCountdown` to `false` for static labels such as `codex 5h:59% · wk:61%`.
|
|
85
85
|
|
|
86
86
|
The statusline selects a returned bucket that matches the current Codex model when one is available.
|
|
87
87
|
The percentage preference affects only this compact status and defaults to remaining quota; it does not change the detailed report or returned usage data.
|
|
@@ -101,7 +101,7 @@ Opaque credit and account IDs are never shown or persisted by the extension.
|
|
|
101
101
|
- Source: `GET https://api.kimi.com/coding/v1/usages` using Pi's freshly resolved runtime Bearer credential
|
|
102
102
|
- Displayed plan data: count-based weekly summaries and returned sub-windows, plus reported ratio-based five-hour, weekly, or monthly quotas and valid reset times
|
|
103
103
|
- Displayed wallet data: balance, monthly spend, and monthly charge limit when the wallet supplies consistent currency and amounts
|
|
104
|
-
- Statusline examples: `kimi
|
|
104
|
+
- Statusline examples: `kimi 77% ↻ 2h52m · 87% ↻ 29d20h`, `kimi 77% ↻ 2h52m · mo:87%`, or static fallback `kimi 1d:95%`
|
|
105
105
|
|
|
106
106
|
Both Pi API-key credentials and Pi OAuth credentials are accepted because current Pi resolves each form as Bearer authorization for the same official Kimi inference origin.
|
|
107
107
|
The extension queries the fixed usage endpoint only when both the selected model origin and the effective resolved-auth origin are `https://api.kimi.com`.
|
|
@@ -148,7 +148,7 @@ The contracts were verified on 2026-08-30 against the official [Global balance r
|
|
|
148
148
|
- Token Plan source: `GET {region-api-root}/v1/token_plan/remains`
|
|
149
149
|
- Pay-as-you-go source: `GET {region-api-root}/account/query_balance`
|
|
150
150
|
- Region API roots: `https://api.minimax.io` and `https://api.minimaxi.com`
|
|
151
|
-
- Statusline examples: `minimax 15%
|
|
151
|
+
- Statusline examples: `minimax 15% ↻ 2h30m · 80% ↻ 6d`, static fallback `minimax 5h:15% · wk:80%`, unlimited `minimax cn 5h:unlimited · wk:unlimited`, or pay-as-you-go `minimax USD 98.00001`
|
|
152
152
|
|
|
153
153
|
Pi's resolved MiniMax API key selects exactly one endpoint before network access.
|
|
154
154
|
Keys with the first-party `sk-api-` prefix query pay-as-you-go balance; other MiniMax API keys query Token Plan quota.
|
|
@@ -166,7 +166,7 @@ The contract was verified on 2026-08-30 against MiniMax's [Token Plan FAQ](https
|
|
|
166
166
|
- Allowance labels: AI credits for usage-based billing, premium requests for legacy annual billing, or chat requests for Copilot Free
|
|
167
167
|
- Source: GitHub's undocumented `GET /copilot_internal/user` endpoint
|
|
168
168
|
- Displayed data: entitlement, remaining allowance, percentage, reset time, plan, and any additional usage beyond the included allowance
|
|
169
|
-
- Statusline examples: `copilot credits 1200/1500 80
|
|
169
|
+
- Statusline examples: `copilot credits 1200/1500 80% ↻ 6d`, `copilot 245/300 82% ↻ 6d`, or static fallback `copilot chat 40/50 80%`
|
|
170
170
|
|
|
171
171
|
GitHub's quota endpoint requires the original GitHub OAuth token rather than the short-lived Copilot inference token exposed by runtime auth.
|
|
172
172
|
`pi-usage` supports Copilot accounts created through Pi's `/login` flow and named accounts offered by a compatible `oauth:credential-source:v1` owner.
|
|
@@ -260,7 +260,7 @@ The contract was verified on 2026-08-30 against Baseten's [Billing and usage](ht
|
|
|
260
260
|
- Source: `GET https://opencode.ai/zen/go/v1/usage` using Pi's resolved inference API key
|
|
261
261
|
- Displayed data: used percentage and reset time for each window
|
|
262
262
|
- Status handling: `rate-limited` windows remain visible, while unknown statuses become unavailable notes
|
|
263
|
-
- Statusline examples: `zen 0%
|
|
263
|
+
- Statusline examples: `zen 0% ↻ 2h30m · 4% ↻ 6d · m:2%` or static fallback `zen r:0% · w:4% · m:2%`
|
|
264
264
|
|
|
265
265
|
The fixed endpoint is queried only when the OpenCode Go model uses the official `https://opencode.ai` origin.
|
|
266
266
|
When resolved provider auth includes a base URL, that URL must use the same origin.
|
|
@@ -275,7 +275,7 @@ Other origins fail before the credential is sent.
|
|
|
275
275
|
- Plan route: `GET https://api.commandcode.ai/alpha/billing/subscriptions`
|
|
276
276
|
- Period route: `GET https://api.commandcode.ai/alpha/usage/summary`
|
|
277
277
|
- Displayed data: five-hour and weekly spend caps in USD with reset times, the billing-period credit pool and remaining balance, plan name and status, request and token totals, and purchased or free credits when present
|
|
278
|
-
- Statusline examples: `cmd 99%
|
|
278
|
+
- Statusline examples: `cmd 99% ↻ 2h30m · 92% ↻ 6d · 96% ↻ 20d` or static fallback `cmd 5h:99% · wk:92% · mo:96%`
|
|
279
279
|
|
|
280
280
|
The official `@commandcode/pi-commandcode-provider` extension registers this provider through `pi.registerProvider()`, so `command-code` is not a Pi built-in provider.
|
|
281
281
|
Pi still resolves its stored `/login` API key through the normal provider registry, and the extension sends it as Bearer authorization only when the selected model and resolved auth share the official `https://api.commandcode.ai` origin.
|
|
@@ -329,7 +329,7 @@ xAI identity and billing requests occur only after an explicit current, configur
|
|
|
329
329
|
- Allowed origins: the model base URL must resolve to `https://api.z.ai` or `https://open.bigmodel.cn`
|
|
330
330
|
- Displayed data: explicit used and remaining values, reset times, provider-reported per-tool MCP details, and the plan name with its renewal date
|
|
331
331
|
- Percentage-only windows remain percent-based
|
|
332
|
-
- Statusline: publishes remaining plan percentages such as `zai 87%
|
|
332
|
+
- Statusline: publishes remaining plan percentages such as `zai 87% ↻ 2h30m · 76% ↻ 6d`, with static fallback `zai 5h:87% · wk:76%`; monthly MCP details remain available through `/usage`
|
|
333
333
|
|
|
334
334
|
The monitor endpoint is not a published API contract and may return legacy `TOKENS_LIMIT` or newer `CREDIT_LIMIT` window names.
|
|
335
335
|
The extension classifies both forms by the provider's window unit and does not label provider-reported counts as tokens or calls.
|
package/package.json
CHANGED
package/src/format.ts
CHANGED
|
@@ -60,6 +60,36 @@ export function formatUsageStatusline(
|
|
|
60
60
|
now = Date.now(),
|
|
61
61
|
showCodexResetCountdown = true,
|
|
62
62
|
codexStatusPercentage: CodexStatusPercentage = "remaining",
|
|
63
|
+
): string | undefined {
|
|
64
|
+
return formatUsageStatuslineWithCountdown(report, model, now, showCodexResetCountdown, codexStatusPercentage).value;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
interface StatusCountdown {
|
|
68
|
+
now: number;
|
|
69
|
+
active: boolean;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Record new-provider eligibility while formatting the selected windows so repaint cannot drift from display policy.
|
|
73
|
+
// Codex and ChatGPT companion keep their existing repaint policy in usage.ts.
|
|
74
|
+
export function formatUsageStatuslineWithCountdown(
|
|
75
|
+
report: UsageReport,
|
|
76
|
+
model?: UsageModel,
|
|
77
|
+
now = Date.now(),
|
|
78
|
+
showCodexResetCountdown = true,
|
|
79
|
+
codexStatusPercentage: CodexStatusPercentage = "remaining",
|
|
80
|
+
): { value: string | undefined; hasCountdown: boolean } {
|
|
81
|
+
const countdown: StatusCountdown = { now, active: false };
|
|
82
|
+
const value = formatStatusline(report, model, now, showCodexResetCountdown, codexStatusPercentage, countdown);
|
|
83
|
+
return { value, hasCountdown: countdown.active };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function formatStatusline(
|
|
87
|
+
report: UsageReport,
|
|
88
|
+
model: UsageModel | undefined,
|
|
89
|
+
now: number,
|
|
90
|
+
showCodexResetCountdown: boolean,
|
|
91
|
+
codexStatusPercentage: CodexStatusPercentage,
|
|
92
|
+
countdown: StatusCountdown,
|
|
63
93
|
): string | undefined {
|
|
64
94
|
if (report.providerId === "openai" && report.source === "openai-chatgpt-auth") return "chatgpt usage: web only";
|
|
65
95
|
if (report.source === "openai-chatgpt-companion") {
|
|
@@ -79,24 +109,24 @@ export function formatUsageStatusline(
|
|
|
79
109
|
if (report.providerId === "deepseek") return formatDeepSeekStatusline(report);
|
|
80
110
|
if (report.providerId === "fireworks") return formatFireworksStatusline(report);
|
|
81
111
|
if (report.providerId === "vercel-ai-gateway") return formatVercelAIGatewayStatusline(report);
|
|
82
|
-
if (report.providerId === "github-copilot") return formatGitHubCopilotStatusline(report);
|
|
112
|
+
if (report.providerId === "github-copilot") return formatGitHubCopilotStatusline(report, countdown);
|
|
83
113
|
if (report.providerId === "openrouter") {
|
|
84
114
|
const limit = report.buckets.find((bucket) => bucket.id === "key-limit");
|
|
85
115
|
if (limit?.remaining !== undefined) return `openrouter ${formatUsd(limit.remaining)} left`;
|
|
86
116
|
const total = report.metrics.find((metric) => metric.id === "usage-total");
|
|
87
117
|
if (typeof total?.value === "number") return `openrouter ${formatUsd(total.value)} used`;
|
|
88
118
|
}
|
|
89
|
-
if (report.providerId === "opencode-go") return formatOpenCodeZenStatusline(report);
|
|
90
|
-
if (report.providerId === "command-code") return formatCommandCodeStatusline(report);
|
|
91
|
-
if (report.providerId === "kimi-coding") return formatKimiCodingStatusline(report);
|
|
119
|
+
if (report.providerId === "opencode-go") return formatOpenCodeZenStatusline(report, countdown);
|
|
120
|
+
if (report.providerId === "command-code") return formatCommandCodeStatusline(report, countdown);
|
|
121
|
+
if (report.providerId === "kimi-coding") return formatKimiCodingStatusline(report, countdown);
|
|
92
122
|
if (report.providerId === "moonshotai" || report.providerId === "moonshotai-cn") {
|
|
93
123
|
return formatMoonshotStatusline(report);
|
|
94
124
|
}
|
|
95
125
|
if (report.providerId === "minimax" || report.providerId === "minimax-cn") {
|
|
96
|
-
return formatMiniMaxStatusline(report, model);
|
|
126
|
+
return formatMiniMaxStatusline(report, countdown, model);
|
|
97
127
|
}
|
|
98
128
|
if (report.providerId === "zai" || report.providerId === "zai-coding-cn") {
|
|
99
|
-
return formatZaiStatusline(report);
|
|
129
|
+
return formatZaiStatusline(report, countdown);
|
|
100
130
|
}
|
|
101
131
|
return undefined;
|
|
102
132
|
}
|
|
@@ -232,7 +262,7 @@ function formatGitHubCopilotReport(lines: string[], report: UsageReport): void {
|
|
|
232
262
|
}
|
|
233
263
|
}
|
|
234
264
|
|
|
235
|
-
function formatGitHubCopilotStatusline(report: UsageReport): string {
|
|
265
|
+
function formatGitHubCopilotStatusline(report: UsageReport, countdown: StatusCountdown): string {
|
|
236
266
|
const quota = findGitHubCopilotQuota(report);
|
|
237
267
|
const kind = compactGitHubCopilotQuotaKind(quota);
|
|
238
268
|
if (!quota || quota.limit === undefined || quota.remaining === undefined) {
|
|
@@ -240,7 +270,8 @@ function formatGitHubCopilotStatusline(report: UsageReport): string {
|
|
|
240
270
|
}
|
|
241
271
|
const overage = report.metrics.find((metric) => metric.id === "overage-used");
|
|
242
272
|
const overageSuffix = typeof overage?.value === "number" && overage.value > 0 ? ` +${overage.value} over` : "";
|
|
243
|
-
|
|
273
|
+
const reset = formatQuotaCountdown(quota, countdown);
|
|
274
|
+
return `copilot ${kind === "premium" ? "" : `${kind} `}${quota.remaining}/${quota.limit} ${percentRemaining(quota)}%${overageSuffix}${reset ? ` ↻ ${reset}` : ""}`;
|
|
244
275
|
}
|
|
245
276
|
|
|
246
277
|
function findGitHubCopilotQuota(report: UsageReport): UsageBucket | undefined {
|
|
@@ -285,14 +316,14 @@ function formatOpenCodeZenReport(lines: string[], report: UsageReport): void {
|
|
|
285
316
|
}
|
|
286
317
|
}
|
|
287
318
|
|
|
288
|
-
function formatOpenCodeZenStatusline(report: UsageReport): string | undefined {
|
|
289
|
-
const parts = [
|
|
319
|
+
function formatOpenCodeZenStatusline(report: UsageReport, countdown: StatusCountdown): string | undefined {
|
|
320
|
+
const parts: string[] = [];
|
|
290
321
|
for (const bucket of report.buckets) {
|
|
291
322
|
if (bucket.used === undefined) continue;
|
|
292
323
|
const compact = bucket.id === "rolling" ? "r" : bucket.id === "weekly" ? "w" : "m";
|
|
293
|
-
parts.push(`${clampPercent(bucket.used).toFixed(0)}
|
|
324
|
+
parts.push(formatQuotaWindow(bucket, `${clampPercent(bucket.used).toFixed(0)}%`, compact, countdown));
|
|
294
325
|
}
|
|
295
|
-
return parts.length >
|
|
326
|
+
return parts.length > 0 ? `zen ${parts.join(" · ")}` : undefined;
|
|
296
327
|
}
|
|
297
328
|
|
|
298
329
|
function formatCommandCodeReport(lines: string[], report: UsageReport): void {
|
|
@@ -311,8 +342,8 @@ function formatCommandCodeReport(lines: string[], report: UsageReport): void {
|
|
|
311
342
|
}
|
|
312
343
|
}
|
|
313
344
|
|
|
314
|
-
function formatCommandCodeStatusline(report: UsageReport): string | undefined {
|
|
315
|
-
const parts = [
|
|
345
|
+
function formatCommandCodeStatusline(report: UsageReport, countdown: StatusCountdown): string | undefined {
|
|
346
|
+
const parts: string[] = [];
|
|
316
347
|
for (const [id, label] of [
|
|
317
348
|
["five-hour", "5h"],
|
|
318
349
|
["weekly", "wk"],
|
|
@@ -320,9 +351,9 @@ function formatCommandCodeStatusline(report: UsageReport): string | undefined {
|
|
|
320
351
|
] as const) {
|
|
321
352
|
const bucket = report.buckets.find((candidate) => candidate.id === id);
|
|
322
353
|
if (bucket?.unit !== "usd" || bucket.limit === undefined || bucket.remaining === undefined) continue;
|
|
323
|
-
parts.push(`${percentRemaining(bucket)}
|
|
354
|
+
parts.push(formatQuotaWindow(bucket, `${percentRemaining(bucket)}%`, label, countdown));
|
|
324
355
|
}
|
|
325
|
-
return parts.length >
|
|
356
|
+
return parts.length > 0 ? `cmd ${parts.join(" · ")}` : undefined;
|
|
326
357
|
}
|
|
327
358
|
|
|
328
359
|
function formatKimiCodingReport(lines: string[], report: UsageReport): void {
|
|
@@ -363,7 +394,7 @@ function formatKimiCodingReport(lines: string[], report: UsageReport): void {
|
|
|
363
394
|
}
|
|
364
395
|
}
|
|
365
396
|
|
|
366
|
-
function formatKimiCodingStatusline(report: UsageReport): string | undefined {
|
|
397
|
+
function formatKimiCodingStatusline(report: UsageReport, countdown: StatusCountdown): string | undefined {
|
|
367
398
|
const fiveHour = report.buckets.find((bucket) => bucket.id === "five-hour");
|
|
368
399
|
const weekly = report.buckets.find((bucket) => bucket.id === "weekly");
|
|
369
400
|
const monthly = report.buckets.find((bucket) => bucket.id === "monthly");
|
|
@@ -371,15 +402,15 @@ function formatKimiCodingStatusline(report: UsageReport): string | undefined {
|
|
|
371
402
|
const selected = [subWindow, weekly, monthly].filter(
|
|
372
403
|
(bucket, index, buckets): bucket is UsageBucket => bucket !== undefined && buckets.indexOf(bucket) === index,
|
|
373
404
|
);
|
|
374
|
-
const parts = [
|
|
405
|
+
const parts: string[] = [];
|
|
375
406
|
for (const bucket of selected) {
|
|
376
407
|
if (bucket.remaining === undefined || (bucket.unit !== "percent" && !bucket.limit)) continue;
|
|
377
408
|
const fallback = bucket.id === "weekly" ? "weekly" : "5h";
|
|
378
409
|
const window = bucket.id === "monthly" ? "mo" : formatWindowLabel(bucket.windowMinutes, fallback, true);
|
|
379
410
|
const remaining = bucket.unit === "percent" ? Math.round(clampPercent(bucket.remaining)) : percentRemaining(bucket);
|
|
380
|
-
parts.push(`${remaining}
|
|
411
|
+
parts.push(formatQuotaWindow(bucket, `${remaining}%`, window, countdown));
|
|
381
412
|
}
|
|
382
|
-
return parts.length >
|
|
413
|
+
return parts.length > 0 ? `kimi ${parts.join(" · ")}` : undefined;
|
|
383
414
|
}
|
|
384
415
|
|
|
385
416
|
function formatMoonshotReport(lines: string[], report: UsageReport): void {
|
|
@@ -418,7 +449,11 @@ function formatMiniMaxReport(lines: string[], report: UsageReport): void {
|
|
|
418
449
|
}
|
|
419
450
|
}
|
|
420
451
|
|
|
421
|
-
function formatMiniMaxStatusline(
|
|
452
|
+
function formatMiniMaxStatusline(
|
|
453
|
+
report: UsageReport,
|
|
454
|
+
countdown: StatusCountdown,
|
|
455
|
+
model?: UsageModel,
|
|
456
|
+
): string | undefined {
|
|
422
457
|
const prefix = report.providerId === "minimax-cn" ? "minimax cn" : "minimax";
|
|
423
458
|
if (report.source === "minimax-account-balance") {
|
|
424
459
|
const available = report.metrics.find((metric) => metric.id === "available-balance");
|
|
@@ -427,22 +462,22 @@ function formatMiniMaxStatusline(report: UsageReport, model?: UsageModel): strin
|
|
|
427
462
|
const selectedGroup = selectMiniMaxGroup(report, model);
|
|
428
463
|
if (!selectedGroup) return undefined;
|
|
429
464
|
const selected = report.buckets.filter((bucket) => bucket.groupId === selectedGroup);
|
|
430
|
-
const parts = [
|
|
465
|
+
const parts: string[] = [];
|
|
431
466
|
for (const bucket of selected) {
|
|
432
467
|
const fallback = bucket.id.endsWith(":weekly") ? "weekly" : "5h";
|
|
433
468
|
const window = formatWindowLabel(bucket.windowMinutes, fallback, true);
|
|
434
469
|
if (bucket.period === "unlimited") {
|
|
435
|
-
parts.push(
|
|
470
|
+
parts.push(`${window}:unlimited`);
|
|
436
471
|
continue;
|
|
437
472
|
}
|
|
438
473
|
if (bucket.unit === "percent" && bucket.remaining !== undefined) {
|
|
439
|
-
parts.push(`${bucket.remaining}
|
|
474
|
+
parts.push(formatQuotaWindow(bucket, `${bucket.remaining}%`, window, countdown));
|
|
440
475
|
continue;
|
|
441
476
|
}
|
|
442
477
|
if (bucket.limit === undefined || bucket.remaining === undefined) continue;
|
|
443
|
-
parts.push(`${percentRemaining(bucket)}
|
|
478
|
+
parts.push(formatQuotaWindow(bucket, `${percentRemaining(bucket)}%`, window, countdown));
|
|
444
479
|
}
|
|
445
|
-
return parts.length >
|
|
480
|
+
return parts.length > 0 ? `${prefix} ${parts.join(" · ")}` : undefined;
|
|
446
481
|
}
|
|
447
482
|
|
|
448
483
|
function selectMiniMaxGroup(report: UsageReport, model?: UsageModel): string | undefined {
|
|
@@ -493,18 +528,25 @@ function wildcardKeyMatches(pattern: string, value: string): boolean {
|
|
|
493
528
|
return pattern.endsWith("*") || (last !== undefined && value.endsWith(last));
|
|
494
529
|
}
|
|
495
530
|
|
|
496
|
-
function formatZaiStatusline(report: UsageReport): string | undefined {
|
|
531
|
+
function formatZaiStatusline(report: UsageReport, countdown: StatusCountdown): string | undefined {
|
|
497
532
|
const selected = [
|
|
498
533
|
report.buckets.find((bucket) => bucket.id === "five-hour"),
|
|
499
534
|
report.buckets.find((bucket) => bucket.id === "weekly"),
|
|
500
535
|
];
|
|
501
|
-
const parts = [
|
|
536
|
+
const parts: string[] = [];
|
|
502
537
|
for (const bucket of selected) {
|
|
503
538
|
if (!bucket?.limit || bucket.remaining === undefined) continue;
|
|
504
539
|
const fallback = bucket.id === "weekly" ? "weekly" : "5h";
|
|
505
|
-
parts.push(
|
|
540
|
+
parts.push(
|
|
541
|
+
formatQuotaWindow(
|
|
542
|
+
bucket,
|
|
543
|
+
`${percentRemaining(bucket)}%`,
|
|
544
|
+
formatWindowLabel(bucket.windowMinutes, fallback, true),
|
|
545
|
+
countdown,
|
|
546
|
+
),
|
|
547
|
+
);
|
|
506
548
|
}
|
|
507
|
-
return parts.length >
|
|
549
|
+
return parts.length > 0 ? `zai ${parts.join(" · ")}` : undefined;
|
|
508
550
|
}
|
|
509
551
|
|
|
510
552
|
function formatCurrencyMetric(metric: UsageReport["metrics"][number]): string {
|
|
@@ -604,7 +646,8 @@ function formatCodexStatusline(
|
|
|
604
646
|
if (!group) return formatCodexCreditsStatus(report);
|
|
605
647
|
const buckets = report.buckets.filter((bucket) => (bucket.groupId ?? bucket.id) === group);
|
|
606
648
|
const labelBucket = buckets[0];
|
|
607
|
-
const
|
|
649
|
+
const prefix = group === "codex" ? "codex" : `codex ${compactLimitLabel(labelBucket?.groupLabel ?? group)}`;
|
|
650
|
+
const parts: string[] = [];
|
|
608
651
|
for (const bucket of buckets) {
|
|
609
652
|
if (bucket.remaining === undefined) continue;
|
|
610
653
|
const displayedPercentage = percentage === "used" ? (bucket.used ?? 100 - bucket.remaining) : bucket.remaining;
|
|
@@ -612,13 +655,13 @@ function formatCodexStatusline(
|
|
|
612
655
|
const fallback = bucket.id.endsWith(":secondary") ? "weekly" : "5h";
|
|
613
656
|
const window = formatWindowLabel(bucket.windowMinutes, fallback, true);
|
|
614
657
|
if (!showResetCountdown) {
|
|
615
|
-
parts.push(`${
|
|
658
|
+
parts.push(`${window}:${percent}`);
|
|
616
659
|
continue;
|
|
617
660
|
}
|
|
618
661
|
const reset = formatResetCountdown(bucket.resetsAt, now);
|
|
619
|
-
parts.push(`${percent}
|
|
662
|
+
parts.push(reset ? `${percent} ↻ ${reset}` : `${window}:${percent}`);
|
|
620
663
|
}
|
|
621
|
-
return parts.length >
|
|
664
|
+
return parts.length > 0 ? `${prefix} ${parts.join(" · ")}` : formatCodexCreditsStatus(report);
|
|
622
665
|
}
|
|
623
666
|
|
|
624
667
|
function formatCodexCreditsStatus(report: UsageReport): string {
|
|
@@ -711,6 +754,25 @@ function formatWindowLabel(minutes: number | undefined, fallback: "5h" | "weekly
|
|
|
711
754
|
return `${minutes}m`;
|
|
712
755
|
}
|
|
713
756
|
|
|
757
|
+
function formatQuotaWindow(bucket: UsageBucket, percent: string, window: string, countdown: StatusCountdown): string {
|
|
758
|
+
const reset = formatQuotaCountdown(bucket, countdown);
|
|
759
|
+
return reset ? `${percent} ↻ ${reset}` : `${window}:${percent}`;
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
function formatQuotaCountdown(bucket: UsageBucket, countdown: StatusCountdown): string | undefined {
|
|
763
|
+
if (
|
|
764
|
+
bucket.period === "unlimited" ||
|
|
765
|
+
bucket.resetsAt === undefined ||
|
|
766
|
+
!Number.isFinite(bucket.resetsAt * 1_000) ||
|
|
767
|
+
!Number.isFinite(countdown.now) ||
|
|
768
|
+
bucket.resetsAt * 1_000 <= countdown.now
|
|
769
|
+
)
|
|
770
|
+
return undefined;
|
|
771
|
+
const reset = formatResetCountdown(bucket.resetsAt, countdown.now);
|
|
772
|
+
if (reset !== undefined) countdown.active = true;
|
|
773
|
+
return reset;
|
|
774
|
+
}
|
|
775
|
+
|
|
714
776
|
function formatResetCountdown(resetsAt: number | undefined, now: number): string | undefined {
|
|
715
777
|
if (resetsAt === undefined || !Number.isFinite(resetsAt) || !Number.isFinite(now)) return undefined;
|
|
716
778
|
const totalMinutes = Math.max(0, Math.ceil((resetsAt * 1_000 - now) / 60_000));
|
package/src/usage.ts
CHANGED
|
@@ -20,7 +20,7 @@ import {
|
|
|
20
20
|
resolveCodexResetAuth,
|
|
21
21
|
} from "./codex-resets.js";
|
|
22
22
|
import { abortError, awaitWithDeadline, errorMessage, runWithConcurrency, UsageCache } from "./core.js";
|
|
23
|
-
import { formatProviderStates,
|
|
23
|
+
import { formatProviderStates, formatUsageStatuslineWithCountdown } from "./format.js";
|
|
24
24
|
import { createOAuthCredentialCandidateReader } from "./oauth-credential-source.js";
|
|
25
25
|
import { UnsupportedOpenAIUsageAuthError } from "./providers/openai-chatgpt.js";
|
|
26
26
|
import {
|
|
@@ -181,31 +181,33 @@ export default function usageExtension(pi: ExtensionAPI, dependencies: UsageExte
|
|
|
181
181
|
outcome.state.report.providerId === "openai-codex" && settings.codexStatusResetCountdown;
|
|
182
182
|
const isCompanionReport = outcome.state.report.source === "openai-chatgpt-companion";
|
|
183
183
|
const now = Date.now();
|
|
184
|
-
const
|
|
184
|
+
const status = formatUsageStatuslineWithCountdown(
|
|
185
185
|
outcome.state.report,
|
|
186
186
|
model,
|
|
187
187
|
now,
|
|
188
188
|
showCodexResetCountdown,
|
|
189
189
|
settings.codexStatusPercentage,
|
|
190
190
|
);
|
|
191
|
-
const value =
|
|
191
|
+
const value = status.value ? fastRuntime.decorateStatus(model, status.value) : undefined;
|
|
192
192
|
if (!safeSetStatus(ctx, value)) return;
|
|
193
193
|
if (shouldSchedule && sessionActive) scheduleStatusRefresh(ctx, model);
|
|
194
194
|
if (
|
|
195
195
|
sessionActive &&
|
|
196
|
-
(
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
196
|
+
(status.hasCountdown ||
|
|
197
|
+
((showCodexResetCountdown || isCompanionReport) &&
|
|
198
|
+
outcome.state.report.buckets.some(
|
|
199
|
+
(bucket) =>
|
|
200
|
+
(!isCompanionReport || bucket.groupId === "chatgpt-plan") &&
|
|
201
|
+
bucket.resetsAt !== undefined &&
|
|
202
|
+
Number.isFinite(bucket.resetsAt) &&
|
|
203
|
+
bucket.resetsAt * 1_000 > now,
|
|
204
|
+
)))
|
|
204
205
|
) {
|
|
205
206
|
const generation = statusGeneration;
|
|
206
207
|
statusCountdownTimer = setTimeout(() => {
|
|
207
|
-
|
|
208
|
+
// A queued obsolete callback must not detach its successor's owned timer.
|
|
208
209
|
if (!sessionActive || generation !== statusGeneration) return;
|
|
210
|
+
statusCountdownTimer = undefined;
|
|
209
211
|
publishStatus(ctx, outcome, model, false);
|
|
210
212
|
}, STATUS_COUNTDOWN_REFRESH_MS);
|
|
211
213
|
statusCountdownTimer.unref?.();
|