@narumitw/pi-usage 0.52.0 → 0.52.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.ts ADDED
@@ -0,0 +1,2760 @@
1
+ // @generated by scripts/build-runtime.mjs; do not edit.
2
+ // @ts-nocheck -- generated JavaScript uses a .ts extension for Pi's Jiti loader.
3
+
4
+ // src/codex-fast.ts
5
+ import { calculateCost, hasApi } from "@earendil-works/pi-ai";
6
+ var CODEX_FAST_SERVICE_TIER = "priority";
7
+ var CODEX_STANDARD_SERVICE_TIER = "default";
8
+ var CODEX_FAST_MODEL_IDS = /* @__PURE__ */ new Set([
9
+ "gpt-5.4",
10
+ "gpt-5.5",
11
+ "gpt-5.6-luna",
12
+ "gpt-5.6-sol",
13
+ "gpt-5.6-terra"
14
+ ]);
15
+ function codexFastAvailability(model, enabled) {
16
+ if (model?.provider !== "openai-codex") return { kind: "not-codex" };
17
+ if (!isOfficialCodexModel(model)) {
18
+ return {
19
+ kind: "unavailable",
20
+ reason: "Fast mode requires the official OpenAI Codex Responses endpoint."
21
+ };
22
+ }
23
+ if (!CODEX_FAST_MODEL_IDS.has(model.id)) {
24
+ return {
25
+ kind: "unavailable",
26
+ reason: `${model.id} does not advertise Codex Fast support.`
27
+ };
28
+ }
29
+ return { kind: "available", enabled };
30
+ }
31
+ function codexFastIsEffective(model, enabled) {
32
+ return codexFastAvailability(model, enabled).kind === "available" && enabled;
33
+ }
34
+ function codexFastRequestTier(model, enabled) {
35
+ if (!isOfficialCodexModel(model)) return void 0;
36
+ return enabled && CODEX_FAST_MODEL_IDS.has(model.id) ? CODEX_FAST_SERVICE_TIER : CODEX_STANDARD_SERVICE_TIER;
37
+ }
38
+ function rewriteCodexFastPayload(payload, model, enabled) {
39
+ const serviceTier = codexFastRequestTier(model, enabled);
40
+ if (!serviceTier || !isRecord(payload)) return void 0;
41
+ return { ...payload, service_tier: serviceTier };
42
+ }
43
+ function correctCodexFastMessageCost(message, model, fastRequested) {
44
+ if (!codexFastIsEffective(model, fastRequested) || !isRecord(message) || message.role !== "assistant" || message.provider !== model?.provider || message.model !== model?.id) {
45
+ return void 0;
46
+ }
47
+ const usage = isRecord(message.usage) ? message.usage : void 0;
48
+ const cost = usage && isRecord(usage.cost) ? usage.cost : void 0;
49
+ if (!usage || !cost || !hasCompleteUsage(usage) || !isOfficialCodexModel(model)) return void 0;
50
+ const correctedUsage = structuredClone(usage);
51
+ calculateCost(model, correctedUsage);
52
+ const multiplier = model.id === "gpt-5.5" ? 2.5 : 2;
53
+ const correctedCost = correctedUsage.cost;
54
+ for (const key of ["input", "output", "cacheRead", "cacheWrite", "total"]) {
55
+ correctedCost[key] *= multiplier;
56
+ }
57
+ if (costsEqual(cost, correctedCost)) return void 0;
58
+ return { ...message, usage: correctedUsage };
59
+ }
60
+ function codexFastStatusLabel(status, enabled) {
61
+ if (!enabled || !/^codex(?:\s|$)/u.test(status)) return status;
62
+ return status === "codex" ? "codex fast" : `codex fast${status.slice("codex".length)}`;
63
+ }
64
+ function isOfficialCodexModel(model) {
65
+ if (model?.provider !== "openai-codex" || !hasApi(model, "openai-codex-responses")) {
66
+ return false;
67
+ }
68
+ try {
69
+ return new URL(model.baseUrl).origin === "https://chatgpt.com";
70
+ } catch {
71
+ return false;
72
+ }
73
+ }
74
+ function isRecord(value) {
75
+ return typeof value === "object" && value !== null && !Array.isArray(value);
76
+ }
77
+ function hasCompleteUsage(value) {
78
+ return ["input", "output", "cacheRead", "cacheWrite"].every(
79
+ (key) => typeof value[key] === "number" && Number.isFinite(value[key])
80
+ );
81
+ }
82
+ function costsEqual(left, right) {
83
+ return ["input", "output", "cacheRead", "cacheWrite", "total"].every(
84
+ (key) => left[key] === right[key]
85
+ );
86
+ }
87
+
88
+ // src/codex-resets.ts
89
+ import { readStoredCredential as readStoredCredential3 } from "@earendil-works/pi-coding-agent";
90
+
91
+ // src/core.ts
92
+ import { createHmac } from "node:crypto";
93
+ var UsageCache = class {
94
+ entries = /* @__PURE__ */ new Map();
95
+ ttlMs;
96
+ maxEntries;
97
+ constructor(ttlMs, maxEntries = 32) {
98
+ if (!Number.isFinite(ttlMs) || ttlMs <= 0) throw new Error("Cache TTL must be positive.");
99
+ if (!Number.isSafeInteger(maxEntries) || maxEntries < 1) {
100
+ throw new Error("Cache entry limit must be a positive integer.");
101
+ }
102
+ this.ttlMs = ttlMs;
103
+ this.maxEntries = maxEntries;
104
+ }
105
+ get size() {
106
+ return this.entries.size;
107
+ }
108
+ get(providerId, fingerprint, now = Date.now()) {
109
+ this.sweepExpired(now);
110
+ return this.entries.get(cacheKey(providerId, fingerprint))?.report;
111
+ }
112
+ set(providerId, fingerprint, report, now = Date.now()) {
113
+ this.sweepExpired(now);
114
+ const key = cacheKey(providerId, fingerprint);
115
+ this.entries.delete(key);
116
+ while (this.entries.size >= this.maxEntries) {
117
+ const oldest = this.entries.keys().next().value;
118
+ if (oldest === void 0) break;
119
+ this.entries.delete(oldest);
120
+ }
121
+ this.entries.set(key, { createdAt: now, report });
122
+ }
123
+ clearProvider(providerId) {
124
+ for (const key of this.entries.keys()) {
125
+ if (key.startsWith(`${providerId}:`)) this.entries.delete(key);
126
+ }
127
+ }
128
+ clear() {
129
+ this.entries.clear();
130
+ }
131
+ sweepExpired(now) {
132
+ for (const [key, entry] of this.entries) {
133
+ if (now - entry.createdAt >= this.ttlMs) this.entries.delete(key);
134
+ }
135
+ }
136
+ };
137
+ function fingerprintResolvedAuth(auth, salt) {
138
+ const headers = Object.entries(auth.headers ?? {}).map(([name, value]) => [name.toLowerCase(), value]).sort(([left], [right]) => left.localeCompare(right));
139
+ const canonical = JSON.stringify({ apiKey: auth.apiKey ?? "", headers });
140
+ return createHmac("sha256", salt).update(canonical).digest("hex");
141
+ }
142
+ async function runWithConcurrency(items, limit, worker, signal) {
143
+ if (signal.aborted) throw abortError();
144
+ if (!Number.isSafeInteger(limit) || limit < 1)
145
+ throw new Error("Concurrency limit must be positive.");
146
+ const results = new Array(items.length);
147
+ let nextIndex = 0;
148
+ const runners = Array.from({ length: Math.min(limit, items.length) }, async () => {
149
+ while (nextIndex < items.length) {
150
+ if (signal.aborted) throw abortError();
151
+ const index = nextIndex;
152
+ nextIndex += 1;
153
+ try {
154
+ results[index] = {
155
+ status: "fulfilled",
156
+ value: await worker(items[index], index, signal)
157
+ };
158
+ } catch (reason) {
159
+ results[index] = { status: "rejected", reason };
160
+ }
161
+ }
162
+ });
163
+ await Promise.all(runners);
164
+ if (signal.aborted) throw abortError();
165
+ return results;
166
+ }
167
+ async function awaitWithDeadline(operation, signal, timeoutMs, description) {
168
+ if (signal.aborted) throw abortError();
169
+ const controller = new AbortController();
170
+ const abortFromCaller = () => controller.abort();
171
+ signal.addEventListener("abort", abortFromCaller, { once: true });
172
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
173
+ try {
174
+ return await Promise.race([
175
+ operation,
176
+ new Promise((_resolve, reject) => {
177
+ controller.signal.addEventListener(
178
+ "abort",
179
+ () => {
180
+ reject(
181
+ signal.aborted ? abortError() : Object.assign(
182
+ new Error(`Timed out after ${Math.round(timeoutMs / 1e3)}s ${description}.`),
183
+ { name: "TimeoutError" }
184
+ )
185
+ );
186
+ },
187
+ { once: true }
188
+ );
189
+ })
190
+ ]);
191
+ } finally {
192
+ clearTimeout(timeout);
193
+ signal.removeEventListener("abort", abortFromCaller);
194
+ }
195
+ }
196
+ function sanitizeDisplayText(value, maxChars = 160) {
197
+ let result = "";
198
+ for (let index = 0; index < value.length; ) {
199
+ const codePoint = value.codePointAt(index) ?? 0;
200
+ const character = String.fromCodePoint(codePoint);
201
+ if (codePoint === 27 || codePoint === 155 || codePoint === 157) {
202
+ index = skipTerminalEscape(value, index, codePoint);
203
+ continue;
204
+ }
205
+ if (codePoint <= 31 || codePoint >= 127 && codePoint <= 159) {
206
+ if (codePoint === 9 || codePoint === 10 || codePoint === 13) result += " ";
207
+ index += character.length;
208
+ continue;
209
+ }
210
+ result += character;
211
+ index += character.length;
212
+ }
213
+ return truncate(result.replace(/\s+/gu, " ").trim(), maxChars);
214
+ }
215
+ function redactUsageError(value, secrets = []) {
216
+ let redacted = value;
217
+ for (const secret of [...new Set(secrets)].filter(Boolean).sort((a, b) => b.length - a.length)) {
218
+ redacted = redacted.replace(new RegExp(escapeRegExp(secret), "g"), "<redacted>");
219
+ }
220
+ redacted = redacted.replace(/Bearer\s+[A-Za-z0-9._~+/=-]+/gi, "Bearer <redacted>").replace(/"(?:access_token|refresh_token|api_key)"\s*:\s*"[^"]+"/gi, (match) => {
221
+ const separator = match.indexOf(":");
222
+ return `${match.slice(0, separator + 1)}"<redacted>"`;
223
+ });
224
+ return sanitizeDisplayText(redacted, 600);
225
+ }
226
+ function errorMessage(error) {
227
+ return sanitizeDisplayText(error instanceof Error ? error.message : String(error), 600);
228
+ }
229
+ function abortError() {
230
+ return Object.assign(new Error("Usage query aborted."), { name: "AbortError" });
231
+ }
232
+ function skipTerminalEscape(value, start, codePoint) {
233
+ let index = start + 1;
234
+ const next = value.charCodeAt(index);
235
+ const isOsc = codePoint === 157 || codePoint === 27 && next === 93;
236
+ if (isOsc) {
237
+ if (codePoint === 27) index += 1;
238
+ while (index < value.length) {
239
+ const current = value.charCodeAt(index);
240
+ if (current === 7) return index + 1;
241
+ if (current === 27 && value.charCodeAt(index + 1) === 92) return index + 2;
242
+ index += 1;
243
+ }
244
+ return index;
245
+ }
246
+ const isCsi = codePoint === 155 || codePoint === 27 && next === 91;
247
+ if (isCsi) {
248
+ if (codePoint === 27) index += 1;
249
+ while (index < value.length) {
250
+ const current = value.charCodeAt(index);
251
+ index += 1;
252
+ if (current >= 64 && current <= 126) break;
253
+ }
254
+ return index;
255
+ }
256
+ return Math.min(value.length, start + (codePoint === 27 ? 2 : 1));
257
+ }
258
+ function cacheKey(providerId, fingerprint) {
259
+ return `${providerId}:${fingerprint}`;
260
+ }
261
+ function escapeRegExp(value) {
262
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
263
+ }
264
+ function truncate(value, maxChars) {
265
+ if (value.length <= maxChars) return value;
266
+ return `${value.slice(0, maxChars - 1)}\u2026`;
267
+ }
268
+
269
+ // src/oauth-credential-source.ts
270
+ import {
271
+ readStoredCredential
272
+ } from "@earendil-works/pi-coding-agent";
273
+ var OAUTH_CREDENTIAL_SOURCE_CHANNEL = "oauth:credential-source:v1";
274
+ function createOAuthCredentialCandidateReader(pi, credentialReader = readStoredCredential) {
275
+ return (ctx, providerId) => collectOAuthCredentialCandidates(pi, ctx, providerId, credentialReader);
276
+ }
277
+ function collectOAuthCredentialCandidates(pi, ctx, providerId, credentialReader = readStoredCredential) {
278
+ const candidates = [];
279
+ let collecting = true;
280
+ const request = Object.freeze({
281
+ session: ctx.sessionManager,
282
+ provider: providerId,
283
+ offer(candidate) {
284
+ if (!collecting) return;
285
+ const clone = cloneOAuthCredential(candidate);
286
+ if (clone) candidates.push(clone);
287
+ }
288
+ });
289
+ try {
290
+ pi.events.emit(OAUTH_CREDENTIAL_SOURCE_CHANNEL, request);
291
+ } catch {
292
+ return { ok: false };
293
+ } finally {
294
+ collecting = false;
295
+ }
296
+ const offeredCount = candidates.length;
297
+ try {
298
+ const fallback = cloneOAuthCredential(credentialReader(providerId));
299
+ if (fallback) candidates.push(fallback);
300
+ } catch {
301
+ }
302
+ return { ok: true, candidates, offeredCount };
303
+ }
304
+ function fallbackOAuthCredentialCandidates(providerId, credentialReader) {
305
+ try {
306
+ const credential = cloneOAuthCredential(credentialReader(providerId));
307
+ return { ok: true, candidates: credential ? [credential] : [], offeredCount: 0 };
308
+ } catch {
309
+ return { ok: true, candidates: [], offeredCount: 0 };
310
+ }
311
+ }
312
+ function cloneOAuthCredential(value) {
313
+ try {
314
+ if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
315
+ const clone = structuredClone(value);
316
+ if (!clone || typeof clone !== "object" || Array.isArray(clone)) return void 0;
317
+ if (clone.type !== "oauth") return void 0;
318
+ if (typeof clone.access !== "string" || !clone.access) return void 0;
319
+ if (typeof clone.refresh !== "string" || !clone.refresh) return void 0;
320
+ if (typeof clone.expires !== "number" || !Number.isFinite(clone.expires)) return void 0;
321
+ return clone;
322
+ } catch {
323
+ return void 0;
324
+ }
325
+ }
326
+
327
+ // src/query.ts
328
+ import { randomBytes } from "node:crypto";
329
+ import { readStoredCredential as readStoredCredential2 } from "@earendil-works/pi-coding-agent";
330
+
331
+ // src/providers/codex.ts
332
+ function normalizeCodexBackendPayload(payload, capturedAt) {
333
+ const buckets = [];
334
+ normalizeRateLimitGroup(buckets, "codex", "Codex", payload.rate_limit, false);
335
+ const additional = Array.isArray(payload.additional_rate_limits) ? payload.additional_rate_limits : [];
336
+ for (const item of additional) {
337
+ const value = asObject(item);
338
+ const id = asString(value?.metered_feature) ?? asString(value?.limit_name);
339
+ if (!value || !id) continue;
340
+ try {
341
+ normalizeRateLimitGroup(
342
+ buckets,
343
+ id,
344
+ asString(value.limit_name) ?? id,
345
+ value.rate_limit,
346
+ true
347
+ );
348
+ } catch {
349
+ }
350
+ }
351
+ const metrics = [];
352
+ const credits = asObject(payload.credits);
353
+ if (credits?.has_credits === true) {
354
+ if (credits.unlimited === true) {
355
+ metrics.push({ id: "credits", label: "Credits", value: "unlimited" });
356
+ } else {
357
+ const balance = asNumber(credits.balance);
358
+ if (balance !== void 0) {
359
+ metrics.push({ id: "credits", label: "Credits", value: balance, unit: "count" });
360
+ } else {
361
+ metrics.push({ id: "credits", label: "Credits", value: "available" });
362
+ }
363
+ }
364
+ } else if (credits?.has_credits === false) {
365
+ metrics.push({ id: "credits", label: "Credits", value: "none" });
366
+ }
367
+ const resetCredits = asObject(payload.rate_limit_reset_credits);
368
+ const resetCount = asNonnegativeInteger(resetCredits?.available_count);
369
+ if (resetCount !== void 0) {
370
+ metrics.push({
371
+ id: "reset-credits",
372
+ label: "Usage limit resets",
373
+ value: resetCount,
374
+ unit: "count"
375
+ });
376
+ }
377
+ if (buckets.length === 0 && metrics.length === 0) {
378
+ throw new Error("Codex usage endpoint returned no displayable usage data.");
379
+ }
380
+ const planType = asString(payload.plan_type);
381
+ return {
382
+ providerId: "openai-codex",
383
+ providerName: "OpenAI Codex",
384
+ capturedAt,
385
+ source: "codex-pi-auth",
386
+ semantics: {
387
+ kind: "consumer-subscription",
388
+ label: "ChatGPT subscription limits"
389
+ },
390
+ buckets,
391
+ metrics,
392
+ ...planType ? { notes: [`Plan: ${planType}`] } : {}
393
+ };
394
+ }
395
+ function normalizeRateLimitGroup(buckets, groupId, groupLabel, raw, optional) {
396
+ if (raw === void 0 || raw === null) return;
397
+ const details = asObject(raw);
398
+ if (!details) {
399
+ if (optional) return;
400
+ throw new Error("Codex rate limit was not an object.");
401
+ }
402
+ addWindow(buckets, groupId, groupLabel, "primary", details.primary_window);
403
+ addWindow(buckets, groupId, groupLabel, "secondary", details.secondary_window);
404
+ }
405
+ function addWindow(buckets, groupId, groupLabel, position, raw) {
406
+ if (raw === void 0 || raw === null) return;
407
+ const value = asObject(raw);
408
+ if (!value) throw new Error("Codex rate-limit window was not an object.");
409
+ const used = asNumber(value.used_percent);
410
+ if (used === void 0) return;
411
+ const seconds = asNumber(value.limit_window_seconds);
412
+ const resetsAt = asNumber(value.reset_at);
413
+ buckets.push({
414
+ id: `${groupId}:${position}`,
415
+ label: position === "primary" ? "Primary limit" : "Secondary limit",
416
+ groupId,
417
+ groupLabel,
418
+ modelKeys: [groupId, groupLabel],
419
+ used,
420
+ remaining: 100 - clampPercent(used),
421
+ limit: 100,
422
+ unit: "percent",
423
+ ...seconds !== void 0 && seconds > 0 ? { windowMinutes: Math.ceil(seconds / 60) } : {},
424
+ ...resetsAt !== void 0 ? { resetsAt } : {}
425
+ });
426
+ }
427
+ function asObject(value) {
428
+ if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
429
+ return value;
430
+ }
431
+ function asString(value) {
432
+ if (typeof value !== "string") return void 0;
433
+ return sanitizeDisplayText(value, 160) || void 0;
434
+ }
435
+ function asNumber(value) {
436
+ if (typeof value === "number" && Number.isFinite(value)) return value;
437
+ if (typeof value === "string" && value.trim()) {
438
+ const parsed = Number(value);
439
+ return Number.isFinite(parsed) ? parsed : void 0;
440
+ }
441
+ return void 0;
442
+ }
443
+ function asNonnegativeInteger(value) {
444
+ const parsed = asNumber(value);
445
+ if (parsed === void 0 || !Number.isSafeInteger(parsed)) return void 0;
446
+ return Math.max(0, parsed);
447
+ }
448
+ function clampPercent(value) {
449
+ return Math.min(100, Math.max(0, value));
450
+ }
451
+
452
+ // src/providers/github-copilot.ts
453
+ function normalizeGitHubCopilotUsagePayload(payload, capturedAt) {
454
+ const snapshots = asObject2(payload.quota_snapshots);
455
+ const premium = asObject2(snapshots?.premium_interactions);
456
+ const metrics = [];
457
+ let semanticsLabel;
458
+ let bucket;
459
+ if (premium) {
460
+ const tokenBasedBilling = premium.token_based_billing === true;
461
+ const id = tokenBasedBilling ? "ai-credits" : "premium-requests";
462
+ const label = tokenBasedBilling ? "AI credits" : "Premium requests";
463
+ semanticsLabel = tokenBasedBilling ? "GitHub Copilot AI Credits allowance" : "GitHub Copilot premium request quota";
464
+ if (premium.unlimited === true) {
465
+ bucket = { id, label, unit: "count" };
466
+ } else {
467
+ const entitlement = asNonnegativeNumber(premium.entitlement);
468
+ const rawRemaining = asFiniteNumber(premium.remaining) ?? asFiniteNumber(premium.quota_remaining);
469
+ if (entitlement === void 0 || rawRemaining === void 0) {
470
+ throw new Error(`GitHub Copilot ${label.toLowerCase()} quota was incomplete.`);
471
+ }
472
+ const overageUsed = Math.max(
473
+ asNonnegativeNumber(premium.overage_count) ?? 0,
474
+ Math.max(0, -rawRemaining)
475
+ );
476
+ if (overageUsed > 0) {
477
+ metrics.push({
478
+ id: "overage-used",
479
+ label: "Additional usage",
480
+ value: overageUsed,
481
+ unit: "count"
482
+ });
483
+ }
484
+ bucket = {
485
+ id,
486
+ label,
487
+ used: asNonnegativeNumber(premium.credits_used) ?? Math.max(0, entitlement - rawRemaining),
488
+ remaining: Math.max(0, rawRemaining),
489
+ limit: entitlement,
490
+ unit: "count",
491
+ period: "monthly",
492
+ ...resetTimestamp(payload)
493
+ };
494
+ }
495
+ } else {
496
+ const limited = asObject2(payload.limited_user_quotas);
497
+ const monthly = asObject2(payload.monthly_quotas);
498
+ const remaining = asNonnegativeNumber(limited?.chat);
499
+ const entitlement = asNonnegativeNumber(monthly?.chat);
500
+ if (remaining === void 0 || entitlement === void 0) {
501
+ throw new Error("GitHub Copilot usage response contained no supported quota.");
502
+ }
503
+ semanticsLabel = "GitHub Copilot Free chat quota";
504
+ bucket = {
505
+ id: "chat-requests",
506
+ label: "Chat requests",
507
+ used: Math.max(0, entitlement - remaining),
508
+ remaining,
509
+ limit: entitlement,
510
+ unit: "count",
511
+ period: "monthly",
512
+ ...resetTimestamp(payload)
513
+ };
514
+ }
515
+ const notes = [];
516
+ const plan = asString2(payload.copilot_plan) ?? asString2(payload.access_type_sku);
517
+ if (plan) notes.push(`Plan: ${plan}`);
518
+ return {
519
+ providerId: "github-copilot",
520
+ providerName: "GitHub Copilot",
521
+ capturedAt,
522
+ source: "github-copilot-user",
523
+ semantics: { kind: "consumer-subscription", label: semanticsLabel },
524
+ accountLabel: asString2(payload.login),
525
+ buckets: [bucket],
526
+ metrics,
527
+ ...notes.length > 0 ? { notes } : {}
528
+ };
529
+ }
530
+ function resetTimestamp(payload) {
531
+ const raw = asString2(payload.quota_reset_date_utc) ?? asString2(payload.quota_reset_date) ?? asString2(payload.limited_user_reset_date);
532
+ if (!raw) return {};
533
+ const milliseconds = Date.parse(raw);
534
+ return Number.isNaN(milliseconds) ? {} : { resetsAt: Math.floor(milliseconds / 1e3) };
535
+ }
536
+ function asObject2(value) {
537
+ if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
538
+ return value;
539
+ }
540
+ function asString2(value) {
541
+ if (typeof value !== "string") return void 0;
542
+ return sanitizeDisplayText(value, 80) || void 0;
543
+ }
544
+ function asFiniteNumber(value) {
545
+ if (typeof value !== "number" || !Number.isFinite(value)) return void 0;
546
+ return value;
547
+ }
548
+ function asNonnegativeNumber(value) {
549
+ const number = asFiniteNumber(value);
550
+ return number === void 0 || number < 0 ? void 0 : number;
551
+ }
552
+
553
+ // src/providers/opencode-zen.ts
554
+ var ZEN_WINDOWS = [
555
+ { key: "rolling", label: "Rolling" },
556
+ { key: "weekly", label: "Weekly" },
557
+ { key: "monthly", label: "Monthly" }
558
+ ];
559
+ function normalizeOpenCodeZenPayload(payload, capturedAt) {
560
+ const usage = asObject3(payload.usage);
561
+ if (!usage) throw new Error("OpenCode Zen usage response was not an object.");
562
+ const buckets = [];
563
+ const notes = [];
564
+ for (const window of ZEN_WINDOWS) {
565
+ const raw = asObject3(usage[window.key]);
566
+ if (!raw) continue;
567
+ const status = asString3(raw.status);
568
+ if (status !== "ok" && status !== "rate-limited") {
569
+ notes.push(`${window.label} window unavailable (${status ?? "unknown status"}).`);
570
+ continue;
571
+ }
572
+ const used = asNonnegativeNumber2(raw.percent);
573
+ if (used === void 0) continue;
574
+ const resetsAt = asEpochSeconds(raw.resetsAt);
575
+ buckets.push({
576
+ id: window.key,
577
+ label: `${window.label} window`,
578
+ used,
579
+ remaining: 100 - clampPercent2(used),
580
+ limit: 100,
581
+ unit: "percent",
582
+ ...resetsAt !== void 0 ? { resetsAt } : {}
583
+ });
584
+ }
585
+ if (buckets.length === 0) {
586
+ throw new Error("OpenCode Zen usage endpoint returned no displayable usage data.");
587
+ }
588
+ return {
589
+ providerId: "opencode-go",
590
+ providerName: "OpenCode Go",
591
+ capturedAt,
592
+ source: "opencode-zen-usage",
593
+ semantics: {
594
+ kind: "consumer-subscription",
595
+ label: "OpenCode Zen plan usage"
596
+ },
597
+ buckets,
598
+ metrics: [],
599
+ ...notes.length > 0 ? { notes } : {}
600
+ };
601
+ }
602
+ function asObject3(value) {
603
+ if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
604
+ return value;
605
+ }
606
+ function asString3(value) {
607
+ if (typeof value !== "string") return void 0;
608
+ return sanitizeDisplayText(value, 80) || void 0;
609
+ }
610
+ function asNonnegativeNumber2(value) {
611
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 0) return void 0;
612
+ return value;
613
+ }
614
+ function asEpochSeconds(value) {
615
+ if (typeof value !== "string" || !value.trim()) return void 0;
616
+ const parsed = Date.parse(value);
617
+ if (Number.isNaN(parsed)) return void 0;
618
+ return Math.floor(parsed / 1e3);
619
+ }
620
+ function clampPercent2(value) {
621
+ return Math.min(100, Math.max(0, value));
622
+ }
623
+
624
+ // src/providers/openrouter.ts
625
+ function normalizeOpenRouterKeyPayload(payload, capturedAt) {
626
+ const data = asObject4(payload.data);
627
+ if (!data) throw new Error("OpenRouter key response data was not an object.");
628
+ const limit = asNonnegativeNumber3(data.limit);
629
+ const remaining = asNonnegativeNumber3(data.limit_remaining);
630
+ const period = asString4(data.limit_reset);
631
+ const totalUsage = asNonnegativeNumber3(data.usage);
632
+ const buckets = [];
633
+ if (limit !== void 0) {
634
+ buckets.push({
635
+ id: "key-limit",
636
+ label: "Key limit",
637
+ ...remaining !== void 0 ? { used: Math.max(0, limit - remaining), remaining } : {},
638
+ limit,
639
+ unit: "usd",
640
+ ...period ? { period } : {}
641
+ });
642
+ }
643
+ const metrics = [];
644
+ addUsageMetric(metrics, "usage-daily", "Usage today", data.usage_daily);
645
+ addUsageMetric(metrics, "usage-weekly", "Usage this week", data.usage_weekly);
646
+ addUsageMetric(metrics, "usage-monthly", "Usage this month", data.usage_monthly);
647
+ addUsageMetric(metrics, "usage-total", "All-time usage", totalUsage);
648
+ if (buckets.length === 0 && metrics.length === 0) {
649
+ throw new Error("OpenRouter key response returned no displayable usage data.");
650
+ }
651
+ const notes = [];
652
+ if (data.limit === null) notes.push("No per-key spend cap");
653
+ else if (limit === void 0) notes.push("Per-key spend cap unavailable");
654
+ if (data.is_free_tier === true) notes.push("Free-tier API key");
655
+ return {
656
+ providerId: "openrouter",
657
+ providerName: "OpenRouter",
658
+ capturedAt,
659
+ source: "openrouter-key",
660
+ semantics: { kind: "api-key", label: "API-key spend limits" },
661
+ accountLabel: asString4(data.label),
662
+ buckets,
663
+ metrics,
664
+ ...notes.length > 0 ? { notes } : {}
665
+ };
666
+ }
667
+ function addUsageMetric(metrics, id, label, value) {
668
+ const amount = typeof value === "number" ? asNonnegativeNumber3(value) : void 0;
669
+ if (amount === void 0) return;
670
+ metrics.push({ id, label, value: amount, unit: "usd" });
671
+ }
672
+ function asObject4(value) {
673
+ if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
674
+ return value;
675
+ }
676
+ function asString4(value) {
677
+ if (typeof value !== "string") return void 0;
678
+ return sanitizeDisplayText(value, 80) || void 0;
679
+ }
680
+ function asNonnegativeNumber3(value) {
681
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 0) return void 0;
682
+ return value;
683
+ }
684
+
685
+ // src/query.ts
686
+ var CODEX_USAGE_URL = "https://chatgpt.com/backend-api/wham/usage";
687
+ var GITHUB_COPILOT_USAGE_URL = "https://api.github.com/copilot_internal/user";
688
+ var OPENROUTER_KEY_URL = "https://openrouter.ai/api/v1/key";
689
+ var MAX_SUCCESS_BODY_BYTES = 64 * 1024;
690
+ var MAX_ERROR_BODY_BYTES = 4 * 1024;
691
+ var AUTH_FINGERPRINT_SALT = randomBytes(32);
692
+ var SUPPORTED_ADAPTERS = [
693
+ {
694
+ id: "openai-codex",
695
+ displayName: "OpenAI Codex",
696
+ semantics: {
697
+ kind: "consumer-subscription",
698
+ label: "ChatGPT subscription limits"
699
+ },
700
+ async query(auth, signal, timeoutMs) {
701
+ const payload = await fetchProviderJson(
702
+ CODEX_USAGE_URL,
703
+ auth,
704
+ signal,
705
+ timeoutMs,
706
+ "Codex usage endpoint"
707
+ );
708
+ return normalizeCodexBackendPayload(payload, Date.now());
709
+ }
710
+ },
711
+ {
712
+ id: "github-copilot",
713
+ displayName: "GitHub Copilot",
714
+ semantics: {
715
+ kind: "consumer-subscription",
716
+ label: "GitHub Copilot account allowance"
717
+ },
718
+ async query(auth, signal, timeoutMs) {
719
+ const payload = await fetchProviderJson(
720
+ GITHUB_COPILOT_USAGE_URL,
721
+ auth,
722
+ signal,
723
+ timeoutMs,
724
+ "GitHub Copilot usage endpoint"
725
+ );
726
+ return normalizeGitHubCopilotUsagePayload(payload, Date.now());
727
+ }
728
+ },
729
+ {
730
+ id: "openrouter",
731
+ displayName: "OpenRouter",
732
+ semantics: { kind: "api-key", label: "API-key spend limits" },
733
+ async query(auth, signal, timeoutMs) {
734
+ const payload = await fetchProviderJson(
735
+ OPENROUTER_KEY_URL,
736
+ auth,
737
+ signal,
738
+ timeoutMs,
739
+ "OpenRouter key endpoint"
740
+ );
741
+ return normalizeOpenRouterKeyPayload(payload, Date.now());
742
+ }
743
+ },
744
+ {
745
+ id: "opencode-go",
746
+ displayName: "OpenCode Go",
747
+ semantics: { kind: "consumer-subscription", label: "OpenCode Zen plan usage" },
748
+ async query(auth, signal, timeoutMs) {
749
+ const payload = await fetchProviderJson(
750
+ opencodeUsageUrl(auth.model.baseUrl),
751
+ auth,
752
+ signal,
753
+ timeoutMs,
754
+ "OpenCode Zen usage endpoint"
755
+ );
756
+ return normalizeOpenCodeZenPayload(payload, Date.now());
757
+ }
758
+ }
759
+ ];
760
+ function adapterForProvider(providerId) {
761
+ return SUPPORTED_ADAPTERS.find((adapter) => adapter.id === providerId);
762
+ }
763
+ function isStaleExtensionContextError(error) {
764
+ return error instanceof Error && error.message.includes("This extension ctx is stale after session replacement or reload");
765
+ }
766
+ async function resolveUsageAuth(ctx, adapter, salt = AUTH_FINGERPRINT_SALT, credentialReader = readStoredCredential2, candidateReader) {
767
+ if (ctx.model?.provider === adapter.id && !hasOfficialOrigin(ctx.model, adapter.id)) {
768
+ throw new Error(
769
+ `${adapter.displayName} usage cannot send a custom provider base URL credential to the official usage endpoint.`
770
+ );
771
+ }
772
+ const model = candidateModels(ctx, adapter.id).find(
773
+ (candidate) => hasOfficialOrigin(candidate, adapter.id)
774
+ );
775
+ if (!model) return void 0;
776
+ const registry = ctx.modelRegistry;
777
+ let modelAuth;
778
+ if (ctx.model?.provider === adapter.id && typeof registry.getApiKeyAndHeaders === "function") {
779
+ const result = await registry.getApiKeyAndHeaders(ctx.model);
780
+ if (!result.ok) throw new Error(redactUsageError(result.error));
781
+ if (authorizationFrom(result)) modelAuth = result;
782
+ }
783
+ if (typeof registry.getProviderAuth !== "function") {
784
+ throw new Error("pi-usage requires Pi 0.81.0 or newer to validate resolved provider auth.");
785
+ }
786
+ const providerResult = await registry.getProviderAuth(adapter.id);
787
+ if (providerResult?.auth.baseUrl && !hasOfficialUrlOrigin(providerResult.auth.baseUrl, adapter.id)) {
788
+ throw new Error(
789
+ `${adapter.displayName} usage cannot send a proxy-resolved credential to the official usage endpoint.`
790
+ );
791
+ }
792
+ const auth = modelAuth ?? providerResult?.auth;
793
+ if (!auth) return void 0;
794
+ if (adapter.id === "github-copilot") {
795
+ const offered = candidateReader ? candidateReader(ctx, adapter.id) : fallbackOAuthCredentialCandidates(adapter.id, credentialReader);
796
+ if (!offered.ok) {
797
+ throw new Error("GitHub Copilot OAuth credential discovery failed closed.");
798
+ }
799
+ return resolveGitHubCopilotUsageAuth(
800
+ auth,
801
+ model,
802
+ salt,
803
+ offered.candidates,
804
+ offered.offeredCount === 0
805
+ );
806
+ }
807
+ const authorization = authorizationFrom(auth);
808
+ if (!authorization) return void 0;
809
+ const headers = { Authorization: authorization };
810
+ const secrets = [auth.apiKey, headerValue(auth.headers, "Authorization"), authorization].filter(
811
+ (value) => Boolean(value)
812
+ );
813
+ return {
814
+ apiKey: auth.apiKey,
815
+ headers,
816
+ fingerprint: fingerprintResolvedAuth({ headers }, salt),
817
+ secrets,
818
+ model
819
+ };
820
+ }
821
+ async function queryProviderUsage(adapter, auth, signal, timeoutMs) {
822
+ try {
823
+ return await adapter.query(auth, signal, timeoutMs);
824
+ } catch (error) {
825
+ if (isStaleExtensionContextError(error) || isAbortError(error)) throw error;
826
+ throw new Error(redactUsageError(errorMessage(error), auth.secrets));
827
+ }
828
+ }
829
+ function providerIsConfigured(ctx, providerId) {
830
+ try {
831
+ return ctx.modelRegistry.getProviderAuthStatus(providerId).configured;
832
+ } catch {
833
+ return candidateModels(ctx, providerId).length > 0;
834
+ }
835
+ }
836
+ function candidateModels(ctx, providerId) {
837
+ const candidates = [];
838
+ const seen = /* @__PURE__ */ new Set();
839
+ const add = (model) => {
840
+ if (!model || model.provider !== providerId) return;
841
+ const key = `${model.provider}/${model.id}`;
842
+ if (seen.has(key)) return;
843
+ seen.add(key);
844
+ candidates.push(model);
845
+ };
846
+ add(ctx.model);
847
+ for (const model of ctx.modelRegistry.getAvailable()) add(model);
848
+ for (const model of ctx.modelRegistry.getAll()) add(model);
849
+ return candidates;
850
+ }
851
+ async function fetchProviderJson(url, auth, signal, timeoutMs, description, request = {}) {
852
+ const controller = new AbortController();
853
+ let timedOut = false;
854
+ const abortFromCaller = () => controller.abort();
855
+ if (signal.aborted) controller.abort();
856
+ else signal.addEventListener("abort", abortFromCaller, { once: true });
857
+ const timeout = setTimeout(() => {
858
+ timedOut = true;
859
+ controller.abort();
860
+ }, timeoutMs);
861
+ try {
862
+ const headers = { ...auth.headers };
863
+ if (!hasHeader(headers, "User-Agent")) headers["User-Agent"] = "pi-usage";
864
+ if (request.body && !hasHeader(headers, "Content-Type")) {
865
+ headers["Content-Type"] = "application/json";
866
+ }
867
+ const response = await fetch(url, {
868
+ method: request.method ?? "GET",
869
+ headers,
870
+ ...request.body ? { body: JSON.stringify(request.body) } : {},
871
+ signal: controller.signal
872
+ });
873
+ if (controller.signal.aborted)
874
+ throw Object.assign(new Error("Usage query aborted."), { name: "AbortError" });
875
+ const text = await readBoundedResponse(
876
+ response,
877
+ response.ok ? MAX_SUCCESS_BODY_BYTES : MAX_ERROR_BODY_BYTES,
878
+ !response.ok,
879
+ description
880
+ );
881
+ if (controller.signal.aborted)
882
+ throw Object.assign(new Error("Usage query aborted."), { name: "AbortError" });
883
+ if (!response.ok) {
884
+ throw new Error(
885
+ `${description} returned ${response.status} ${response.statusText}: ${redactUsageError(text, auth.secrets)}`
886
+ );
887
+ }
888
+ let parsed;
889
+ try {
890
+ parsed = JSON.parse(text);
891
+ } catch (error) {
892
+ throw new Error(`${description} returned invalid JSON: ${errorMessage(error)}`);
893
+ }
894
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
895
+ throw new Error(`${description} response was not an object.`);
896
+ }
897
+ return parsed;
898
+ } catch (error) {
899
+ if (timedOut) {
900
+ throw new Error(`Timed out after ${Math.round(timeoutMs / 1e3)}s while fetching usage.`);
901
+ }
902
+ if (signal.aborted)
903
+ throw Object.assign(new Error("Usage query aborted."), { name: "AbortError" });
904
+ throw error;
905
+ } finally {
906
+ clearTimeout(timeout);
907
+ signal.removeEventListener("abort", abortFromCaller);
908
+ }
909
+ }
910
+ async function readBoundedResponse(response, maxBytes, truncateOverflow, description) {
911
+ if (!response.body) return "";
912
+ const reader = response.body.getReader();
913
+ const chunks = [];
914
+ let total = 0;
915
+ let truncated = false;
916
+ try {
917
+ while (true) {
918
+ const { done, value } = await reader.read();
919
+ if (done) break;
920
+ const remaining = maxBytes - total;
921
+ if (value.byteLength > remaining) {
922
+ if (remaining > 0) chunks.push(value.subarray(0, remaining));
923
+ total = maxBytes;
924
+ truncated = true;
925
+ await reader.cancel();
926
+ break;
927
+ }
928
+ chunks.push(value);
929
+ total += value.byteLength;
930
+ }
931
+ } finally {
932
+ reader.releaseLock();
933
+ }
934
+ if (truncated && !truncateOverflow) {
935
+ throw new Error(`${description} response exceeded ${maxBytes} bytes.`);
936
+ }
937
+ const body = new Uint8Array(total);
938
+ let offset = 0;
939
+ for (const chunk of chunks) {
940
+ body.set(chunk, offset);
941
+ offset += chunk.byteLength;
942
+ }
943
+ const text = new TextDecoder().decode(body);
944
+ return truncated ? `${text}\u2026` : text;
945
+ }
946
+ function resolveGitHubCopilotUsageAuth(auth, model, salt, candidates, standaloneFallback) {
947
+ const resolvedAccess = bearerToken(headerValue(auth.headers, "Authorization")) ?? auth.apiKey;
948
+ if (!resolvedAccess) throw new Error("GitHub Copilot OAuth credentials were incomplete.");
949
+ let sawOAuth = false;
950
+ let sawMatchingAccess = false;
951
+ let sawIncompleteMatch = false;
952
+ let sawEnterpriseMatch = false;
953
+ const matches = /* @__PURE__ */ new Map();
954
+ for (const candidate of candidates) {
955
+ try {
956
+ const credential = asObject5(candidate);
957
+ if (credential?.type !== "oauth") continue;
958
+ sawOAuth = true;
959
+ const storedAccess2 = typeof credential.access === "string" && credential.access ? credential.access : void 0;
960
+ if (storedAccess2 !== resolvedAccess) continue;
961
+ sawMatchingAccess = true;
962
+ const enterpriseUrl = credential.enterpriseUrl;
963
+ if (typeof enterpriseUrl === "string" && enterpriseUrl && !isPublicGitHubDomain(enterpriseUrl)) {
964
+ sawEnterpriseMatch = true;
965
+ continue;
966
+ }
967
+ const refresh2 = typeof credential.refresh === "string" && credential.refresh ? credential.refresh : void 0;
968
+ if (!refresh2) {
969
+ sawIncompleteMatch = true;
970
+ continue;
971
+ }
972
+ matches.set(`${storedAccess2.length}:${storedAccess2}${refresh2}`, { refresh: refresh2, storedAccess: storedAccess2 });
973
+ } catch {
974
+ }
975
+ }
976
+ if (sawEnterpriseMatch) {
977
+ throw new Error("GitHub Copilot usage does not yet support GitHub Enterprise accounts.");
978
+ }
979
+ if (sawIncompleteMatch) throw new Error("GitHub Copilot OAuth credentials were incomplete.");
980
+ if (matches.size > 1) {
981
+ throw new Error(
982
+ "Conflicting OAuth credentials match the active GitHub Copilot runtime account."
983
+ );
984
+ }
985
+ const match = matches.values().next().value;
986
+ if (!match) {
987
+ if (!sawOAuth) {
988
+ throw new Error(
989
+ standaloneFallback ? "GitHub Copilot usage requires the OAuth account configured through Pi /login." : "GitHub Copilot usage requires an OAuth account configured through Pi /login or a compatible credential source."
990
+ );
991
+ }
992
+ if (sawMatchingAccess) throw new Error("GitHub Copilot OAuth credentials were incomplete.");
993
+ throw new Error(
994
+ standaloneFallback ? "The active GitHub Copilot runtime account does not match Pi's stored OAuth account." : "The active GitHub Copilot runtime account does not match any available OAuth account."
995
+ );
996
+ }
997
+ const { refresh, storedAccess } = match;
998
+ const authorization = `Bearer ${refresh}`;
999
+ const headers = {
1000
+ Authorization: authorization,
1001
+ "X-GitHub-Api-Version": "2025-05-01"
1002
+ };
1003
+ return {
1004
+ apiKey: refresh,
1005
+ headers,
1006
+ fingerprint: fingerprintResolvedAuth({ headers }, salt),
1007
+ secrets: [refresh, storedAccess, resolvedAccess, authorization],
1008
+ model
1009
+ };
1010
+ }
1011
+ function authorizationFrom(auth) {
1012
+ return headerValue(auth.headers, "Authorization") ?? (auth.apiKey ? `Bearer ${auth.apiKey}` : void 0);
1013
+ }
1014
+ function bearerToken(authorization) {
1015
+ const match = /^Bearer\s+(.+)$/iu.exec(authorization ?? "");
1016
+ return match?.[1];
1017
+ }
1018
+ function asObject5(value) {
1019
+ if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
1020
+ return value;
1021
+ }
1022
+ function isPublicGitHubDomain(value) {
1023
+ try {
1024
+ const url = new URL(value.includes("://") ? value : `https://${value}`);
1025
+ return url.hostname.toLowerCase() === "github.com";
1026
+ } catch {
1027
+ return false;
1028
+ }
1029
+ }
1030
+ function hasOfficialOrigin(model, providerId) {
1031
+ return hasOfficialUrlOrigin(model.baseUrl, providerId);
1032
+ }
1033
+ function hasOfficialUrlOrigin(value, providerId) {
1034
+ try {
1035
+ const url = new URL(value);
1036
+ if (providerId === "openai-codex") return url.origin === "https://chatgpt.com";
1037
+ if (providerId === "openrouter") return url.origin === "https://openrouter.ai";
1038
+ if (providerId === "opencode-go") return url.origin === "https://opencode.ai";
1039
+ if (providerId === "github-copilot") {
1040
+ return url.protocol === "https:" && /^api\.[a-z0-9-]+\.githubcopilot\.com$/u.test(url.hostname);
1041
+ }
1042
+ return false;
1043
+ } catch {
1044
+ return false;
1045
+ }
1046
+ }
1047
+ function headerValue(headers, name) {
1048
+ const entry = Object.entries(headers ?? {}).find(
1049
+ ([candidate]) => candidate.toLowerCase() === name.toLowerCase()
1050
+ );
1051
+ return entry?.[1] ?? void 0;
1052
+ }
1053
+ function hasHeader(headers, name) {
1054
+ return Object.keys(headers).some((key) => key.toLowerCase() === name.toLowerCase());
1055
+ }
1056
+ function opencodeUsageUrl(baseUrl) {
1057
+ const base = baseUrl?.trim().replace(/\/+$/u, "");
1058
+ if (!base) throw new Error("OpenCode Go model base URL is unavailable.");
1059
+ return `${base}/usage`;
1060
+ }
1061
+ function isAbortError(error) {
1062
+ return error instanceof Error && error.name === "AbortError";
1063
+ }
1064
+
1065
+ // src/codex-resets.ts
1066
+ var CODEX_RESET_CREDITS_URL = "https://chatgpt.com/backend-api/wham/rate-limit-reset-credits";
1067
+ var CODEX_RESET_CONSUME_URL = `${CODEX_RESET_CREDITS_URL}/consume`;
1068
+ var MAX_RESET_OPTIONS = 32;
1069
+ var MAX_CREDIT_ID_CHARS = 1024;
1070
+ function codexResetCount(report) {
1071
+ const value = report.metrics.find((metric) => metric.id === "reset-credits")?.value;
1072
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : void 0;
1073
+ }
1074
+ function codexResetActionDescription(report) {
1075
+ const count = codexResetCount(report);
1076
+ if (count === void 0) return "Check reset availability.";
1077
+ if (count === 0) return "No usage limit resets available.";
1078
+ return `You have ${count} ${resetLabel(count)} available.`;
1079
+ }
1080
+ function genericCodexResetOption() {
1081
+ return {
1082
+ title: "Full reset",
1083
+ description: "Reset your current usage limits."
1084
+ };
1085
+ }
1086
+ function resetOptionExpiration(option) {
1087
+ if (option.expiresAt === void 0) return "Does not expire.";
1088
+ const expiration = new Date(option.expiresAt * 1e3);
1089
+ if (Number.isNaN(expiration.getTime())) return "Expiration unavailable.";
1090
+ return `Expires ${expiration.toLocaleString()}.`;
1091
+ }
1092
+ function resetConfirmationLines(option) {
1093
+ if (!option) return ["The selected reset is unavailable."];
1094
+ return [
1095
+ option.title,
1096
+ resetOptionExpiration(option),
1097
+ option.description,
1098
+ "This consumes one earned reset for the current OpenAI Codex account."
1099
+ ];
1100
+ }
1101
+ function formatCodexResetOutcome(outcome, remainingCount) {
1102
+ const remaining = remainingCount === void 0 ? "" : ` You have ${remainingCount} ${resetLabel(remainingCount)} left.`;
1103
+ if (!outcome) return "You don't have any usage limit resets available.";
1104
+ if (outcome.code === "reset") return `Usage reset.${remaining}`.trim();
1105
+ if (outcome.code === "already_redeemed") {
1106
+ return `Usage reset was already completed.${remaining}`.trim();
1107
+ }
1108
+ if (outcome.code === "nothing_to_reset") {
1109
+ return "Your usage does not need a reset right now.";
1110
+ }
1111
+ return "No usage limit resets are available.";
1112
+ }
1113
+ function resetLabel(count) {
1114
+ return count === 1 ? "usage limit reset" : "usage limit resets";
1115
+ }
1116
+ async function resolveCodexResetAuth(ctx, salt = AUTH_FINGERPRINT_SALT, credentialReader = readStoredCredential3, candidateReader) {
1117
+ const model = ctx.model;
1118
+ if (model?.provider !== "openai-codex") {
1119
+ throw new Error("Usage limit resets require the current model to use OpenAI Codex.");
1120
+ }
1121
+ const expectedModel = `${model.provider}/${model.id}`;
1122
+ const adapter = adapterForProvider("openai-codex");
1123
+ if (!adapter) throw new Error("OpenAI Codex usage support is unavailable.");
1124
+ const auth = await resolveUsageAuth(ctx, adapter, salt, credentialReader);
1125
+ if (`${ctx.model?.provider}/${ctx.model?.id}` !== expectedModel) {
1126
+ throw new Error("The current model changed while resolving Codex reset authentication.");
1127
+ }
1128
+ if (!auth) throw new Error("No runtime credential is configured for OpenAI Codex.");
1129
+ const resolvedAccess = bearerToken2(headerValue2(auth.headers, "Authorization")) ?? auth.apiKey;
1130
+ if (!resolvedAccess) throw new Error("OpenAI Codex OAuth credentials were incomplete.");
1131
+ const resolvedAccountId = codexAccountIdFromAccessToken(resolvedAccess);
1132
+ if (!resolvedAccountId) {
1133
+ throw new Error("The active OpenAI Codex access token did not contain a valid account ID.");
1134
+ }
1135
+ const offered = candidateReader ? candidateReader(ctx, "openai-codex") : fallbackOAuthCredentialCandidates("openai-codex", credentialReader);
1136
+ if (!offered.ok) throw new Error("OpenAI Codex OAuth credential discovery failed closed.");
1137
+ const { accountId, storedAccess } = selectCodexResetCredential(
1138
+ offered.candidates,
1139
+ resolvedAccess,
1140
+ resolvedAccountId,
1141
+ offered.offeredCount === 0
1142
+ );
1143
+ const authorization = `Bearer ${resolvedAccess}`;
1144
+ const headers = {
1145
+ Authorization: authorization,
1146
+ "chatgpt-account-id": accountId
1147
+ };
1148
+ return {
1149
+ apiKey: resolvedAccess,
1150
+ headers,
1151
+ fingerprint: fingerprintResolvedAuth({ headers }, salt),
1152
+ secrets: [
1153
+ .../* @__PURE__ */ new Set([...auth.secrets, storedAccess, resolvedAccess, authorization, accountId])
1154
+ ],
1155
+ model: auth.model
1156
+ };
1157
+ }
1158
+ async function listCodexResetCredits(auth, signal, timeoutMs) {
1159
+ const payload = await fetchProviderJson(
1160
+ CODEX_RESET_CREDITS_URL,
1161
+ auth,
1162
+ signal,
1163
+ timeoutMs,
1164
+ "Codex usage-limit reset endpoint"
1165
+ );
1166
+ return normalizeCodexResetCreditsPayload(payload);
1167
+ }
1168
+ async function consumeCodexResetCredit(auth, option, redeemRequestId, signal, timeoutMs) {
1169
+ if (!redeemRequestId) throw new Error("Codex reset redemption request ID must not be empty.");
1170
+ const payload = await fetchProviderJson(
1171
+ CODEX_RESET_CONSUME_URL,
1172
+ auth,
1173
+ signal,
1174
+ timeoutMs,
1175
+ "Codex usage-limit reset consume endpoint",
1176
+ {
1177
+ method: "POST",
1178
+ body: {
1179
+ redeem_request_id: redeemRequestId,
1180
+ ...option.creditId ? { credit_id: option.creditId } : {}
1181
+ }
1182
+ }
1183
+ );
1184
+ const code = payload.code;
1185
+ if (!isCodexResetOutcomeCode(code)) {
1186
+ throw new Error("Codex reset consume endpoint returned an unknown outcome code.");
1187
+ }
1188
+ const windowsReset = payload.windows_reset === void 0 ? 0 : nonnegativeInteger(payload.windows_reset);
1189
+ if (windowsReset === void 0) {
1190
+ throw new Error("Codex reset consume endpoint returned an invalid windows_reset value.");
1191
+ }
1192
+ return { code, windowsReset };
1193
+ }
1194
+ function normalizeCodexResetCreditsPayload(payload) {
1195
+ const availableCount = nonnegativeInteger(payload.available_count);
1196
+ if (availableCount === void 0) {
1197
+ throw new Error("Codex reset credits response returned an invalid available_count.");
1198
+ }
1199
+ const rawCredits = payload.credits;
1200
+ if (rawCredits !== void 0 && !Array.isArray(rawCredits)) {
1201
+ throw new Error("Codex reset credits response returned invalid credits.");
1202
+ }
1203
+ const options = (rawCredits ?? []).map(asObject6).filter((credit) => Boolean(credit)).filter((credit) => credit.status === "available" && credit.reset_type === "codex_rate_limits").map(normalizeResetOption).sort(
1204
+ (left, right) => (left.expiresAt ?? Number.MAX_SAFE_INTEGER) - (right.expiresAt ?? Number.MAX_SAFE_INTEGER)
1205
+ ).slice(0, Math.min(availableCount, MAX_RESET_OPTIONS));
1206
+ if (availableCount > 0 && options.length === 0) {
1207
+ options.push(genericCodexResetOption());
1208
+ }
1209
+ return { availableCount, options };
1210
+ }
1211
+ function selectCodexResetCredential(candidates, resolvedAccess, resolvedAccountId, standaloneFallback) {
1212
+ let sawOAuth = false;
1213
+ let sawMatchingAccess = false;
1214
+ let sawInvalidAccountId = false;
1215
+ const matches = /* @__PURE__ */ new Map();
1216
+ for (const candidate of candidates) {
1217
+ try {
1218
+ const credential = asObject6(candidate);
1219
+ if (credential?.type !== "oauth") continue;
1220
+ sawOAuth = true;
1221
+ const storedAccess = asNonemptyString(credential.access);
1222
+ if (storedAccess !== resolvedAccess) continue;
1223
+ sawMatchingAccess = true;
1224
+ const accountId = validHeaderValue(credential.accountId);
1225
+ const refresh = asNonemptyString(credential.refresh);
1226
+ if (!accountId || accountId !== resolvedAccountId || !refresh) {
1227
+ sawInvalidAccountId = true;
1228
+ continue;
1229
+ }
1230
+ matches.set(refresh, { accountId, storedAccess });
1231
+ } catch {
1232
+ }
1233
+ }
1234
+ if (sawInvalidAccountId) {
1235
+ throw new Error("The OpenAI Codex OAuth credential did not include a valid account ID.");
1236
+ }
1237
+ if (matches.size > 1) {
1238
+ throw new Error("Conflicting OAuth credentials match the active OpenAI Codex runtime account.");
1239
+ }
1240
+ const match = matches.values().next().value;
1241
+ if (match) return match;
1242
+ if (!sawOAuth) {
1243
+ throw new Error(
1244
+ standaloneFallback ? "Usage limit resets require the OpenAI Codex OAuth account configured through Pi /login." : "Usage limit resets require an OpenAI Codex OAuth account configured through Pi /login or a compatible credential source."
1245
+ );
1246
+ }
1247
+ if (sawMatchingAccess) throw new Error("OpenAI Codex OAuth credentials were incomplete.");
1248
+ throw new Error(
1249
+ standaloneFallback ? "The active OpenAI Codex runtime account does not match Pi's stored OAuth account." : "The active OpenAI Codex runtime account does not match any available OAuth account."
1250
+ );
1251
+ }
1252
+ function codexAccountIdFromAccessToken(access) {
1253
+ try {
1254
+ const parts = access.split(".");
1255
+ if (parts.length !== 3 || !parts[1]) return void 0;
1256
+ const payload = JSON.parse(Buffer.from(parts[1], "base64url").toString("utf8"));
1257
+ const claims = asObject6(asObject6(payload)?.["https://api.openai.com/auth"]);
1258
+ return validHeaderValue(claims?.chatgpt_account_id);
1259
+ } catch {
1260
+ return void 0;
1261
+ }
1262
+ }
1263
+ function normalizeResetOption(credit) {
1264
+ const creditId = asOpaqueId(credit.id);
1265
+ if (!creditId) throw new Error("Codex reset credits response returned an invalid credit ID.");
1266
+ let expiresAt;
1267
+ if (credit.expires_at !== void 0 && credit.expires_at !== null) {
1268
+ if (typeof credit.expires_at !== "string") {
1269
+ throw new Error("Codex reset credits response returned an invalid expiration time.");
1270
+ }
1271
+ const parsed = Date.parse(credit.expires_at);
1272
+ if (!Number.isFinite(parsed)) {
1273
+ throw new Error("Codex reset credits response returned an invalid expiration time.");
1274
+ }
1275
+ expiresAt = Math.floor(parsed / 1e3);
1276
+ }
1277
+ const title = displayString(credit.title) ?? "Full reset";
1278
+ const description = displayString(credit.description) ?? "Reset your current usage limits.";
1279
+ return {
1280
+ creditId,
1281
+ title,
1282
+ description,
1283
+ ...expiresAt === void 0 ? {} : { expiresAt }
1284
+ };
1285
+ }
1286
+ function isCodexResetOutcomeCode(value) {
1287
+ return value === "reset" || value === "nothing_to_reset" || value === "no_credit" || value === "already_redeemed";
1288
+ }
1289
+ function asObject6(value) {
1290
+ if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
1291
+ return value;
1292
+ }
1293
+ function asNonemptyString(value) {
1294
+ return typeof value === "string" && value.length > 0 ? value : void 0;
1295
+ }
1296
+ function asOpaqueId(value) {
1297
+ if (typeof value !== "string" || value.length === 0 || value.length > MAX_CREDIT_ID_CHARS) {
1298
+ return void 0;
1299
+ }
1300
+ return value;
1301
+ }
1302
+ function displayString(value) {
1303
+ if (typeof value !== "string") return void 0;
1304
+ return sanitizeDisplayText(value, 160) || void 0;
1305
+ }
1306
+ function validHeaderValue(value) {
1307
+ if (typeof value !== "string" || !value || value.length > 512) return void 0;
1308
+ if (/[^\x20-\x7e]/u.test(value)) return void 0;
1309
+ return value;
1310
+ }
1311
+ function nonnegativeInteger(value) {
1312
+ const parsed = typeof value === "number" ? value : typeof value === "string" && value.trim() ? Number(value) : Number.NaN;
1313
+ if (!Number.isSafeInteger(parsed) || parsed < 0) return void 0;
1314
+ return parsed;
1315
+ }
1316
+ function bearerToken2(authorization) {
1317
+ return /^Bearer\s+(.+)$/iu.exec(authorization ?? "")?.[1];
1318
+ }
1319
+ function headerValue2(headers, name) {
1320
+ return Object.entries(headers).find(
1321
+ ([candidate]) => candidate.toLowerCase() === name.toLowerCase()
1322
+ )?.[1];
1323
+ }
1324
+
1325
+ // src/format.ts
1326
+ var BAR_SEGMENTS = 20;
1327
+ var VALUE_COLUMN = 29;
1328
+ function formatUsageReport(report, displayState) {
1329
+ const stateLabel = displayState === "current" ? "Current" : "Configured";
1330
+ const lines = [`${report.providerName} Usage \xB7 ${stateLabel}`];
1331
+ if (report.accountLabel) lines.push(`Account: ${report.accountLabel}`);
1332
+ lines.push(`Semantics: ${report.semantics.label}`, "");
1333
+ if (report.providerId === "openai-codex") formatCodexReport(lines, report);
1334
+ else if (report.providerId === "github-copilot") formatGitHubCopilotReport(lines, report);
1335
+ else if (report.providerId === "openrouter") formatOpenRouterReport(lines, report);
1336
+ else if (report.providerId === "opencode-go") formatOpenCodeZenReport(lines, report);
1337
+ else formatGenericReport(lines, report);
1338
+ if (report.notes) {
1339
+ for (const note of report.notes) lines.push(note);
1340
+ }
1341
+ return lines.join("\n").trimEnd();
1342
+ }
1343
+ function formatUsageStatusline(report, model) {
1344
+ if (report.providerId === "openai-codex") return formatCodexStatusline(report, model);
1345
+ if (report.providerId === "github-copilot") return formatGitHubCopilotStatusline(report);
1346
+ if (report.providerId === "openrouter") {
1347
+ const limit = report.buckets.find((bucket) => bucket.id === "key-limit");
1348
+ if (limit?.remaining !== void 0) return `openrouter ${formatUsd(limit.remaining)} left`;
1349
+ const total = report.metrics.find((metric) => metric.id === "usage-total");
1350
+ if (typeof total?.value === "number") return `openrouter ${formatUsd(total.value)} used`;
1351
+ }
1352
+ if (report.providerId === "opencode-go") return formatOpenCodeZenStatusline(report);
1353
+ return void 0;
1354
+ }
1355
+ function formatProviderStates(states) {
1356
+ return states.map((state) => {
1357
+ if (state.status === "ready") return formatUsageReport(state.report, state.displayState);
1358
+ const label = state.displayState === "current" ? "Current" : "Configured";
1359
+ const status = state.status === "auth-unavailable" ? "Authentication unavailable" : state.status === "unsupported" ? "Unsupported" : "Query failed";
1360
+ return `${state.providerName} \xB7 ${label}
1361
+ ${status}: ${state.message}`;
1362
+ }).join("\n\n");
1363
+ }
1364
+ function formatCodexReport(lines, report) {
1365
+ let previousGroup;
1366
+ for (const bucket of report.buckets) {
1367
+ const group = bucket.groupId ?? bucket.id;
1368
+ if (group !== previousGroup && group !== "codex") {
1369
+ lines.push(`${bucket.groupLabel ?? group} limit:`);
1370
+ }
1371
+ previousGroup = group;
1372
+ const fallback = bucket.id.endsWith(":secondary") ? "weekly" : "5h";
1373
+ const label = `${formatWindowLabel(bucket.windowMinutes, fallback, false)} limit:`;
1374
+ lines.push(`${label.padEnd(VALUE_COLUMN)}${formatPercentBucket(bucket)}`);
1375
+ }
1376
+ for (const metric of report.metrics) {
1377
+ if (metric.id === "reset-credits") {
1378
+ lines.push(`${"Usage limit resets:".padEnd(VALUE_COLUMN)}${metric.value} available`);
1379
+ } else if (metric.id === "credits") {
1380
+ lines.push(
1381
+ `${"Credits:".padEnd(VALUE_COLUMN)}${formatMetricValue(metric.value, metric.unit)}`
1382
+ );
1383
+ }
1384
+ }
1385
+ }
1386
+ function formatGitHubCopilotReport(lines, report) {
1387
+ const quota = findGitHubCopilotQuota(report);
1388
+ if (!quota || quota.limit === void 0 || quota.remaining === void 0) {
1389
+ lines.push(`${`${quota?.label ?? "Copilot quota"}:`.padEnd(VALUE_COLUMN)}unlimited`);
1390
+ return;
1391
+ }
1392
+ const percent = percentRemaining(quota);
1393
+ const reset = quota.resetsAt ? ` (resets ${formatReset(quota.resetsAt)})` : "";
1394
+ lines.push(
1395
+ `${`${quota.label}:`.padEnd(VALUE_COLUMN)}${quota.remaining} of ${quota.limit} left \xB7 ${percent}%${reset}`
1396
+ );
1397
+ const overage = report.metrics.find((metric) => metric.id === "overage-used");
1398
+ if (typeof overage?.value === "number" && overage.value > 0) {
1399
+ lines.push(`${"Additional usage:".padEnd(VALUE_COLUMN)}${overage.value} ${quota.label}`);
1400
+ }
1401
+ }
1402
+ function formatGitHubCopilotStatusline(report) {
1403
+ const quota = findGitHubCopilotQuota(report);
1404
+ const kind = compactGitHubCopilotQuotaKind(quota);
1405
+ if (!quota || quota.limit === void 0 || quota.remaining === void 0) {
1406
+ return `copilot ${kind} unlimited`;
1407
+ }
1408
+ const overage = report.metrics.find((metric) => metric.id === "overage-used");
1409
+ const overageSuffix = typeof overage?.value === "number" && overage.value > 0 ? ` +${overage.value} over` : "";
1410
+ return `copilot ${kind === "premium" ? "" : `${kind} `}${quota.remaining}/${quota.limit} ${percentRemaining(quota)}%${overageSuffix}`;
1411
+ }
1412
+ function findGitHubCopilotQuota(report) {
1413
+ return report.buckets.find(
1414
+ (bucket) => ["ai-credits", "premium-requests", "chat-requests"].includes(bucket.id)
1415
+ );
1416
+ }
1417
+ function compactGitHubCopilotQuotaKind(bucket) {
1418
+ if (bucket?.id === "ai-credits") return "credits";
1419
+ if (bucket?.id === "chat-requests") return "chat";
1420
+ return "premium";
1421
+ }
1422
+ function percentRemaining(bucket) {
1423
+ if (!bucket.limit || bucket.remaining === void 0) return 0;
1424
+ return Math.round(clampPercent3(bucket.remaining / bucket.limit * 100));
1425
+ }
1426
+ function formatOpenRouterReport(lines, report) {
1427
+ const limit = report.buckets.find((bucket) => bucket.id === "key-limit");
1428
+ if (limit) {
1429
+ const period = limit.period ? ` (${limit.period})` : "";
1430
+ const value = limit.remaining === void 0 ? `${formatUsd(limit.limit ?? 0)} cap; remaining unavailable` : `${formatUsd(limit.remaining)} of ${formatUsd(limit.limit ?? 0)} left`;
1431
+ lines.push(`${`Key limit${period}:`.padEnd(VALUE_COLUMN)}${value}`);
1432
+ }
1433
+ for (const metric of report.metrics) {
1434
+ lines.push(
1435
+ `${`${metric.label}:`.padEnd(VALUE_COLUMN)}${formatMetricValue(metric.value, metric.unit)}`
1436
+ );
1437
+ }
1438
+ }
1439
+ function formatOpenCodeZenReport(lines, report) {
1440
+ for (const bucket of report.buckets) {
1441
+ const reset = bucket.resetsAt ? ` (resets ${formatReset(bucket.resetsAt)})` : "";
1442
+ const used = bucket.used ?? "unavailable";
1443
+ lines.push(`${`${bucket.label}:`.padEnd(VALUE_COLUMN)}${used}% used${reset}`);
1444
+ }
1445
+ }
1446
+ function formatOpenCodeZenStatusline(report) {
1447
+ const parts = ["zen"];
1448
+ for (const bucket of report.buckets) {
1449
+ if (bucket.used === void 0) continue;
1450
+ const compact = bucket.id === "rolling" ? "r" : bucket.id === "weekly" ? "w" : "m";
1451
+ parts.push(`${clampPercent3(bucket.used).toFixed(0)}% ${compact}`);
1452
+ }
1453
+ return parts.length > 1 ? parts.join(" ") : void 0;
1454
+ }
1455
+ function formatGenericReport(lines, report) {
1456
+ for (const bucket of report.buckets) {
1457
+ lines.push(
1458
+ `${`${bucket.label}:`.padEnd(VALUE_COLUMN)}${formatMetricValue(bucket.remaining ?? bucket.used ?? "unavailable", bucket.unit)}`
1459
+ );
1460
+ }
1461
+ for (const metric of report.metrics) {
1462
+ lines.push(
1463
+ `${`${metric.label}:`.padEnd(VALUE_COLUMN)}${formatMetricValue(metric.value, metric.unit)}`
1464
+ );
1465
+ }
1466
+ }
1467
+ function formatCodexStatusline(report, model) {
1468
+ const group = selectCodexGroup(report, model);
1469
+ if (!group) return formatCodexCreditsStatus(report);
1470
+ const buckets = report.buckets.filter((bucket) => (bucket.groupId ?? bucket.id) === group);
1471
+ const labelBucket = buckets[0];
1472
+ const parts = [
1473
+ group === "codex" ? "codex" : `codex ${compactLimitLabel(labelBucket?.groupLabel ?? group)}`
1474
+ ];
1475
+ for (const bucket of buckets) {
1476
+ if (bucket.remaining === void 0) continue;
1477
+ const fallback = bucket.id.endsWith(":secondary") ? "weekly" : "5h";
1478
+ parts.push(
1479
+ `${clampPercent3(bucket.remaining).toFixed(0)}% ${formatWindowLabel(bucket.windowMinutes, fallback, true)}`
1480
+ );
1481
+ }
1482
+ return parts.length > 1 ? parts.join(" ") : formatCodexCreditsStatus(report);
1483
+ }
1484
+ function formatCodexCreditsStatus(report) {
1485
+ const credits = report.metrics.find((metric) => metric.id === "credits");
1486
+ if (!credits) return "codex usage unavailable";
1487
+ if (credits.value === "none") return "codex no credits";
1488
+ if (credits.value === "available") return "codex credits available";
1489
+ if (credits.value === "unlimited") return "codex credits unlimited";
1490
+ return `codex ${formatMetricValue(credits.value, "count")} credits`;
1491
+ }
1492
+ function selectCodexGroup(report, model) {
1493
+ const groups = [...new Set(report.buckets.map((bucket) => bucket.groupId ?? bucket.id))];
1494
+ if (model?.provider !== "openai-codex") {
1495
+ return groups.includes("codex") ? "codex" : groups[0];
1496
+ }
1497
+ const modelKeys = normalizedModelKeys(model);
1498
+ for (const group of groups) {
1499
+ const bucket = report.buckets.find(
1500
+ (candidate) => (candidate.groupId ?? candidate.id) === group
1501
+ );
1502
+ const keys = [group, bucket?.groupLabel, ...bucket?.modelKeys ?? []].map(normalizeKey).filter((key) => key !== void 0);
1503
+ if (keys.some((key) => modelKeys.has(key))) return group;
1504
+ }
1505
+ const variants = [...modelKeys].map((key) => key.match(/(?:^|-)codex-(.+)$/)?.[1]).filter((value) => Boolean(value));
1506
+ for (const variant of variants) {
1507
+ const matches = groups.filter((group) => {
1508
+ if (group === "codex") return false;
1509
+ const key = normalizeKey(group);
1510
+ return key ? normalizedKeyHasToken(key, variant) : false;
1511
+ });
1512
+ if (matches.length === 1) return matches[0];
1513
+ }
1514
+ return groups.includes("codex") ? "codex" : groups[0];
1515
+ }
1516
+ function normalizedModelKeys(model) {
1517
+ const keys = /* @__PURE__ */ new Set();
1518
+ for (const value of [model.id, model.name]) {
1519
+ const key = normalizeKey(value);
1520
+ if (!key) continue;
1521
+ keys.add(key);
1522
+ const index = key.indexOf("codex");
1523
+ if (index >= 0) keys.add(key.slice(index));
1524
+ }
1525
+ return keys;
1526
+ }
1527
+ function normalizeKey(value) {
1528
+ const separated = value?.toLowerCase().replace(/[^a-z0-9]+/g, "-");
1529
+ if (!separated) return void 0;
1530
+ let start = 0;
1531
+ let end = separated.length;
1532
+ while (separated[start] === "-") start += 1;
1533
+ while (end > start && separated[end - 1] === "-") end -= 1;
1534
+ return separated.slice(start, end) || void 0;
1535
+ }
1536
+ function normalizedKeyHasToken(key, token) {
1537
+ return key === token || key.startsWith(`${token}-`) || key.endsWith(`-${token}`) || key.includes(`-${token}-`);
1538
+ }
1539
+ function compactLimitLabel(label) {
1540
+ const normalized = label.replace(/[_-]+/g, " ").trim();
1541
+ const codex = /\bcodex\s/iu.exec(normalized);
1542
+ const suffix = codex ? normalized.slice(codex.index + codex[0].length).trim() : "";
1543
+ return (suffix || normalized).toLowerCase().replace(/\s+/g, " ");
1544
+ }
1545
+ function formatPercentBucket(bucket) {
1546
+ const remaining = clampPercent3(bucket.remaining ?? 0);
1547
+ const filled = Math.round(remaining / 100 * BAR_SEGMENTS);
1548
+ const reset = bucket.resetsAt ? ` (resets ${formatReset(bucket.resetsAt)})` : "";
1549
+ return `[${"\u2588".repeat(filled)}${"\u2591".repeat(BAR_SEGMENTS - filled)}] ${remaining.toFixed(0)}% left${reset}`;
1550
+ }
1551
+ function formatWindowLabel(minutes, fallback, compact) {
1552
+ if (!minutes || !Number.isFinite(minutes) || minutes <= 0) {
1553
+ return compact && fallback === "weekly" ? "wk" : capitalize(fallback);
1554
+ }
1555
+ if (minutes === 10080) return compact ? "wk" : "Weekly";
1556
+ if (minutes % 10080 === 0) return `${minutes / 10080}w`;
1557
+ if (minutes % 1440 === 0) return `${minutes / 1440}d`;
1558
+ if (minutes % 60 === 0) return `${minutes / 60}h`;
1559
+ return `${minutes}m`;
1560
+ }
1561
+ function formatMetricValue(value, unit) {
1562
+ if (unit === "usd" && typeof value === "number") return formatUsd(value);
1563
+ return String(value);
1564
+ }
1565
+ function formatUsd(value) {
1566
+ return `$${value.toFixed(2)}`;
1567
+ }
1568
+ function formatReset(epochSeconds) {
1569
+ const reset = new Date(epochSeconds * 1e3);
1570
+ if (Number.isNaN(reset.getTime())) return "at an unknown time";
1571
+ const time = `${reset.getHours().toString().padStart(2, "0")}:${reset.getMinutes().toString().padStart(2, "0")}`;
1572
+ const now = /* @__PURE__ */ new Date();
1573
+ if (reset.toDateString() === now.toDateString()) return time;
1574
+ return `${time} on ${reset.getDate()} ${reset.toLocaleDateString(void 0, { month: "short" })}`;
1575
+ }
1576
+ function capitalize(value) {
1577
+ return `${value[0]?.toUpperCase() ?? ""}${value.slice(1)}`;
1578
+ }
1579
+ function clampPercent3(value) {
1580
+ return Math.min(100, Math.max(0, value));
1581
+ }
1582
+
1583
+ // src/settings.ts
1584
+ import { randomUUID } from "node:crypto";
1585
+ import { constants } from "node:fs";
1586
+ import { chmod, mkdir, open, rename, rm, writeFile } from "node:fs/promises";
1587
+ import { basename, dirname, join } from "node:path";
1588
+ import { getAgentDir } from "@earendil-works/pi-coding-agent";
1589
+ var USAGE_SETTINGS_FILE = "pi-usage.json";
1590
+ var MAX_USAGE_SETTINGS_BYTES = 64 * 1024;
1591
+ var DEFAULT_USAGE_SETTINGS = Object.freeze({
1592
+ codexFastMode: false
1593
+ });
1594
+ function usageSettingsPath() {
1595
+ return join(getAgentDir(), USAGE_SETTINGS_FILE);
1596
+ }
1597
+ function normalizeUsageSettings(value) {
1598
+ if (!isRecord2(value)) return void 0;
1599
+ if (Object.hasOwn(value, "codexFastMode") && typeof value.codexFastMode !== "boolean") {
1600
+ return void 0;
1601
+ }
1602
+ return {
1603
+ codexFastMode: typeof value.codexFastMode === "boolean" ? value.codexFastMode : DEFAULT_USAGE_SETTINGS.codexFastMode
1604
+ };
1605
+ }
1606
+ async function loadUsageSettings(path = usageSettingsPath(), signal) {
1607
+ throwIfAborted(signal);
1608
+ try {
1609
+ const handle = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW);
1610
+ let text;
1611
+ try {
1612
+ const stats = await handle.stat();
1613
+ throwIfAborted(signal);
1614
+ if (!stats.isFile()) throw new Error("settings path is not a regular file");
1615
+ if (stats.size > MAX_USAGE_SETTINGS_BYTES) {
1616
+ throw new Error("settings file exceeds 64 KiB");
1617
+ }
1618
+ text = await handle.readFile("utf8");
1619
+ } finally {
1620
+ await handle.close();
1621
+ }
1622
+ throwIfAborted(signal);
1623
+ const document = JSON.parse(text);
1624
+ const settings = normalizeUsageSettings(document);
1625
+ if (!settings || !isRecord2(document)) throw new Error("invalid settings shape");
1626
+ return { kind: "loaded", path, settings, document };
1627
+ } catch (error) {
1628
+ if (signal?.aborted) throw error;
1629
+ if (isNodeError(error) && error.code === "ENOENT") {
1630
+ return {
1631
+ kind: "missing",
1632
+ path,
1633
+ settings: { ...DEFAULT_USAGE_SETTINGS },
1634
+ document: {}
1635
+ };
1636
+ }
1637
+ return {
1638
+ kind: "invalid",
1639
+ path,
1640
+ settings: { ...DEFAULT_USAGE_SETTINGS },
1641
+ issue: isNodeError(error) && error.code === "ELOOP" ? "symbolic links are not accepted" : error instanceof Error ? error.message : String(error)
1642
+ };
1643
+ }
1644
+ }
1645
+ function createUsageSettingsRuntime(options = {}) {
1646
+ const path = typeof options === "string" ? options : options.path ?? usageSettingsPath();
1647
+ const operations = {
1648
+ rename,
1649
+ writeFile,
1650
+ ...typeof options === "string" ? void 0 : options.operations
1651
+ };
1652
+ let state = {
1653
+ kind: "missing",
1654
+ path,
1655
+ settings: { ...DEFAULT_USAGE_SETTINGS },
1656
+ document: {}
1657
+ };
1658
+ let queue = Promise.resolve();
1659
+ const enqueue = (operation) => {
1660
+ const result = queue.then(operation, operation);
1661
+ queue = result.then(
1662
+ () => void 0,
1663
+ () => void 0
1664
+ );
1665
+ return result;
1666
+ };
1667
+ return {
1668
+ get: () => structuredClone(state),
1669
+ reload: (signal) => enqueue(async () => {
1670
+ const loaded = await loadUsageSettings(path, signal);
1671
+ state = loaded;
1672
+ return structuredClone(state);
1673
+ }),
1674
+ update: (patch, signal) => enqueue(async () => {
1675
+ const saved = await saveUsageSettingsPatch(path, patch, operations, signal);
1676
+ state = saved;
1677
+ return structuredClone(state);
1678
+ }),
1679
+ flush: () => queue
1680
+ };
1681
+ }
1682
+ async function saveUsageSettingsPatch(path, patch, operations, signal) {
1683
+ const latest = await loadUsageSettings(path, signal);
1684
+ if (latest.kind === "invalid") {
1685
+ throw new Error("Cannot overwrite an invalid pi-usage.json; repair it and reload first");
1686
+ }
1687
+ const document = { ...latest.document, ...patch };
1688
+ const settings = normalizeUsageSettings(document);
1689
+ if (!settings) throw new Error("Refusing to save invalid pi-usage settings");
1690
+ const directory = dirname(path);
1691
+ const temporaryPath = join(directory, `.${basename(path)}.${randomUUID()}.tmp`);
1692
+ await mkdir(directory, { recursive: true, mode: 448 });
1693
+ throwIfAborted(signal);
1694
+ try {
1695
+ await operations.writeFile(temporaryPath, `${JSON.stringify(document, null, 2)}
1696
+ `, {
1697
+ encoding: "utf8",
1698
+ flag: "wx",
1699
+ mode: 384
1700
+ });
1701
+ if (process.platform !== "win32") await chmodPrivate(temporaryPath);
1702
+ throwIfAborted(signal);
1703
+ const current = await loadUsageSettings(path, signal);
1704
+ if (current.kind === "invalid" || current.kind !== latest.kind || JSON.stringify(current.document) !== JSON.stringify(latest.document)) {
1705
+ throw new Error("pi-usage.json changed while saving; retry the action");
1706
+ }
1707
+ throwIfAborted(signal);
1708
+ await operations.rename(temporaryPath, path);
1709
+ } finally {
1710
+ await rm(temporaryPath, { force: true }).catch(() => void 0);
1711
+ }
1712
+ return { kind: "loaded", path, settings, document };
1713
+ }
1714
+ async function chmodPrivate(path) {
1715
+ await chmod(path, 384);
1716
+ }
1717
+ function throwIfAborted(signal) {
1718
+ if (signal?.aborted) throw new DOMException("Settings operation aborted", "AbortError");
1719
+ }
1720
+ function isRecord2(value) {
1721
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1722
+ }
1723
+ function isNodeError(error) {
1724
+ return error instanceof Error && "code" in error;
1725
+ }
1726
+
1727
+ // src/usage.ts
1728
+ import { randomUUID as randomUUID2 } from "node:crypto";
1729
+
1730
+ // src/codex-fast-runtime.ts
1731
+ var NO_FAST_REQUEST = /* @__PURE__ */ Symbol("no-fast-request");
1732
+ var FAST_USAGE_WARNING = "Fast is about 1.5\xD7 faster and uses more of your plan allowance.";
1733
+ function registerCodexFastMode(pi, settingsRuntime, refreshStatus) {
1734
+ let sessionController = new AbortController();
1735
+ let generation = 0;
1736
+ const pendingFastRequests = /* @__PURE__ */ new Map();
1737
+ const toggle = async (ctx, enabled, callerSignal) => {
1738
+ const ownerGeneration = generation;
1739
+ const sessionId = ctx.sessionManager.getSessionId();
1740
+ const signal = callerSignal ? AbortSignal.any([callerSignal, sessionController.signal]) : sessionController.signal;
1741
+ try {
1742
+ await settingsRuntime.update({ codexFastMode: enabled }, signal);
1743
+ } catch (error) {
1744
+ if (isAbortError2(error) || isStaleExtensionContextError(error)) return false;
1745
+ ctx.ui.notify(`Could not save pi-usage.json: ${errorMessage(error)}`, "error");
1746
+ return false;
1747
+ }
1748
+ if (signal.aborted || ownerGeneration !== generation || ctx.sessionManager.getSessionId() !== sessionId) {
1749
+ return false;
1750
+ }
1751
+ refreshStatus(ctx);
1752
+ ctx.ui.notify(
1753
+ enabled ? `Codex Fast mode enabled. ${FAST_USAGE_WARNING}` : "Codex Fast mode disabled; standard routing will be used.",
1754
+ "info"
1755
+ );
1756
+ return true;
1757
+ };
1758
+ pi.registerCommand("fast", {
1759
+ description: "Toggle Codex Fast mode",
1760
+ handler: async (args, ctx) => {
1761
+ if (args.trim()) {
1762
+ if (!ctx.hasUI) throw new Error("/fast does not accept arguments.");
1763
+ ctx.ui.notify("/fast does not accept arguments.", "warning");
1764
+ return;
1765
+ }
1766
+ if (!ctx.hasUI) throw new Error("/fast requires TUI or RPC mode.");
1767
+ const availability = codexFastAvailability(
1768
+ ctx.model,
1769
+ settingsRuntime.get().settings.codexFastMode
1770
+ );
1771
+ if (availability.kind === "not-codex") {
1772
+ ctx.ui.notify("/fast is available only for the active OpenAI Codex model.", "warning");
1773
+ return;
1774
+ }
1775
+ if (availability.kind === "unavailable") {
1776
+ ctx.ui.notify(availability.reason, "warning");
1777
+ return;
1778
+ }
1779
+ if (settingsRuntime.get().kind === "invalid") {
1780
+ ctx.ui.notify(
1781
+ "pi-usage.json is invalid; repair it and run /reload before changing Fast mode.",
1782
+ "error"
1783
+ );
1784
+ return;
1785
+ }
1786
+ await toggle(ctx, !availability.enabled);
1787
+ }
1788
+ });
1789
+ pi.on("session_start", async (_event, ctx) => {
1790
+ generation += 1;
1791
+ sessionController.abort();
1792
+ pendingFastRequests.clear();
1793
+ sessionController = new AbortController();
1794
+ const ownerGeneration = generation;
1795
+ const sessionId = ctx.sessionManager.getSessionId();
1796
+ let state;
1797
+ try {
1798
+ state = await settingsRuntime.reload(sessionController.signal);
1799
+ } catch (error) {
1800
+ if (sessionController.signal.aborted || ownerGeneration !== generation) return;
1801
+ if (ctx.hasUI) {
1802
+ ctx.ui.notify(
1803
+ `Could not load pi-usage.json; using defaults. ${errorMessage(error)}`,
1804
+ "warning"
1805
+ );
1806
+ }
1807
+ return;
1808
+ }
1809
+ if (sessionController.signal.aborted || ownerGeneration !== generation || ctx.sessionManager.getSessionId() !== sessionId) {
1810
+ return;
1811
+ }
1812
+ if (ctx.hasUI && state.kind === "invalid") {
1813
+ ctx.ui.notify(
1814
+ `Invalid pi-usage.json; using defaults without overwriting it. ${state.issue}`,
1815
+ "warning"
1816
+ );
1817
+ }
1818
+ refreshStatus(ctx);
1819
+ });
1820
+ pi.on("before_provider_request", (event, ctx) => {
1821
+ const rewritten = rewriteCodexFastPayload(
1822
+ event.payload,
1823
+ ctx.model,
1824
+ settingsRuntime.get().settings.codexFastMode
1825
+ );
1826
+ const key = activeRequestKey(ctx);
1827
+ if (key && ctx.model) {
1828
+ pendingFastRequests.set(key, {
1829
+ fastRequested: isRecord3(rewritten) && rewritten.service_tier === "priority",
1830
+ model: ctx.model
1831
+ });
1832
+ }
1833
+ return rewritten;
1834
+ });
1835
+ pi.on("message_end", (event, ctx) => {
1836
+ const request = consumeFastRequest(ctx, event.message, pendingFastRequests);
1837
+ if (request === NO_FAST_REQUEST) return void 0;
1838
+ const message = correctCodexFastMessageCost(
1839
+ event.message,
1840
+ request.model,
1841
+ request.fastRequested
1842
+ );
1843
+ return message ? { message } : void 0;
1844
+ });
1845
+ pi.on("session_shutdown", async () => {
1846
+ generation += 1;
1847
+ sessionController.abort();
1848
+ pendingFastRequests.clear();
1849
+ await settingsRuntime.flush();
1850
+ });
1851
+ return {
1852
+ availability(model) {
1853
+ return codexFastAvailability(model, settingsRuntime.get().settings.codexFastMode);
1854
+ },
1855
+ decorateStatus(model, status) {
1856
+ return codexFastStatusLabel(
1857
+ status,
1858
+ codexFastIsEffective(model, settingsRuntime.get().settings.codexFastMode)
1859
+ );
1860
+ },
1861
+ toggle
1862
+ };
1863
+ }
1864
+ function activeRequestKey(ctx) {
1865
+ const model = ctx.model;
1866
+ return model ? `${ctx.sessionManager.getSessionId()}:${model.provider}/${model.id}` : void 0;
1867
+ }
1868
+ function consumeFastRequest(ctx, message, pending) {
1869
+ if (!isRecord3(message) || message.role !== "assistant") return NO_FAST_REQUEST;
1870
+ const key = messageRequestKey(ctx, message);
1871
+ if (!key) return NO_FAST_REQUEST;
1872
+ const request = pending.get(key);
1873
+ pending.delete(key);
1874
+ return request ?? NO_FAST_REQUEST;
1875
+ }
1876
+ function messageRequestKey(ctx, message) {
1877
+ if (typeof message.provider !== "string" || typeof message.model !== "string") return void 0;
1878
+ return `${ctx.sessionManager.getSessionId()}:${message.provider}/${message.model}`;
1879
+ }
1880
+ function isRecord3(value) {
1881
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1882
+ }
1883
+ function isAbortError2(error) {
1884
+ return error instanceof Error && error.name === "AbortError";
1885
+ }
1886
+
1887
+ // src/usage-helpers.ts
1888
+ function configuredAdapters(ctx) {
1889
+ return SUPPORTED_ADAPTERS.filter(
1890
+ (adapter) => adapter.id === ctx.model?.provider || providerIsConfigured(ctx, adapter.id)
1891
+ );
1892
+ }
1893
+ function providerDisplayName(ctx, providerId) {
1894
+ try {
1895
+ return sanitizeDisplayText(ctx.modelRegistry.getProviderDisplayName(providerId), 80);
1896
+ } catch {
1897
+ return sanitizeDisplayText(providerId, 80);
1898
+ }
1899
+ }
1900
+ function setBoundedMap(map, key, value, limit) {
1901
+ map.delete(key);
1902
+ while (map.size >= limit) {
1903
+ const oldest = map.keys().next().value;
1904
+ if (oldest === void 0) break;
1905
+ map.delete(oldest);
1906
+ }
1907
+ map.set(key, value);
1908
+ }
1909
+ function modelIdentity(model) {
1910
+ return model ? `${model.provider}/${model.id}` : void 0;
1911
+ }
1912
+ function isAbortError3(error) {
1913
+ return error instanceof Error && error.name === "AbortError";
1914
+ }
1915
+ function isTimeoutError(error) {
1916
+ return error instanceof Error && error.name === "TimeoutError";
1917
+ }
1918
+
1919
+ // src/usage.ts
1920
+ var CACHE_TTL_MS = 5 * 60 * 1e3;
1921
+ var DEFAULT_TIMEOUT_MS = 15e3;
1922
+ var ALL_PROVIDER_CONCURRENCY = 2;
1923
+ var FAILURE_BACKOFF_MS = 3e4;
1924
+ var MAX_ACCOUNT_STATES = 32;
1925
+ var STATUS_KEY = "usage";
1926
+ var REFRESH_CURRENT = "Refresh current usage";
1927
+ var VIEW_ANOTHER = "View another configured provider\u2026";
1928
+ var VIEW_ALL = "View all configured providers\u2026";
1929
+ var CLOSE = "Close";
1930
+ var REDEEM_CODEX_RESET = "Redeem usage limit reset\u2026";
1931
+ function usageExtension(pi, dependencies = {}) {
1932
+ const credentialReader = dependencies.credentialReader;
1933
+ const credentialCandidates = createOAuthCredentialCandidateReader(pi, credentialReader);
1934
+ const createRedemptionId = dependencies.createRedemptionId ?? randomUUID2;
1935
+ const settingsRuntime = dependencies.settingsRuntime ?? createUsageSettingsRuntime();
1936
+ const cache = new UsageCache(CACHE_TTL_MS);
1937
+ const failureBackoff = /* @__PURE__ */ new Map();
1938
+ const latestQueries = /* @__PURE__ */ new Map();
1939
+ const activeControllers = /* @__PURE__ */ new Set();
1940
+ let querySequence = 0;
1941
+ let activeCurrentIdentity;
1942
+ let sessionActive = false;
1943
+ let statusGeneration = 0;
1944
+ let statusRefreshTimer;
1945
+ let statusController;
1946
+ let fastRuntime;
1947
+ const clearStatusTimer = () => {
1948
+ if (statusRefreshTimer) clearTimeout(statusRefreshTimer);
1949
+ statusRefreshTimer = void 0;
1950
+ };
1951
+ const safeSetStatus = (ctx, value) => {
1952
+ try {
1953
+ ctx.ui.setStatus(STATUS_KEY, value);
1954
+ return true;
1955
+ } catch (error) {
1956
+ if (isStaleExtensionContextError(error)) return false;
1957
+ throw error;
1958
+ }
1959
+ };
1960
+ const clearStatus = (ctx) => {
1961
+ statusGeneration += 1;
1962
+ statusController?.abort();
1963
+ statusController = void 0;
1964
+ clearStatusTimer();
1965
+ safeSetStatus(ctx, void 0);
1966
+ };
1967
+ const scheduleStatusRefresh = (ctx, model) => {
1968
+ clearStatusTimer();
1969
+ const generation = statusGeneration;
1970
+ statusRefreshTimer = setTimeout(() => {
1971
+ statusRefreshTimer = void 0;
1972
+ if (!sessionActive || generation !== statusGeneration) return;
1973
+ startStatusRefresh(ctx, model, true);
1974
+ }, CACHE_TTL_MS);
1975
+ statusRefreshTimer.unref?.();
1976
+ };
1977
+ const publishStatus = (ctx, outcome, model, shouldSchedule) => {
1978
+ if (outcome.state.status === "unsupported") {
1979
+ clearStatusTimer();
1980
+ safeSetStatus(ctx, void 0);
1981
+ return;
1982
+ }
1983
+ if (outcome.state.status !== "ready") {
1984
+ if (safeSetStatus(
1985
+ ctx,
1986
+ outcome.state.status === "auth-unavailable" ? "auth unavailable" : "usage error"
1987
+ )) {
1988
+ if (shouldSchedule && sessionActive) scheduleStatusRefresh(ctx, model);
1989
+ }
1990
+ return;
1991
+ }
1992
+ const rawValue = formatUsageStatusline(outcome.state.report, model);
1993
+ const value = rawValue ? fastRuntime.decorateStatus(model, rawValue) : void 0;
1994
+ if (!safeSetStatus(ctx, value)) return;
1995
+ if (shouldSchedule && sessionActive) scheduleStatusRefresh(ctx, model);
1996
+ };
1997
+ const invalidateProviderState = (providerId) => {
1998
+ cache.clearProvider(providerId);
1999
+ for (const key of failureBackoff.keys()) {
2000
+ if (key.startsWith(`${providerId}:`)) failureBackoff.delete(key);
2001
+ }
2002
+ for (const key of latestQueries.keys()) {
2003
+ if (key.startsWith(`${providerId}:`)) latestQueries.delete(key);
2004
+ }
2005
+ };
2006
+ const transitionCurrentIdentity = (nextIdentity, providerId) => {
2007
+ if (!activeCurrentIdentity || activeCurrentIdentity === nextIdentity) {
2008
+ activeCurrentIdentity = nextIdentity;
2009
+ return;
2010
+ }
2011
+ const previousProviderId = activeCurrentIdentity.split(":", 1)[0] ?? "";
2012
+ for (const id of /* @__PURE__ */ new Set([previousProviderId, providerId])) {
2013
+ if (id) invalidateProviderState(id);
2014
+ }
2015
+ activeCurrentIdentity = nextIdentity;
2016
+ };
2017
+ const queryAdapterState = async (ctx, adapter, displayState, force, signal) => {
2018
+ const startedAt = Date.now();
2019
+ let auth;
2020
+ try {
2021
+ auth = await awaitWithDeadline(
2022
+ resolveUsageAuth(ctx, adapter, void 0, credentialReader, credentialCandidates),
2023
+ signal,
2024
+ DEFAULT_TIMEOUT_MS,
2025
+ `resolving ${adapter.displayName} runtime auth`
2026
+ );
2027
+ } catch (error) {
2028
+ if (isStaleExtensionContextError(error) || isAbortError3(error)) throw error;
2029
+ if (displayState === "current") {
2030
+ transitionCurrentIdentity(`${adapter.id}:auth-error`, adapter.id);
2031
+ }
2032
+ return {
2033
+ state: {
2034
+ providerId: adapter.id,
2035
+ providerName: adapter.displayName,
2036
+ displayState,
2037
+ status: isTimeoutError(error) ? "query-failed" : "auth-unavailable",
2038
+ message: errorMessage(error)
2039
+ }
2040
+ };
2041
+ }
2042
+ if (!auth) {
2043
+ if (displayState === "current") {
2044
+ transitionCurrentIdentity(`${adapter.id}:unavailable`, adapter.id);
2045
+ }
2046
+ return {
2047
+ state: {
2048
+ providerId: adapter.id,
2049
+ providerName: adapter.displayName,
2050
+ displayState,
2051
+ status: "auth-unavailable",
2052
+ message: `No runtime credential is configured for ${adapter.displayName}.`
2053
+ },
2054
+ authState: "unavailable"
2055
+ };
2056
+ }
2057
+ if (displayState === "current") {
2058
+ transitionCurrentIdentity(`${adapter.id}:${auth.fingerprint}`, adapter.id);
2059
+ }
2060
+ const cached = !force ? cache.get(adapter.id, auth.fingerprint) : void 0;
2061
+ if (cached) {
2062
+ return {
2063
+ state: {
2064
+ providerId: adapter.id,
2065
+ providerName: adapter.displayName,
2066
+ displayState,
2067
+ status: "ready",
2068
+ report: cached
2069
+ },
2070
+ fingerprint: auth.fingerprint
2071
+ };
2072
+ }
2073
+ const failureKey = `${adapter.id}:${auth.fingerprint}`;
2074
+ const previousFailure = failureBackoff.get(failureKey);
2075
+ if (!force && previousFailure && previousFailure.until > Date.now()) {
2076
+ return {
2077
+ state: {
2078
+ providerId: adapter.id,
2079
+ providerName: adapter.displayName,
2080
+ displayState,
2081
+ status: "query-failed",
2082
+ message: previousFailure.message
2083
+ },
2084
+ fingerprint: auth.fingerprint
2085
+ };
2086
+ }
2087
+ failureBackoff.delete(failureKey);
2088
+ querySequence += 1;
2089
+ const queryId = querySequence;
2090
+ setBoundedMap(latestQueries, failureKey, queryId, MAX_ACCOUNT_STATES);
2091
+ try {
2092
+ const remainingMs = Math.max(1, DEFAULT_TIMEOUT_MS - (Date.now() - startedAt));
2093
+ const report = await queryProviderUsage(adapter, auth, signal, remainingMs);
2094
+ if (latestQueries.get(failureKey) === queryId) {
2095
+ cache.set(adapter.id, auth.fingerprint, report);
2096
+ failureBackoff.delete(failureKey);
2097
+ }
2098
+ return {
2099
+ state: {
2100
+ providerId: adapter.id,
2101
+ providerName: adapter.displayName,
2102
+ displayState,
2103
+ status: "ready",
2104
+ report
2105
+ },
2106
+ fingerprint: auth.fingerprint
2107
+ };
2108
+ } catch (error) {
2109
+ if (isStaleExtensionContextError(error) || isAbortError3(error)) throw error;
2110
+ const message = errorMessage(error);
2111
+ const now = Date.now();
2112
+ for (const [key, failure] of failureBackoff) {
2113
+ if (failure.until <= now) failureBackoff.delete(key);
2114
+ }
2115
+ if (latestQueries.get(failureKey) === queryId) {
2116
+ setBoundedMap(
2117
+ failureBackoff,
2118
+ failureKey,
2119
+ { until: now + FAILURE_BACKOFF_MS, message },
2120
+ MAX_ACCOUNT_STATES
2121
+ );
2122
+ }
2123
+ return {
2124
+ state: {
2125
+ providerId: adapter.id,
2126
+ providerName: adapter.displayName,
2127
+ displayState,
2128
+ status: "query-failed",
2129
+ message
2130
+ },
2131
+ fingerprint: auth.fingerprint
2132
+ };
2133
+ }
2134
+ };
2135
+ const queryCurrentState = async (ctx, model, force, signal) => {
2136
+ const adapter = adapterForProvider(model?.provider);
2137
+ if (!adapter) {
2138
+ const providerId = model?.provider ?? "none";
2139
+ transitionCurrentIdentity(`unsupported:${providerId}`, providerId);
2140
+ return {
2141
+ state: {
2142
+ providerId,
2143
+ providerName: providerDisplayName(ctx, providerId),
2144
+ displayState: "current",
2145
+ status: "unsupported",
2146
+ message: model ? `Usage reporting is not supported for ${providerDisplayName(ctx, providerId)}.` : "No model is selected."
2147
+ }
2148
+ };
2149
+ }
2150
+ return queryAdapterState(ctx, adapter, "current", force, signal);
2151
+ };
2152
+ const refreshCurrentStatus = async (ctx, model, force) => {
2153
+ const adapter = adapterForProvider(model?.provider);
2154
+ if (!adapter || !model) {
2155
+ const providerId = model?.provider ?? "none";
2156
+ transitionCurrentIdentity(`unsupported:${providerId}`, providerId);
2157
+ clearStatus(ctx);
2158
+ return;
2159
+ }
2160
+ statusGeneration += 1;
2161
+ const generation = statusGeneration;
2162
+ statusController?.abort();
2163
+ const controller = new AbortController();
2164
+ statusController = controller;
2165
+ activeControllers.add(controller);
2166
+ try {
2167
+ if (!safeSetStatus(ctx, "checking")) return;
2168
+ const outcome = await queryCurrentState(ctx, model, force, controller.signal);
2169
+ if (!sessionActive || generation !== statusGeneration || controller.signal.aborted) return;
2170
+ if (!await outcomeStillCurrent(ctx, model, generation, outcome, controller.signal)) {
2171
+ if (sessionActive && generation === statusGeneration) {
2172
+ queueMicrotask(() => startStatusRefresh(ctx, ctx.model, false));
2173
+ }
2174
+ return;
2175
+ }
2176
+ publishStatus(ctx, outcome, model, true);
2177
+ } finally {
2178
+ activeControllers.delete(controller);
2179
+ if (statusController === controller) statusController = void 0;
2180
+ }
2181
+ };
2182
+ const startStatusRefresh = (ctx, model, force) => {
2183
+ void refreshCurrentStatus(ctx, model, force).catch((error) => {
2184
+ if (isStaleExtensionContextError(error) || isAbortError3(error)) return;
2185
+ safeSetStatus(ctx, "usage error");
2186
+ });
2187
+ };
2188
+ const runMenuOperation = async (ctx, label, parentSignal, operation, cancellable = true) => {
2189
+ const { runTask } = await import("@narumitw/pi-tui-kit");
2190
+ if (parentSignal.aborted) return void 0;
2191
+ const result = await runTask(ctx, {
2192
+ label,
2193
+ signal: parentSignal,
2194
+ cancellable,
2195
+ onError: () => void 0,
2196
+ task: ({ signal }) => operation(signal)
2197
+ });
2198
+ switch (result.kind) {
2199
+ case "completed":
2200
+ return result.value;
2201
+ case "cancelled":
2202
+ case "stale":
2203
+ return void 0;
2204
+ case "error":
2205
+ throw result.error;
2206
+ }
2207
+ };
2208
+ const outcomeStillCurrent = async (ctx, model, generation, outcome, signal) => {
2209
+ if (generation !== statusGeneration || modelIdentity(ctx.model) !== modelIdentity(model)) {
2210
+ return false;
2211
+ }
2212
+ const adapter = adapterForProvider(model?.provider);
2213
+ if (outcome.authState === "unavailable") {
2214
+ if (!adapter) return false;
2215
+ try {
2216
+ const auth = await awaitWithDeadline(
2217
+ resolveUsageAuth(ctx, adapter, void 0, credentialReader, credentialCandidates),
2218
+ signal,
2219
+ DEFAULT_TIMEOUT_MS,
2220
+ `revalidating ${adapter.displayName} runtime auth`
2221
+ );
2222
+ return generation === statusGeneration && modelIdentity(ctx.model) === modelIdentity(model) && auth === void 0;
2223
+ } catch (error) {
2224
+ if (isAbortError3(error) || isStaleExtensionContextError(error)) throw error;
2225
+ return false;
2226
+ }
2227
+ }
2228
+ if (!outcome.fingerprint) return true;
2229
+ if (!adapter) return false;
2230
+ try {
2231
+ const auth = await awaitWithDeadline(
2232
+ resolveUsageAuth(ctx, adapter, void 0, credentialReader, credentialCandidates),
2233
+ signal,
2234
+ DEFAULT_TIMEOUT_MS,
2235
+ `revalidating ${adapter.displayName} runtime auth`
2236
+ );
2237
+ return generation === statusGeneration && modelIdentity(ctx.model) === modelIdentity(model) && auth?.fingerprint === outcome.fingerprint;
2238
+ } catch (error) {
2239
+ if (isAbortError3(error) || isStaleExtensionContextError(error)) throw error;
2240
+ return false;
2241
+ }
2242
+ };
2243
+ const queryStableCurrent = async (ctx, force, controller, label) => {
2244
+ for (let attempt = 0; attempt < 3; attempt += 1) {
2245
+ const model = ctx.model;
2246
+ const generation = statusGeneration;
2247
+ const result = await runMenuOperation(ctx, label, controller.signal, async (signal) => {
2248
+ const outcome = await queryCurrentState(ctx, model, force, signal);
2249
+ return {
2250
+ outcome,
2251
+ stable: await outcomeStillCurrent(ctx, model, generation, outcome, signal)
2252
+ };
2253
+ });
2254
+ if (!result) return void 0;
2255
+ if (result.stable) return { outcome: result.outcome, model };
2256
+ force = false;
2257
+ }
2258
+ ctx.ui.notify("The active model or account kept changing; reopen /usage to retry.", "warning");
2259
+ return void 0;
2260
+ };
2261
+ const publishStableCurrent = (ctx, current) => {
2262
+ if (current.model) publishStatus(ctx, current.outcome, current.model, sessionActive);
2263
+ else safeSetStatus(ctx, void 0);
2264
+ };
2265
+ const showMenu = async (ctx) => {
2266
+ if (!ctx.hasUI) throw new Error("/usage requires TUI or RPC mode.");
2267
+ statusGeneration += 1;
2268
+ const menuGeneration = statusGeneration;
2269
+ statusController?.abort();
2270
+ statusController = void 0;
2271
+ clearStatusTimer();
2272
+ const controller = new AbortController();
2273
+ activeControllers.add(controller);
2274
+ try {
2275
+ let stableCurrent = await queryStableCurrent(
2276
+ ctx,
2277
+ false,
2278
+ controller,
2279
+ "Checking current usage\u2026"
2280
+ );
2281
+ if (!stableCurrent) return;
2282
+ publishStableCurrent(ctx, stableCurrent);
2283
+ let current = stableCurrent.outcome;
2284
+ let visibleStates = [current.state];
2285
+ let fastState = settingsRuntime.get();
2286
+ let resetAvailability;
2287
+ let selectedReset;
2288
+ let resetAuthFingerprint;
2289
+ let resetModelIdentity;
2290
+ let redemptionId;
2291
+ let resetOutcome;
2292
+ let resetFailure;
2293
+ const { defineMenu, runMenu } = await import("@narumitw/pi-tui-kit");
2294
+ if (controller.signal.aborted || statusGeneration !== menuGeneration) return;
2295
+ const menu = defineMenu({
2296
+ start: "main",
2297
+ screens: {
2298
+ main: () => {
2299
+ const fastAvailability = fastRuntime.availability(ctx.model);
2300
+ const fastLines = fastAvailability.kind === "available" ? [`Fast mode: ${fastAvailability.enabled ? "On" : "Off"}`, FAST_USAGE_WARNING] : fastAvailability.kind === "unavailable" ? [`Fast mode: Unavailable \xB7 ${fastAvailability.reason}`] : [];
2301
+ return {
2302
+ kind: "actions",
2303
+ title: "Provider usage",
2304
+ lines: [...formatProviderStates(visibleStates).split("\n"), ...fastLines],
2305
+ items: [
2306
+ { id: "refresh", label: REFRESH_CURRENT, action: "refresh" },
2307
+ ...fastAvailability.kind === "available" ? [
2308
+ {
2309
+ id: "toggle-fast",
2310
+ label: fastAvailability.enabled ? "Turn Fast mode off" : "Turn Fast mode on",
2311
+ description: fastState.kind === "invalid" ? "Repair pi-usage.json and reload before changing Fast mode." : FAST_USAGE_WARNING,
2312
+ disabled: fastState.kind === "invalid",
2313
+ action: "toggle-fast"
2314
+ }
2315
+ ] : [],
2316
+ ...current.state.status === "ready" && current.state.providerId === "openai-codex" ? [
2317
+ {
2318
+ id: "open-resets",
2319
+ label: REDEEM_CODEX_RESET,
2320
+ description: codexResetActionDescription(current.state.report),
2321
+ disabled: codexResetCount(current.state.report) === 0,
2322
+ action: "open-resets"
2323
+ }
2324
+ ] : [],
2325
+ { id: "another", label: VIEW_ANOTHER, action: "another" },
2326
+ { id: "all", label: VIEW_ALL, action: "all" },
2327
+ { id: "close", label: CLOSE, close: true }
2328
+ ],
2329
+ hint: "close"
2330
+ };
2331
+ },
2332
+ providers: () => ({
2333
+ kind: "actions",
2334
+ title: "Select a configured provider",
2335
+ items: configuredAdapters(ctx).filter((adapter) => adapter.id !== ctx.model?.provider).map((adapter) => ({
2336
+ id: adapter.id,
2337
+ label: adapter.displayName,
2338
+ action: "provider"
2339
+ })),
2340
+ hint: "back"
2341
+ }),
2342
+ "reset-picker": () => ({
2343
+ kind: "choice",
2344
+ title: "Usage limit resets",
2345
+ lines: [
2346
+ `${resetAvailability?.availableCount ?? 0} ${resetLabel(resetAvailability?.availableCount ?? 0)} available.`
2347
+ ],
2348
+ items: (resetAvailability?.options ?? []).map((option, index) => ({
2349
+ id: `reset-${index}`,
2350
+ label: option.title,
2351
+ description: resetOptionExpiration(option),
2352
+ details: [option.description]
2353
+ })),
2354
+ action: "select-reset",
2355
+ initialItemId: "reset-0",
2356
+ hint: "back"
2357
+ }),
2358
+ "reset-confirm": () => ({
2359
+ kind: "actions",
2360
+ title: "Use this reset?",
2361
+ lines: resetConfirmationLines(selectedReset),
2362
+ items: [
2363
+ { id: "cancel-reset", label: "No, go back", action: "cancel-reset" },
2364
+ { id: "consume-reset", label: "Yes, use reset", action: "consume-reset" }
2365
+ ],
2366
+ hint: "back"
2367
+ }),
2368
+ "reset-result": () => ({
2369
+ kind: "actions",
2370
+ title: "Usage limit resets",
2371
+ lines: [
2372
+ formatCodexResetOutcome(
2373
+ resetOutcome,
2374
+ current.state.status === "ready" ? codexResetCount(current.state.report) : void 0
2375
+ )
2376
+ ],
2377
+ items: [
2378
+ {
2379
+ id: "back-to-usage",
2380
+ label: "Back to usage",
2381
+ action: "back-to-usage"
2382
+ },
2383
+ { id: "close", label: CLOSE, close: true }
2384
+ ],
2385
+ hint: "back"
2386
+ }),
2387
+ "reset-error": () => ({
2388
+ kind: "actions",
2389
+ title: "Usage limit resets",
2390
+ lines: [resetFailure ?? "Couldn't reset usage. Please try again."],
2391
+ items: [
2392
+ { id: "consume-reset", label: "Try again", action: "consume-reset" },
2393
+ { id: "back-to-resets", label: "Back", action: "back-to-resets" }
2394
+ ],
2395
+ hint: "back"
2396
+ })
2397
+ },
2398
+ actions: {
2399
+ "toggle-fast": async () => {
2400
+ const availability = fastRuntime.availability(ctx.model);
2401
+ if (availability.kind !== "available" || fastState.kind === "invalid") {
2402
+ return { kind: "rejected" };
2403
+ }
2404
+ const changed = await fastRuntime.toggle(ctx, !availability.enabled, controller.signal);
2405
+ if (!changed) return { kind: "rejected" };
2406
+ fastState = settingsRuntime.get();
2407
+ return { kind: "stay" };
2408
+ },
2409
+ "open-resets": async () => {
2410
+ const summaryCount = current.state.status === "ready" && current.state.providerId === "openai-codex" ? codexResetCount(current.state.report) : void 0;
2411
+ try {
2412
+ const loaded = await runMenuOperation(
2413
+ ctx,
2414
+ "Checking usage limit resets\u2026",
2415
+ controller.signal,
2416
+ async (signal) => {
2417
+ const expectedModel = modelIdentity(ctx.model);
2418
+ const auth = await awaitWithDeadline(
2419
+ resolveCodexResetAuth(ctx, void 0, credentialReader, credentialCandidates),
2420
+ signal,
2421
+ DEFAULT_TIMEOUT_MS,
2422
+ "resolving current Codex reset authentication"
2423
+ );
2424
+ let availability;
2425
+ try {
2426
+ availability = await listCodexResetCredits(auth, signal, DEFAULT_TIMEOUT_MS);
2427
+ } catch (error) {
2428
+ if (isAbortError3(error) || summaryCount === void 0 || summaryCount <= 0) {
2429
+ throw error;
2430
+ }
2431
+ availability = {
2432
+ availableCount: summaryCount,
2433
+ options: [genericCodexResetOption()]
2434
+ };
2435
+ }
2436
+ const revalidated = await awaitWithDeadline(
2437
+ resolveCodexResetAuth(ctx, void 0, credentialReader, credentialCandidates),
2438
+ signal,
2439
+ DEFAULT_TIMEOUT_MS,
2440
+ "revalidating current Codex reset authentication"
2441
+ );
2442
+ if (modelIdentity(ctx.model) !== expectedModel || revalidated.fingerprint !== auth.fingerprint) {
2443
+ throw new Error(
2444
+ "The active Codex model or account changed while loading usage limit resets."
2445
+ );
2446
+ }
2447
+ return { availability, auth, expectedModel };
2448
+ }
2449
+ );
2450
+ if (!loaded) return { kind: "stay" };
2451
+ resetAvailability = loaded.availability;
2452
+ resetAuthFingerprint = loaded.auth.fingerprint;
2453
+ resetModelIdentity = loaded.expectedModel;
2454
+ selectedReset = void 0;
2455
+ redemptionId = void 0;
2456
+ resetOutcome = void 0;
2457
+ resetFailure = void 0;
2458
+ return {
2459
+ kind: "to",
2460
+ screen: loaded.availability.availableCount > 0 ? "reset-picker" : "reset-result"
2461
+ };
2462
+ } catch (error) {
2463
+ if (isAbortError3(error) || isStaleExtensionContextError(error)) {
2464
+ return { kind: "stay" };
2465
+ }
2466
+ ctx.ui.notify(`Couldn't load usage limit resets: ${errorMessage(error)}`, "error");
2467
+ return { kind: "stay" };
2468
+ }
2469
+ },
2470
+ "select-reset": ({ itemId }) => {
2471
+ const index = Number(itemId.replace(/^reset-/u, ""));
2472
+ const option = Number.isSafeInteger(index) ? resetAvailability?.options[index] : void 0;
2473
+ if (!option) return { kind: "rejected" };
2474
+ selectedReset = option;
2475
+ redemptionId = void 0;
2476
+ resetFailure = void 0;
2477
+ return { kind: "to", screen: "reset-confirm" };
2478
+ },
2479
+ "cancel-reset": () => ({ kind: "back" }),
2480
+ "consume-reset": async () => {
2481
+ if (!selectedReset || !resetAuthFingerprint || !resetModelIdentity) {
2482
+ return { kind: "rejected" };
2483
+ }
2484
+ try {
2485
+ redemptionId ??= createRedemptionId();
2486
+ const attemptId = redemptionId;
2487
+ const option = selectedReset;
2488
+ const expectedFingerprint = resetAuthFingerprint;
2489
+ const expectedModel = resetModelIdentity;
2490
+ const result = await runMenuOperation(
2491
+ ctx,
2492
+ "Resetting your usage\u2026",
2493
+ controller.signal,
2494
+ async (signal) => {
2495
+ const auth = await awaitWithDeadline(
2496
+ resolveCodexResetAuth(ctx, void 0, credentialReader, credentialCandidates),
2497
+ signal,
2498
+ DEFAULT_TIMEOUT_MS,
2499
+ "revalidating current Codex reset authentication"
2500
+ );
2501
+ if (modelIdentity(ctx.model) !== expectedModel || auth.fingerprint !== expectedFingerprint) {
2502
+ throw new Error(
2503
+ "The active Codex model or account changed; the reset was not used."
2504
+ );
2505
+ }
2506
+ const outcome = await consumeCodexResetCredit(
2507
+ auth,
2508
+ option,
2509
+ attemptId,
2510
+ signal,
2511
+ DEFAULT_TIMEOUT_MS
2512
+ );
2513
+ invalidateProviderState("openai-codex");
2514
+ const model = ctx.model;
2515
+ if (modelIdentity(model) !== expectedModel) return { outcome };
2516
+ const refreshed = await queryCurrentState(ctx, model, true, signal);
2517
+ const stable = await outcomeStillCurrent(
2518
+ ctx,
2519
+ model,
2520
+ menuGeneration,
2521
+ refreshed,
2522
+ signal
2523
+ );
2524
+ return stable ? { outcome, refreshed, model } : { outcome };
2525
+ },
2526
+ false
2527
+ );
2528
+ if (!result) return { kind: "close" };
2529
+ resetOutcome = result.outcome;
2530
+ resetFailure = void 0;
2531
+ if (result.refreshed && result.model) {
2532
+ stableCurrent = { outcome: result.refreshed, model: result.model };
2533
+ current = result.refreshed;
2534
+ visibleStates = [current.state];
2535
+ publishStableCurrent(ctx, stableCurrent);
2536
+ }
2537
+ return { kind: "to", screen: "reset-result" };
2538
+ } catch (error) {
2539
+ if (isAbortError3(error) || isStaleExtensionContextError(error)) {
2540
+ return { kind: "close" };
2541
+ }
2542
+ resetFailure = `Couldn't reset usage: ${errorMessage(error)}. Try again with the same request.`;
2543
+ return { kind: "to", screen: "reset-error" };
2544
+ }
2545
+ },
2546
+ "back-to-usage": () => ({ kind: "to", screen: "main" }),
2547
+ "back-to-resets": () => {
2548
+ redemptionId = void 0;
2549
+ resetFailure = void 0;
2550
+ return { kind: "to", screen: "reset-picker" };
2551
+ },
2552
+ refresh: async () => {
2553
+ const refreshed = await queryStableCurrent(
2554
+ ctx,
2555
+ true,
2556
+ controller,
2557
+ "Refreshing current usage\u2026"
2558
+ );
2559
+ if (!refreshed) return { kind: "stay" };
2560
+ stableCurrent = refreshed;
2561
+ publishStableCurrent(ctx, refreshed);
2562
+ current = refreshed.outcome;
2563
+ visibleStates = [current.state];
2564
+ return { kind: "stay" };
2565
+ },
2566
+ another: async () => {
2567
+ const others = configuredAdapters(ctx).filter(
2568
+ (adapter) => adapter.id !== ctx.model?.provider
2569
+ );
2570
+ if (others.length === 0) {
2571
+ ctx.ui.notify("No other supported provider has configured runtime auth.", "info");
2572
+ return { kind: "stay" };
2573
+ }
2574
+ return { kind: "to", screen: "providers" };
2575
+ },
2576
+ provider: async ({ itemId }) => {
2577
+ const adapter = configuredAdapters(ctx).find(
2578
+ (candidate) => candidate.id === itemId && candidate.id !== ctx.model?.provider
2579
+ );
2580
+ if (!adapter) return { kind: "back" };
2581
+ const outcome = await runMenuOperation(
2582
+ ctx,
2583
+ `Checking ${adapter.displayName} usage\u2026`,
2584
+ controller.signal,
2585
+ (signal) => queryAdapterState(ctx, adapter, "configured", false, signal)
2586
+ );
2587
+ if (!outcome) return { kind: "back" };
2588
+ const revalidated = await queryStableCurrent(
2589
+ ctx,
2590
+ false,
2591
+ controller,
2592
+ "Revalidating current usage\u2026"
2593
+ );
2594
+ if (!revalidated) return { kind: "back" };
2595
+ stableCurrent = revalidated;
2596
+ current = revalidated.outcome;
2597
+ visibleStates = [
2598
+ outcome.state.providerId === current.state.providerId ? current.state : { ...outcome.state, displayState: "configured" }
2599
+ ];
2600
+ return { kind: "back" };
2601
+ },
2602
+ all: async () => {
2603
+ const adapters = configuredAdapters(ctx);
2604
+ const currentProviderId = ctx.model?.provider;
2605
+ const settled = await runMenuOperation(
2606
+ ctx,
2607
+ "Checking configured provider usage\u2026",
2608
+ controller.signal,
2609
+ (signal) => runWithConcurrency(
2610
+ adapters,
2611
+ ALL_PROVIDER_CONCURRENCY,
2612
+ (adapter, _index, workerSignal) => queryAdapterState(
2613
+ ctx,
2614
+ adapter,
2615
+ adapter.id === currentProviderId ? "current" : "configured",
2616
+ true,
2617
+ workerSignal
2618
+ ),
2619
+ signal
2620
+ )
2621
+ );
2622
+ if (!settled) return { kind: "stay" };
2623
+ const queriedStates = settled.map((result, index) => {
2624
+ if (result.status === "fulfilled") {
2625
+ return { ...result.value.state, displayState: "configured" };
2626
+ }
2627
+ const adapter = adapters[index];
2628
+ return {
2629
+ providerId: adapter.id,
2630
+ providerName: adapter.displayName,
2631
+ displayState: "configured",
2632
+ status: "query-failed",
2633
+ message: errorMessage(result.reason)
2634
+ };
2635
+ });
2636
+ const revalidated = await queryStableCurrent(
2637
+ ctx,
2638
+ false,
2639
+ controller,
2640
+ "Revalidating current usage\u2026"
2641
+ );
2642
+ if (!revalidated) return { kind: "stay" };
2643
+ stableCurrent = revalidated;
2644
+ current = revalidated.outcome;
2645
+ visibleStates = [
2646
+ current.state,
2647
+ ...queriedStates.filter((state) => state.providerId !== current.state.providerId)
2648
+ ];
2649
+ return { kind: "stay" };
2650
+ }
2651
+ }
2652
+ });
2653
+ await runMenu(ctx, menu, {
2654
+ getState: () => void 0,
2655
+ signal: controller.signal,
2656
+ isCurrent: () => statusGeneration === menuGeneration && !controller.signal.aborted
2657
+ });
2658
+ } finally {
2659
+ controller.abort(new DOMException("Usage menu closed", "AbortError"));
2660
+ activeControllers.delete(controller);
2661
+ }
2662
+ };
2663
+ pi.registerCommand("usage", {
2664
+ description: "Show usage for the current runtime account",
2665
+ handler: async (args, ctx) => {
2666
+ if (args.trim()) {
2667
+ ctx.ui.notify(
2668
+ "/usage does not accept arguments; choose an action from its menu.",
2669
+ "warning"
2670
+ );
2671
+ return;
2672
+ }
2673
+ try {
2674
+ await showMenu(ctx);
2675
+ } catch (error) {
2676
+ if (isStaleExtensionContextError(error) || isAbortError3(error)) return;
2677
+ throw error;
2678
+ }
2679
+ }
2680
+ });
2681
+ pi.on("session_start", (_event, ctx) => {
2682
+ statusGeneration += 1;
2683
+ clearStatusTimer();
2684
+ for (const controller of activeControllers) controller.abort();
2685
+ activeControllers.clear();
2686
+ statusController = void 0;
2687
+ sessionActive = true;
2688
+ startStatusRefresh(ctx, ctx.model, false);
2689
+ });
2690
+ pi.on("session_tree", (_event, ctx) => {
2691
+ startStatusRefresh(ctx, ctx.model, false);
2692
+ });
2693
+ pi.on("model_select", (event, ctx) => {
2694
+ startStatusRefresh(ctx, event.model, false);
2695
+ });
2696
+ pi.on("turn_start", (_event, ctx) => {
2697
+ startStatusRefresh(ctx, ctx.model, false);
2698
+ });
2699
+ pi.on("session_shutdown", (_event, ctx) => {
2700
+ sessionActive = false;
2701
+ statusGeneration += 1;
2702
+ clearStatusTimer();
2703
+ for (const controller of activeControllers) controller.abort();
2704
+ activeControllers.clear();
2705
+ statusController = void 0;
2706
+ cache.clear();
2707
+ failureBackoff.clear();
2708
+ latestQueries.clear();
2709
+ activeCurrentIdentity = void 0;
2710
+ safeSetStatus(ctx, void 0);
2711
+ });
2712
+ fastRuntime = registerCodexFastMode(
2713
+ pi,
2714
+ settingsRuntime,
2715
+ (ctx) => startStatusRefresh(ctx, ctx.model, false)
2716
+ );
2717
+ }
2718
+ export {
2719
+ CODEX_FAST_MODEL_IDS,
2720
+ CODEX_FAST_SERVICE_TIER,
2721
+ CODEX_STANDARD_SERVICE_TIER,
2722
+ DEFAULT_USAGE_SETTINGS,
2723
+ SUPPORTED_ADAPTERS,
2724
+ UsageCache,
2725
+ abortError,
2726
+ adapterForProvider,
2727
+ awaitWithDeadline,
2728
+ codexFastAvailability,
2729
+ codexFastIsEffective,
2730
+ codexFastRequestTier,
2731
+ codexFastStatusLabel,
2732
+ consumeCodexResetCredit,
2733
+ correctCodexFastMessageCost,
2734
+ createUsageSettingsRuntime,
2735
+ usageExtension as default,
2736
+ errorMessage,
2737
+ fingerprintResolvedAuth,
2738
+ formatProviderStates,
2739
+ formatUsageReport,
2740
+ formatUsageStatusline,
2741
+ isStaleExtensionContextError,
2742
+ listCodexResetCredits,
2743
+ loadUsageSettings,
2744
+ normalizeCodexBackendPayload,
2745
+ normalizeCodexResetCreditsPayload,
2746
+ normalizeGitHubCopilotUsagePayload,
2747
+ normalizeOpenCodeZenPayload,
2748
+ normalizeOpenRouterKeyPayload,
2749
+ normalizeUsageSettings,
2750
+ providerIsConfigured,
2751
+ queryProviderUsage,
2752
+ redactUsageError,
2753
+ resolveCodexResetAuth,
2754
+ resolveUsageAuth,
2755
+ rewriteCodexFastPayload,
2756
+ runWithConcurrency,
2757
+ sanitizeDisplayText,
2758
+ usageSettingsPath
2759
+ };
2760
+ //# sourceMappingURL=index.ts.map