@narumitw/pi-usage 0.64.3 → 0.65.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/format.ts CHANGED
@@ -60,6 +60,36 @@ export function formatUsageStatusline(
60
60
  now = Date.now(),
61
61
  showCodexResetCountdown = true,
62
62
  codexStatusPercentage: CodexStatusPercentage = "remaining",
63
+ ): string | undefined {
64
+ return formatUsageStatuslineWithCountdown(report, model, now, showCodexResetCountdown, codexStatusPercentage).value;
65
+ }
66
+
67
+ interface StatusCountdown {
68
+ now: number;
69
+ active: boolean;
70
+ }
71
+
72
+ // Record new-provider eligibility while formatting the selected windows so repaint cannot drift from display policy.
73
+ // Codex and ChatGPT companion keep their existing repaint policy in usage.ts.
74
+ export function formatUsageStatuslineWithCountdown(
75
+ report: UsageReport,
76
+ model?: UsageModel,
77
+ now = Date.now(),
78
+ showCodexResetCountdown = true,
79
+ codexStatusPercentage: CodexStatusPercentage = "remaining",
80
+ ): { value: string | undefined; hasCountdown: boolean } {
81
+ const countdown: StatusCountdown = { now, active: false };
82
+ const value = formatStatusline(report, model, now, showCodexResetCountdown, codexStatusPercentage, countdown);
83
+ return { value, hasCountdown: countdown.active };
84
+ }
85
+
86
+ function formatStatusline(
87
+ report: UsageReport,
88
+ model: UsageModel | undefined,
89
+ now: number,
90
+ showCodexResetCountdown: boolean,
91
+ codexStatusPercentage: CodexStatusPercentage,
92
+ countdown: StatusCountdown,
63
93
  ): string | undefined {
64
94
  if (report.providerId === "openai" && report.source === "openai-chatgpt-auth") return "chatgpt usage: web only";
65
95
  if (report.source === "openai-chatgpt-companion") {
@@ -79,24 +109,24 @@ export function formatUsageStatusline(
79
109
  if (report.providerId === "deepseek") return formatDeepSeekStatusline(report);
80
110
  if (report.providerId === "fireworks") return formatFireworksStatusline(report);
81
111
  if (report.providerId === "vercel-ai-gateway") return formatVercelAIGatewayStatusline(report);
82
- if (report.providerId === "github-copilot") return formatGitHubCopilotStatusline(report);
112
+ if (report.providerId === "github-copilot") return formatGitHubCopilotStatusline(report, countdown);
83
113
  if (report.providerId === "openrouter") {
84
114
  const limit = report.buckets.find((bucket) => bucket.id === "key-limit");
85
115
  if (limit?.remaining !== undefined) return `openrouter ${formatUsd(limit.remaining)} left`;
86
116
  const total = report.metrics.find((metric) => metric.id === "usage-total");
87
117
  if (typeof total?.value === "number") return `openrouter ${formatUsd(total.value)} used`;
88
118
  }
89
- if (report.providerId === "opencode-go") return formatOpenCodeZenStatusline(report);
90
- if (report.providerId === "command-code") return formatCommandCodeStatusline(report);
91
- if (report.providerId === "kimi-coding") return formatKimiCodingStatusline(report);
119
+ if (report.providerId === "opencode-go") return formatOpenCodeZenStatusline(report, countdown);
120
+ if (report.providerId === "command-code") return formatCommandCodeStatusline(report, countdown);
121
+ if (report.providerId === "kimi-coding") return formatKimiCodingStatusline(report, countdown);
92
122
  if (report.providerId === "moonshotai" || report.providerId === "moonshotai-cn") {
93
123
  return formatMoonshotStatusline(report);
94
124
  }
95
125
  if (report.providerId === "minimax" || report.providerId === "minimax-cn") {
96
- return formatMiniMaxStatusline(report, model);
126
+ return formatMiniMaxStatusline(report, countdown, model);
97
127
  }
98
128
  if (report.providerId === "zai" || report.providerId === "zai-coding-cn") {
99
- return formatZaiStatusline(report);
129
+ return formatZaiStatusline(report, countdown);
100
130
  }
101
131
  return undefined;
102
132
  }
@@ -232,7 +262,7 @@ function formatGitHubCopilotReport(lines: string[], report: UsageReport): void {
232
262
  }
233
263
  }
234
264
 
235
- function formatGitHubCopilotStatusline(report: UsageReport): string {
265
+ function formatGitHubCopilotStatusline(report: UsageReport, countdown: StatusCountdown): string {
236
266
  const quota = findGitHubCopilotQuota(report);
237
267
  const kind = compactGitHubCopilotQuotaKind(quota);
238
268
  if (!quota || quota.limit === undefined || quota.remaining === undefined) {
@@ -240,7 +270,8 @@ function formatGitHubCopilotStatusline(report: UsageReport): string {
240
270
  }
241
271
  const overage = report.metrics.find((metric) => metric.id === "overage-used");
242
272
  const overageSuffix = typeof overage?.value === "number" && overage.value > 0 ? ` +${overage.value} over` : "";
243
- return `copilot ${kind === "premium" ? "" : `${kind} `}${quota.remaining}/${quota.limit} ${percentRemaining(quota)}%${overageSuffix}`;
273
+ const reset = formatQuotaCountdown(quota, countdown);
274
+ return `copilot ${kind === "premium" ? "" : `${kind} `}${quota.remaining}/${quota.limit} ${percentRemaining(quota)}%${overageSuffix}${reset ? ` ↻ ${reset}` : ""}`;
244
275
  }
245
276
 
246
277
  function findGitHubCopilotQuota(report: UsageReport): UsageBucket | undefined {
@@ -285,14 +316,14 @@ function formatOpenCodeZenReport(lines: string[], report: UsageReport): void {
285
316
  }
286
317
  }
287
318
 
288
- function formatOpenCodeZenStatusline(report: UsageReport): string | undefined {
289
- const parts = ["zen"];
319
+ function formatOpenCodeZenStatusline(report: UsageReport, countdown: StatusCountdown): string | undefined {
320
+ const parts: string[] = [];
290
321
  for (const bucket of report.buckets) {
291
322
  if (bucket.used === undefined) continue;
292
323
  const compact = bucket.id === "rolling" ? "r" : bucket.id === "weekly" ? "w" : "m";
293
- parts.push(`${clampPercent(bucket.used).toFixed(0)}% ${compact}`);
324
+ parts.push(formatQuotaWindow(bucket, `${clampPercent(bucket.used).toFixed(0)}%`, compact, countdown));
294
325
  }
295
- return parts.length > 1 ? parts.join(" ") : undefined;
326
+ return parts.length > 0 ? `zen ${parts.join(" · ")}` : undefined;
296
327
  }
297
328
 
298
329
  function formatCommandCodeReport(lines: string[], report: UsageReport): void {
@@ -311,8 +342,8 @@ function formatCommandCodeReport(lines: string[], report: UsageReport): void {
311
342
  }
312
343
  }
313
344
 
314
- function formatCommandCodeStatusline(report: UsageReport): string | undefined {
315
- const parts = ["cmd"];
345
+ function formatCommandCodeStatusline(report: UsageReport, countdown: StatusCountdown): string | undefined {
346
+ const parts: string[] = [];
316
347
  for (const [id, label] of [
317
348
  ["five-hour", "5h"],
318
349
  ["weekly", "wk"],
@@ -320,9 +351,9 @@ function formatCommandCodeStatusline(report: UsageReport): string | undefined {
320
351
  ] as const) {
321
352
  const bucket = report.buckets.find((candidate) => candidate.id === id);
322
353
  if (bucket?.unit !== "usd" || bucket.limit === undefined || bucket.remaining === undefined) continue;
323
- parts.push(`${percentRemaining(bucket)}% ${label}`);
354
+ parts.push(formatQuotaWindow(bucket, `${percentRemaining(bucket)}%`, label, countdown));
324
355
  }
325
- return parts.length > 1 ? parts.join(" ") : undefined;
356
+ return parts.length > 0 ? `cmd ${parts.join(" · ")}` : undefined;
326
357
  }
327
358
 
328
359
  function formatKimiCodingReport(lines: string[], report: UsageReport): void {
@@ -363,7 +394,7 @@ function formatKimiCodingReport(lines: string[], report: UsageReport): void {
363
394
  }
364
395
  }
365
396
 
366
- function formatKimiCodingStatusline(report: UsageReport): string | undefined {
397
+ function formatKimiCodingStatusline(report: UsageReport, countdown: StatusCountdown): string | undefined {
367
398
  const fiveHour = report.buckets.find((bucket) => bucket.id === "five-hour");
368
399
  const weekly = report.buckets.find((bucket) => bucket.id === "weekly");
369
400
  const monthly = report.buckets.find((bucket) => bucket.id === "monthly");
@@ -371,15 +402,15 @@ function formatKimiCodingStatusline(report: UsageReport): string | undefined {
371
402
  const selected = [subWindow, weekly, monthly].filter(
372
403
  (bucket, index, buckets): bucket is UsageBucket => bucket !== undefined && buckets.indexOf(bucket) === index,
373
404
  );
374
- const parts = ["kimi"];
405
+ const parts: string[] = [];
375
406
  for (const bucket of selected) {
376
407
  if (bucket.remaining === undefined || (bucket.unit !== "percent" && !bucket.limit)) continue;
377
408
  const fallback = bucket.id === "weekly" ? "weekly" : "5h";
378
409
  const window = bucket.id === "monthly" ? "mo" : formatWindowLabel(bucket.windowMinutes, fallback, true);
379
410
  const remaining = bucket.unit === "percent" ? Math.round(clampPercent(bucket.remaining)) : percentRemaining(bucket);
380
- parts.push(`${remaining}% ${window}`);
411
+ parts.push(formatQuotaWindow(bucket, `${remaining}%`, window, countdown));
381
412
  }
382
- return parts.length > 1 ? parts.join(" ") : undefined;
413
+ return parts.length > 0 ? `kimi ${parts.join(" · ")}` : undefined;
383
414
  }
384
415
 
385
416
  function formatMoonshotReport(lines: string[], report: UsageReport): void {
@@ -418,7 +449,11 @@ function formatMiniMaxReport(lines: string[], report: UsageReport): void {
418
449
  }
419
450
  }
420
451
 
421
- function formatMiniMaxStatusline(report: UsageReport, model?: UsageModel): string | undefined {
452
+ function formatMiniMaxStatusline(
453
+ report: UsageReport,
454
+ countdown: StatusCountdown,
455
+ model?: UsageModel,
456
+ ): string | undefined {
422
457
  const prefix = report.providerId === "minimax-cn" ? "minimax cn" : "minimax";
423
458
  if (report.source === "minimax-account-balance") {
424
459
  const available = report.metrics.find((metric) => metric.id === "available-balance");
@@ -427,22 +462,22 @@ function formatMiniMaxStatusline(report: UsageReport, model?: UsageModel): strin
427
462
  const selectedGroup = selectMiniMaxGroup(report, model);
428
463
  if (!selectedGroup) return undefined;
429
464
  const selected = report.buckets.filter((bucket) => bucket.groupId === selectedGroup);
430
- const parts = [prefix];
465
+ const parts: string[] = [];
431
466
  for (const bucket of selected) {
432
467
  const fallback = bucket.id.endsWith(":weekly") ? "weekly" : "5h";
433
468
  const window = formatWindowLabel(bucket.windowMinutes, fallback, true);
434
469
  if (bucket.period === "unlimited") {
435
- parts.push(`unlimited ${window}`);
470
+ parts.push(`${window}:unlimited`);
436
471
  continue;
437
472
  }
438
473
  if (bucket.unit === "percent" && bucket.remaining !== undefined) {
439
- parts.push(`${bucket.remaining}% ${window}`);
474
+ parts.push(formatQuotaWindow(bucket, `${bucket.remaining}%`, window, countdown));
440
475
  continue;
441
476
  }
442
477
  if (bucket.limit === undefined || bucket.remaining === undefined) continue;
443
- parts.push(`${percentRemaining(bucket)}% ${window}`);
478
+ parts.push(formatQuotaWindow(bucket, `${percentRemaining(bucket)}%`, window, countdown));
444
479
  }
445
- return parts.length > 1 ? parts.join(" ") : undefined;
480
+ return parts.length > 0 ? `${prefix} ${parts.join(" · ")}` : undefined;
446
481
  }
447
482
 
448
483
  function selectMiniMaxGroup(report: UsageReport, model?: UsageModel): string | undefined {
@@ -493,18 +528,25 @@ function wildcardKeyMatches(pattern: string, value: string): boolean {
493
528
  return pattern.endsWith("*") || (last !== undefined && value.endsWith(last));
494
529
  }
495
530
 
496
- function formatZaiStatusline(report: UsageReport): string | undefined {
531
+ function formatZaiStatusline(report: UsageReport, countdown: StatusCountdown): string | undefined {
497
532
  const selected = [
498
533
  report.buckets.find((bucket) => bucket.id === "five-hour"),
499
534
  report.buckets.find((bucket) => bucket.id === "weekly"),
500
535
  ];
501
- const parts = ["zai"];
536
+ const parts: string[] = [];
502
537
  for (const bucket of selected) {
503
538
  if (!bucket?.limit || bucket.remaining === undefined) continue;
504
539
  const fallback = bucket.id === "weekly" ? "weekly" : "5h";
505
- parts.push(`${percentRemaining(bucket)}% ${formatWindowLabel(bucket.windowMinutes, fallback, true)}`);
540
+ parts.push(
541
+ formatQuotaWindow(
542
+ bucket,
543
+ `${percentRemaining(bucket)}%`,
544
+ formatWindowLabel(bucket.windowMinutes, fallback, true),
545
+ countdown,
546
+ ),
547
+ );
506
548
  }
507
- return parts.length > 1 ? parts.join(" ") : undefined;
549
+ return parts.length > 0 ? `zai ${parts.join(" · ")}` : undefined;
508
550
  }
509
551
 
510
552
  function formatCurrencyMetric(metric: UsageReport["metrics"][number]): string {
@@ -604,7 +646,8 @@ function formatCodexStatusline(
604
646
  if (!group) return formatCodexCreditsStatus(report);
605
647
  const buckets = report.buckets.filter((bucket) => (bucket.groupId ?? bucket.id) === group);
606
648
  const labelBucket = buckets[0];
607
- const parts = [group === "codex" ? "codex" : `codex ${compactLimitLabel(labelBucket?.groupLabel ?? group)}`];
649
+ const prefix = group === "codex" ? "codex" : `codex ${compactLimitLabel(labelBucket?.groupLabel ?? group)}`;
650
+ const parts: string[] = [];
608
651
  for (const bucket of buckets) {
609
652
  if (bucket.remaining === undefined) continue;
610
653
  const displayedPercentage = percentage === "used" ? (bucket.used ?? 100 - bucket.remaining) : bucket.remaining;
@@ -612,13 +655,13 @@ function formatCodexStatusline(
612
655
  const fallback = bucket.id.endsWith(":secondary") ? "weekly" : "5h";
613
656
  const window = formatWindowLabel(bucket.windowMinutes, fallback, true);
614
657
  if (!showResetCountdown) {
615
- parts.push(`${percent} ${window}`);
658
+ parts.push(`${window}:${percent}`);
616
659
  continue;
617
660
  }
618
661
  const reset = formatResetCountdown(bucket.resetsAt, now);
619
- parts.push(`${percent} ${reset ? `↻ ${reset}` : window}`);
662
+ parts.push(reset ? `${percent} ↻ ${reset}` : `${window}:${percent}`);
620
663
  }
621
- return parts.length > 1 ? parts.join(" ") : formatCodexCreditsStatus(report);
664
+ return parts.length > 0 ? `${prefix} ${parts.join(" · ")}` : formatCodexCreditsStatus(report);
622
665
  }
623
666
 
624
667
  function formatCodexCreditsStatus(report: UsageReport): string {
@@ -711,6 +754,25 @@ function formatWindowLabel(minutes: number | undefined, fallback: "5h" | "weekly
711
754
  return `${minutes}m`;
712
755
  }
713
756
 
757
+ function formatQuotaWindow(bucket: UsageBucket, percent: string, window: string, countdown: StatusCountdown): string {
758
+ const reset = formatQuotaCountdown(bucket, countdown);
759
+ return reset ? `${percent} ↻ ${reset}` : `${window}:${percent}`;
760
+ }
761
+
762
+ function formatQuotaCountdown(bucket: UsageBucket, countdown: StatusCountdown): string | undefined {
763
+ if (
764
+ bucket.period === "unlimited" ||
765
+ bucket.resetsAt === undefined ||
766
+ !Number.isFinite(bucket.resetsAt * 1_000) ||
767
+ !Number.isFinite(countdown.now) ||
768
+ bucket.resetsAt * 1_000 <= countdown.now
769
+ )
770
+ return undefined;
771
+ const reset = formatResetCountdown(bucket.resetsAt, countdown.now);
772
+ if (reset !== undefined) countdown.active = true;
773
+ return reset;
774
+ }
775
+
714
776
  function formatResetCountdown(resetsAt: number | undefined, now: number): string | undefined {
715
777
  if (resetsAt === undefined || !Number.isFinite(resetsAt) || !Number.isFinite(now)) return undefined;
716
778
  const totalMinutes = Math.max(0, Math.ceil((resetsAt * 1_000 - now) / 60_000));
package/src/index.ts CHANGED
@@ -1,5 +1,5 @@
1
+ export type { CodexFastAvailability, CodexFastCapability } from "./codex-fast.js";
1
2
  export {
2
- CODEX_FAST_MODEL_IDS,
3
3
  CODEX_FAST_SERVICE_TIER,
4
4
  CODEX_STANDARD_SERVICE_TIER,
5
5
  codexFastAvailability,
@@ -7,7 +7,9 @@ export {
7
7
  codexFastRequestTier,
8
8
  codexFastStatusLabel,
9
9
  correctCodexFastMessageCost,
10
+ isOfficialCodexModel,
10
11
  rewriteCodexFastPayload,
12
+ UNKNOWN_CODEX_FAST_CAPABILITY,
11
13
  } from "./codex-fast.js";
12
14
  export type {
13
15
  CodexResetAvailability,
package/src/query.ts CHANGED
@@ -679,6 +679,7 @@ export async function fetchProviderJson(
679
679
  body?: Record<string, unknown>;
680
680
  redirect?: RequestRedirect;
681
681
  userAgent?: boolean;
682
+ maxSuccessBodyBytes?: number;
682
683
  responseError?: (status: number, text: string) => string | undefined;
683
684
  } = {},
684
685
  ): Promise<Record<string, unknown>> {
@@ -710,7 +711,7 @@ export async function fetchProviderJson(
710
711
  if (controller.signal.aborted) throw Object.assign(new Error("Usage query aborted."), { name: "AbortError" });
711
712
  const text = await readBoundedResponse(
712
713
  response,
713
- response.ok ? MAX_SUCCESS_BODY_BYTES : MAX_ERROR_BODY_BYTES,
714
+ response.ok ? (request.maxSuccessBodyBytes ?? MAX_SUCCESS_BODY_BYTES) : MAX_ERROR_BODY_BYTES,
714
715
  !response.ok,
715
716
  description,
716
717
  controller.signal,
package/src/usage.ts CHANGED
@@ -2,6 +2,7 @@
2
2
  // account cache, cancellation, and session lifecycle share one consistency boundary.
3
3
  import { randomUUID } from "node:crypto";
4
4
  import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext } from "@earendil-works/pi-coding-agent";
5
+ import { isOfficialCodexModel } from "./codex-fast.js";
5
6
  import { FAST_USAGE_WARNING, registerCodexFastMode } from "./codex-fast-runtime.js";
6
7
  import {
7
8
  type CodexResetAvailability,
@@ -19,7 +20,7 @@ import {
19
20
  resolveCodexResetAuth,
20
21
  } from "./codex-resets.js";
21
22
  import { abortError, awaitWithDeadline, errorMessage, runWithConcurrency, UsageCache } from "./core.js";
22
- import { formatProviderStates, formatUsageStatusline } from "./format.js";
23
+ import { formatProviderStates, formatUsageStatuslineWithCountdown } from "./format.js";
23
24
  import { createOAuthCredentialCandidateReader } from "./oauth-credential-source.js";
24
25
  import { UnsupportedOpenAIUsageAuthError } from "./providers/openai-chatgpt.js";
25
26
  import {
@@ -180,31 +181,33 @@ export default function usageExtension(pi: ExtensionAPI, dependencies: UsageExte
180
181
  outcome.state.report.providerId === "openai-codex" && settings.codexStatusResetCountdown;
181
182
  const isCompanionReport = outcome.state.report.source === "openai-chatgpt-companion";
182
183
  const now = Date.now();
183
- const rawValue = formatUsageStatusline(
184
+ const status = formatUsageStatuslineWithCountdown(
184
185
  outcome.state.report,
185
186
  model,
186
187
  now,
187
188
  showCodexResetCountdown,
188
189
  settings.codexStatusPercentage,
189
190
  );
190
- const value = rawValue ? fastRuntime.decorateStatus(model, rawValue) : undefined;
191
+ const value = status.value ? fastRuntime.decorateStatus(model, status.value) : undefined;
191
192
  if (!safeSetStatus(ctx, value)) return;
192
193
  if (shouldSchedule && sessionActive) scheduleStatusRefresh(ctx, model);
193
194
  if (
194
195
  sessionActive &&
195
- (showCodexResetCountdown || isCompanionReport) &&
196
- outcome.state.report.buckets.some(
197
- (bucket) =>
198
- (!isCompanionReport || bucket.groupId === "chatgpt-plan") &&
199
- bucket.resetsAt !== undefined &&
200
- Number.isFinite(bucket.resetsAt) &&
201
- bucket.resetsAt * 1_000 > now,
202
- )
196
+ (status.hasCountdown ||
197
+ ((showCodexResetCountdown || isCompanionReport) &&
198
+ outcome.state.report.buckets.some(
199
+ (bucket) =>
200
+ (!isCompanionReport || bucket.groupId === "chatgpt-plan") &&
201
+ bucket.resetsAt !== undefined &&
202
+ Number.isFinite(bucket.resetsAt) &&
203
+ bucket.resetsAt * 1_000 > now,
204
+ )))
203
205
  ) {
204
206
  const generation = statusGeneration;
205
207
  statusCountdownTimer = setTimeout(() => {
206
- statusCountdownTimer = undefined;
208
+ // A queued obsolete callback must not detach its successor's owned timer.
207
209
  if (!sessionActive || generation !== statusGeneration) return;
210
+ statusCountdownTimer = undefined;
208
211
  publishStatus(ctx, outcome, model, false);
209
212
  }, STATUS_COUNTDOWN_REFRESH_MS);
210
213
  statusCountdownTimer.unref?.();
@@ -765,14 +768,27 @@ export default function usageExtension(pi: ExtensionAPI, dependencies: UsageExte
765
768
 
766
769
  const showMenu = async (ctx: ExtensionCommandContext): Promise<void> => {
767
770
  if (!ctx.hasUI) throw new Error("/usage requires TUI or RPC mode.");
768
- statusGeneration += 1;
769
- const menuGeneration = statusGeneration;
770
- statusController?.abort();
771
- statusController = undefined;
772
- clearStatusTimers();
773
771
  const controller = new AbortController();
774
772
  activeControllers.add(controller);
775
773
  try {
774
+ if (isOfficialCodexModel(ctx.model)) {
775
+ const refreshed = await runMenuOperation(
776
+ ctx,
777
+ "Checking Codex Fast capability…",
778
+ controller.signal,
779
+ async (signal) => {
780
+ await fastRuntime.refreshCapabilities(ctx, signal);
781
+ return true;
782
+ },
783
+ );
784
+ if (!refreshed || controller.signal.aborted) return;
785
+ }
786
+ // Capability updates can refresh status; take menu ownership after discovery.
787
+ statusGeneration += 1;
788
+ const menuGeneration = statusGeneration;
789
+ statusController?.abort();
790
+ statusController = undefined;
791
+ clearStatusTimers();
776
792
  let stableCurrent = await queryStableCurrent(ctx, false, controller, "Checking current usage…");
777
793
  if (!stableCurrent) return;
778
794
  publishStableCurrent(ctx, stableCurrent);
@@ -919,7 +935,9 @@ export default function usageExtension(pi: ExtensionAPI, dependencies: UsageExte
919
935
  ? [`Fast mode: ${fastAvailability.enabled ? "On" : "Off"}`, FAST_USAGE_WARNING]
920
936
  : fastAvailability.kind === "unavailable"
921
937
  ? [`Fast mode: Unavailable · ${fastAvailability.reason}`]
922
- : [];
938
+ : fastAvailability.kind === "unknown"
939
+ ? [`Fast mode: Unknown · ${fastAvailability.reason}`]
940
+ : [];
923
941
  return {
924
942
  kind: "actions",
925
943
  title: "Provider usage",
@@ -936,11 +954,12 @@ export default function usageExtension(pi: ExtensionAPI, dependencies: UsageExte
936
954
  },
937
955
  ]
938
956
  : []),
939
- ...(fastAvailability.kind === "available"
957
+ ...(fastAvailability.kind === "available" ||
958
+ (fastState.settings.codexFastMode && isOfficialCodexModel(ctx.model))
940
959
  ? [
941
960
  {
942
961
  id: "toggle-fast",
943
- label: fastAvailability.enabled ? "Turn Fast mode off" : "Turn Fast mode on",
962
+ label: fastState.settings.codexFastMode ? "Turn Fast mode off" : "Turn Fast mode on",
944
963
  description:
945
964
  fastState.kind === "invalid"
946
965
  ? "Repair pi-usage.json and reload before changing Fast mode."
@@ -1114,10 +1133,11 @@ export default function usageExtension(pi: ExtensionAPI, dependencies: UsageExte
1114
1133
  },
1115
1134
  "toggle-fast": async () => {
1116
1135
  const availability = fastRuntime.availability(ctx.model);
1117
- if (availability.kind !== "available" || fastState.kind === "invalid") {
1136
+ const enabled = settingsRuntime.get().settings.codexFastMode;
1137
+ if ((availability.kind !== "available" && !enabled) || fastState.kind === "invalid") {
1118
1138
  return { kind: "rejected" };
1119
1139
  }
1120
- const changed = await fastRuntime.toggle(ctx, !availability.enabled, controller.signal);
1140
+ const changed = await fastRuntime.toggle(ctx, !enabled, controller.signal);
1121
1141
  if (!changed) return { kind: "rejected" };
1122
1142
  fastState = settingsRuntime.get();
1123
1143
  return { kind: "stay" };
@@ -1449,5 +1469,8 @@ export default function usageExtension(pi: ExtensionAPI, dependencies: UsageExte
1449
1469
 
1450
1470
  fastRuntime = registerCodexFastMode(pi, settingsRuntime, (ctx) => startStatusRefresh(ctx, ctx.model, false), {
1451
1471
  registerSessionStart: false,
1472
+ credentialReader,
1473
+ candidateReader: credentialCandidates,
1474
+ timeoutMs: DEFAULT_TIMEOUT_MS,
1452
1475
  });
1453
1476
  }