@diousk/pi-usage-fast 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  [![npm](https://img.shields.io/npm/v/@diousk/pi-usage-fast)](https://www.npmjs.com/package/@diousk/pi-usage-fast) [![Pi extension](https://img.shields.io/badge/Pi-extension-blue)](https://pi.dev) [![License: MIT](https://img.shields.io/badge/license-MIT-green.svg)](./LICENSE)
4
4
 
5
- Inspect usage and DeepSeek API balance for Pi's active provider account, query other configured providers, and toggle Fast mode for supported OpenAI Codex models.
5
+ Inspect usage and DeepSeek API balance for Pi's active provider account, query other configured providers, and toggle Fast mode for supported OpenAI and Codex models.
6
6
  The extension keeps each provider's native quota, allowance, and spending semantics instead of treating unlike values as equivalent.
7
7
  xAI OAuth subscription reporting follows the reviewed Grok Build contract and runs only after an explicit `/usage` action.
8
8
 
@@ -51,19 +51,19 @@ The package declares `dist/index.ts`, so an unbuilt local checkout must run the
51
51
 
52
52
  Run `/usage` in TUI or RPC mode to inspect the active provider, refresh its usage, or choose another configured provider.
53
53
  When a provider exposes several billing targets, `/usage` asks for one target before querying usage.
54
- Run `/fast` to toggle Fast mode for a supported active Codex model.
54
+ Run `/fast` to toggle Fast mode for a supported active OpenAI or Codex model.
55
55
 
56
56
  ## 💬 Commands
57
57
 
58
58
  | Command | Purpose |
59
59
  | --- | --- |
60
60
  | `/usage` | Query the active provider's usage, then manage provider queries, preferences, or eligible Codex resets. |
61
- | `/fast` | Toggle Fast mode for the active supported Codex model. |
61
+ | `/fast` | Toggle Fast mode for the active supported OpenAI or Codex model. |
62
62
 
63
63
  Both commands support TUI and RPC, accept no arguments, and reject print and JSON modes.
64
64
  Cross-provider queries require an explicit interactive choice; there are no provider-ID, `--refresh`, or `--all` arguments.
65
65
  Requests use the matched provider credentials; see [Security and privacy](#-security-and-privacy).
66
- Fast mode uses more plan allowance; see [Codex Fast mode](#codex-fast-mode) for eligibility and when changes apply.
66
+ Fast mode uses more plan allowance or API credits; see [Codex Fast mode](#codex-fast-mode) for eligibility and when changes apply.
67
67
 
68
68
  Codex reset redemption requires a freshly matched current OAuth account and explicit confirmation; **after confirmation, its progress view cannot cancel the reset**.
69
69
  Read the [query and reset guide](./docs/operations.md) for target selection, cancellation, and safe retry behavior.
@@ -87,11 +87,11 @@ A successful explicit Fireworks account selection writes the generic field and r
87
87
 
88
88
  ### Codex Fast mode
89
89
 
90
- Run `/fast` without arguments to toggle Fast for the active supported Codex model, or use **Turn Fast mode on/off** in `/usage`.
91
- Fast is about 1.5× faster and uses more of your plan allowance.
90
+ Run `/fast` without arguments to toggle Fast for the active supported OpenAI or Codex model, or use **Turn Fast mode on/off** in `/usage`.
91
+ Fast requests priority routing and consumes more plan allowance or API credits.
92
92
  The `codexFastMode` preference defaults to Off.
93
93
 
94
- Fast currently applies only to official `openai-codex-responses` requests for `gpt-5.5`, `gpt-5.6-sol`, `gpt-5.6-terra`, `gpt-5.6-luna`, `gpt-6-*`, and `gpt-6.<minor>-*` models at `https://chatgpt.com`.
94
+ Fast applies to official OpenAI Codex requests at `https://chatgpt.com` and OpenAI Responses requests at `https://api.openai.com`, including the newer ChatGPT sign-in and API-key billing. Supported models are `gpt-5.5`, `gpt-5.6-sol`, `gpt-5.6-terra`, `gpt-5.6-luna`, `gpt-6-*`, and `gpt-6.<minor>-*`.
95
95
  For example, `gpt-6.1-sol` supports the same toggle, routing, statusline, and priority cost correction as `gpt-6-sol`.
96
96
  It sends `service_tier: "priority"` while enabled and explicit `service_tier: "default"` otherwise.
97
97
  The statusline adds `fast` only while the preference is effective, for example `codex fast 59% ↻ 2h30m` with the default reset countdown.
@@ -133,6 +133,7 @@ Currencies and billing targets remain separate.
133
133
  | Provider | Reported data |
134
134
  | --- | --- |
135
135
  | OpenAI Codex | Subscription windows, credits, resets, and model buckets |
136
+ | OpenAI | Links to ChatGPT plan usage or API billing; numeric quotas are unavailable through this authentication flow |
136
137
  | Kimi For Coding | Plan request windows and a separate booster wallet |
137
138
  | Moonshot AI Global/China | Current API balance in USD/CNY |
138
139
  | MiniMax Global/China | Token Plan windows or pay-as-you-go API balance |
@@ -147,6 +148,7 @@ Currencies and billing targets remain separate.
147
148
  | Z.AI | Coding Plan quota windows, MCP allowance, plan name, and renewal date |
148
149
 
149
150
  Read the [provider reference](./docs/providers.md) for provider IDs, exact endpoints, authentication requirements, normalization rules, statusline examples, limitations, and pinned contract evidence.
151
+ For Pi's newer OpenAI ChatGPT sign-in, `/usage` links to [ChatGPT usage settings](https://chatgpt.com/settings/usage); API-key accounts link to API billing. Numeric quotas are not exposed through this flow, and its token is never sent to legacy Codex endpoints.
150
152
  Codex reset redemption requires a freshly matched current OAuth account and explicit confirmation; custom or proxy origins fail before mutation.
151
153
 
152
154
  ## 🧭 Current and configured accounts
package/dist/index.ts CHANGED
@@ -9,17 +9,18 @@ var CODEX_FAST_MODEL_IDS = /* @__PURE__ */ new Set([
9
9
  "gpt-5.5",
10
10
  "gpt-5.6-luna",
11
11
  "gpt-5.6-sol",
12
- "gpt-5.6-terra"
12
+ "gpt-5.6-terra",
13
+ "gpt-6.1-sol"
13
14
  ]);
14
15
  function supportsCodexFast(modelId) {
15
16
  return CODEX_FAST_MODEL_IDS.has(modelId) || /^gpt-6(?:\.\d+)?-.+/u.test(modelId);
16
17
  }
17
18
  function codexFastAvailability(model, enabled) {
18
- if (model?.provider !== "openai-codex") return { kind: "not-codex" };
19
+ if (model?.provider !== "openai-codex" && model?.provider !== "openai") return { kind: "not-codex" };
19
20
  if (!isOfficialCodexModel(model)) {
20
21
  return {
21
22
  kind: "unavailable",
22
- reason: "Fast mode requires the official OpenAI Codex Responses endpoint."
23
+ reason: "Fast mode requires an official OpenAI Responses or Codex Responses endpoint."
23
24
  };
24
25
  }
25
26
  if (!supportsCodexFast(model.id)) {
@@ -64,11 +65,10 @@ function codexFastStatusLabel(status, enabled) {
64
65
  return status === "codex" ? "codex fast" : `codex fast${status.slice("codex".length)}`;
65
66
  }
66
67
  function isOfficialCodexModel(model) {
67
- if (model?.provider !== "openai-codex" || !hasApi(model, "openai-codex-responses")) {
68
- return false;
69
- }
68
+ if (!model) return false;
70
69
  try {
71
- return new URL(model.baseUrl).origin === "https://chatgpt.com";
70
+ const origin = new URL(model.baseUrl).origin;
71
+ return model.provider === "openai-codex" && hasApi(model, "openai-codex-responses") && origin === "https://chatgpt.com" || model.provider === "openai" && hasApi(model, "openai-responses") && origin === "https://api.openai.com";
72
72
  } catch {
73
73
  return false;
74
74
  }
@@ -1969,6 +1969,116 @@ function clampPercent3(value) {
1969
1969
  return Math.min(100, Math.max(0, value));
1970
1970
  }
1971
1971
 
1972
+ // src/usage-http.ts
1973
+ var MAX_SUCCESS_BODY_BYTES = 64 * 1024;
1974
+ var MAX_ERROR_BODY_BYTES = 4 * 1024;
1975
+ async function fetchProviderJson(url, auth, signal, timeoutMs, description, request = {}) {
1976
+ const controller = new AbortController();
1977
+ let timedOut = false;
1978
+ const abortFromCaller = () => controller.abort();
1979
+ if (signal.aborted) controller.abort();
1980
+ else signal.addEventListener("abort", abortFromCaller, { once: true });
1981
+ const timeout = setTimeout(() => {
1982
+ timedOut = true;
1983
+ controller.abort();
1984
+ }, timeoutMs);
1985
+ try {
1986
+ const headers = { ...auth.headers };
1987
+ if (request.userAgent !== false && !hasHeader(headers, "User-Agent")) {
1988
+ headers["User-Agent"] = "pi-usage";
1989
+ }
1990
+ if (request.body && !hasHeader(headers, "Content-Type")) {
1991
+ headers["Content-Type"] = "application/json";
1992
+ }
1993
+ const response = await fetch(url, {
1994
+ method: request.method ?? "GET",
1995
+ headers,
1996
+ ...request.body ? { body: JSON.stringify(request.body) } : {},
1997
+ ...request.redirect ? { redirect: request.redirect } : {},
1998
+ signal: controller.signal
1999
+ });
2000
+ if (response.redirected) throw new Error(`${description} refused a redirected response.`);
2001
+ if (controller.signal.aborted) throw Object.assign(new Error("Usage query aborted."), { name: "AbortError" });
2002
+ const text = await readBoundedResponse(
2003
+ response,
2004
+ response.ok ? MAX_SUCCESS_BODY_BYTES : MAX_ERROR_BODY_BYTES,
2005
+ !response.ok,
2006
+ description,
2007
+ controller.signal
2008
+ );
2009
+ if (controller.signal.aborted) throw Object.assign(new Error("Usage query aborted."), { name: "AbortError" });
2010
+ const responseError = request.responseError?.(response.status, text);
2011
+ if (responseError) throw new Error(responseError);
2012
+ if (!response.ok) {
2013
+ throw new Error(
2014
+ `${description} returned ${response.status} ${response.statusText}: ${redactUsageError(text, auth.secrets)}`
2015
+ );
2016
+ }
2017
+ let parsed;
2018
+ try {
2019
+ parsed = JSON.parse(text);
2020
+ } catch (error) {
2021
+ throw new Error(`${description} returned invalid JSON: ${errorMessage(error)}`);
2022
+ }
2023
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
2024
+ throw new Error(`${description} response was not an object.`);
2025
+ }
2026
+ return parsed;
2027
+ } catch (error) {
2028
+ if (timedOut) {
2029
+ throw new Error(`Timed out after ${Math.round(timeoutMs / 1e3)}s while fetching usage.`);
2030
+ }
2031
+ if (signal.aborted) throw Object.assign(new Error("Usage query aborted."), { name: "AbortError" });
2032
+ throw error;
2033
+ } finally {
2034
+ clearTimeout(timeout);
2035
+ signal.removeEventListener("abort", abortFromCaller);
2036
+ }
2037
+ }
2038
+ async function readBoundedResponse(response, maxBytes, truncateOverflow, description, signal) {
2039
+ if (!response.body) return "";
2040
+ const reader = response.body.getReader();
2041
+ const chunks = [];
2042
+ let total = 0;
2043
+ let truncated = false;
2044
+ const abort = () => void reader.cancel().catch(() => void 0);
2045
+ if (signal.aborted) abort();
2046
+ else signal.addEventListener("abort", abort, { once: true });
2047
+ try {
2048
+ while (true) {
2049
+ const { done, value } = await reader.read();
2050
+ if (done) break;
2051
+ const remaining = maxBytes - total;
2052
+ if (value.byteLength > remaining) {
2053
+ if (remaining > 0) chunks.push(value.subarray(0, remaining));
2054
+ total = maxBytes;
2055
+ truncated = true;
2056
+ await reader.cancel();
2057
+ break;
2058
+ }
2059
+ chunks.push(value);
2060
+ total += value.byteLength;
2061
+ }
2062
+ } finally {
2063
+ signal.removeEventListener("abort", abort);
2064
+ reader.releaseLock();
2065
+ }
2066
+ if (truncated && !truncateOverflow) {
2067
+ throw new Error(`${description} response exceeded ${maxBytes} bytes.`);
2068
+ }
2069
+ const body = new Uint8Array(total);
2070
+ let offset = 0;
2071
+ for (const chunk of chunks) {
2072
+ body.set(chunk, offset);
2073
+ offset += chunk.byteLength;
2074
+ }
2075
+ const text = new TextDecoder().decode(body);
2076
+ return truncated ? `${text}\u2026` : text;
2077
+ }
2078
+ function hasHeader(headers, name) {
2079
+ return Object.keys(headers).some((key) => key.toLowerCase() === name.toLowerCase());
2080
+ }
2081
+
1972
2082
  // src/usage-targets.ts
1973
2083
  var MAX_TARGETS = 1e3;
1974
2084
  var MAX_TARGET_ID_CHARS = 256;
@@ -2083,10 +2193,30 @@ var XAI_CLIENT_HEADERS = Object.freeze({
2083
2193
  "x-grok-client-version": "1.0.10",
2084
2194
  "x-grok-client-mode": "interactive"
2085
2195
  });
2086
- var MAX_SUCCESS_BODY_BYTES = 64 * 1024;
2087
- var MAX_ERROR_BODY_BYTES = 4 * 1024;
2088
2196
  var AUTH_FINGERPRINT_SALT = randomBytes(32);
2089
2197
  var SUPPORTED_ADAPTERS = [
2198
+ {
2199
+ id: "openai",
2200
+ displayName: "OpenAI",
2201
+ semantics: { kind: "consumer-subscription", label: "ChatGPT plan usage" },
2202
+ publishesStatusline: false,
2203
+ async query(auth) {
2204
+ const subscription = auth.chatgptPlan === true;
2205
+ return {
2206
+ providerId: "openai",
2207
+ providerName: subscription ? "OpenAI (ChatGPT)" : "OpenAI API",
2208
+ capturedAt: Date.now(),
2209
+ source: "openai-usage-settings",
2210
+ semantics: subscription ? { kind: "consumer-subscription", label: "ChatGPT plan usage" } : { kind: "api-key", label: "OpenAI API usage" },
2211
+ buckets: [],
2212
+ metrics: [],
2213
+ notes: [
2214
+ subscription ? "Review plan usage and app limits in ChatGPT Settings \u2192 Usage: https://chatgpt.com/settings/usage" : "Review OpenAI API usage and billing: https://platform.openai.com/usage",
2215
+ "Numeric usage is unavailable through this authentication flow; no quota is inferred."
2216
+ ]
2217
+ };
2218
+ }
2219
+ },
2090
2220
  {
2091
2221
  id: "baseten",
2092
2222
  displayName: "Baseten",
@@ -2332,7 +2462,7 @@ async function resolveUsageAuth(ctx, adapter, salt = AUTH_FINGERPRINT_SALT, cred
2332
2462
  }
2333
2463
  const model = candidateModels(ctx, adapter.id).find((candidate) => hasOfficialOrigin(candidate, adapter.id));
2334
2464
  if (!model) return void 0;
2335
- if (["github-copilot", "xai"].includes(adapter.id) && candidateReader?.waitUntilReady && !await candidateReader.waitUntilReady(ctx, adapter.id)) {
2465
+ if (["github-copilot", "xai", "openai"].includes(adapter.id) && candidateReader?.waitUntilReady && !await candidateReader.waitUntilReady(ctx, adapter.id)) {
2336
2466
  throw new Error(`${adapter.displayName} OAuth credential readiness failed closed.`);
2337
2467
  }
2338
2468
  const registry = ctx.modelRegistry;
@@ -2441,12 +2571,27 @@ async function resolveUsageAuth(ctx, adapter, salt = AUTH_FINGERPRINT_SALT, cred
2441
2571
  const secrets = [auth.apiKey, headerValue(auth.headers, "Authorization"), authorization].filter(
2442
2572
  (value) => Boolean(value)
2443
2573
  );
2574
+ let chatgptPlan;
2575
+ if (adapter.id === "openai") {
2576
+ const offered = candidateReader ? await candidateReader(ctx, adapter.id) : fallbackOAuthCredentialCandidates(adapter.id, credentialReader);
2577
+ if (!offered.ok) throw new Error("OpenAI OAuth credential discovery failed closed.");
2578
+ const access = bearerToken(authorization);
2579
+ chatgptPlan = offered.candidates.some(
2580
+ (credential) => credential.access === access && Array.isArray(credential.scopes) && credential.scopes.includes("chatgpt.tokens.use.direct")
2581
+ );
2582
+ if (!chatgptPlan && providerResult?.source === "OAuth") {
2583
+ throw new Error(
2584
+ "The active OpenAI OAuth credential does not match an available ChatGPT plan credential; reload after account activation."
2585
+ );
2586
+ }
2587
+ }
2444
2588
  return finalize({
2445
2589
  apiKey: auth.apiKey,
2446
2590
  headers,
2447
2591
  fingerprint: "",
2448
2592
  secrets,
2449
- model
2593
+ model,
2594
+ ...chatgptPlan !== void 0 ? { chatgptPlan } : {}
2450
2595
  });
2451
2596
  }
2452
2597
  async function queryProviderUsage(adapter, auth, signal, timeoutMs, guard, targetOrSettings) {
@@ -2522,109 +2667,6 @@ function candidateModels(ctx, providerId) {
2522
2667
  for (const model of ctx.modelRegistry.getAll()) add(model);
2523
2668
  return candidates;
2524
2669
  }
2525
- async function fetchProviderJson(url, auth, signal, timeoutMs, description, request = {}) {
2526
- const controller = new AbortController();
2527
- let timedOut = false;
2528
- const abortFromCaller = () => controller.abort();
2529
- if (signal.aborted) controller.abort();
2530
- else signal.addEventListener("abort", abortFromCaller, { once: true });
2531
- const timeout = setTimeout(() => {
2532
- timedOut = true;
2533
- controller.abort();
2534
- }, timeoutMs);
2535
- try {
2536
- const headers = { ...auth.headers };
2537
- if (request.userAgent !== false && !hasHeader(headers, "User-Agent")) {
2538
- headers["User-Agent"] = "pi-usage";
2539
- }
2540
- if (request.body && !hasHeader(headers, "Content-Type")) {
2541
- headers["Content-Type"] = "application/json";
2542
- }
2543
- const response = await fetch(url, {
2544
- method: request.method ?? "GET",
2545
- headers,
2546
- ...request.body ? { body: JSON.stringify(request.body) } : {},
2547
- ...request.redirect ? { redirect: request.redirect } : {},
2548
- signal: controller.signal
2549
- });
2550
- if (response.redirected) throw new Error(`${description} refused a redirected response.`);
2551
- if (controller.signal.aborted) throw Object.assign(new Error("Usage query aborted."), { name: "AbortError" });
2552
- const text = await readBoundedResponse(
2553
- response,
2554
- response.ok ? MAX_SUCCESS_BODY_BYTES : MAX_ERROR_BODY_BYTES,
2555
- !response.ok,
2556
- description,
2557
- controller.signal
2558
- );
2559
- if (controller.signal.aborted) throw Object.assign(new Error("Usage query aborted."), { name: "AbortError" });
2560
- const responseError = request.responseError?.(response.status, text);
2561
- if (responseError) throw new Error(responseError);
2562
- if (!response.ok) {
2563
- throw new Error(
2564
- `${description} returned ${response.status} ${response.statusText}: ${redactUsageError(text, auth.secrets)}`
2565
- );
2566
- }
2567
- let parsed;
2568
- try {
2569
- parsed = JSON.parse(text);
2570
- } catch (error) {
2571
- throw new Error(`${description} returned invalid JSON: ${errorMessage(error)}`);
2572
- }
2573
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
2574
- throw new Error(`${description} response was not an object.`);
2575
- }
2576
- return parsed;
2577
- } catch (error) {
2578
- if (timedOut) {
2579
- throw new Error(`Timed out after ${Math.round(timeoutMs / 1e3)}s while fetching usage.`);
2580
- }
2581
- if (signal.aborted) throw Object.assign(new Error("Usage query aborted."), { name: "AbortError" });
2582
- throw error;
2583
- } finally {
2584
- clearTimeout(timeout);
2585
- signal.removeEventListener("abort", abortFromCaller);
2586
- }
2587
- }
2588
- async function readBoundedResponse(response, maxBytes, truncateOverflow, description, signal) {
2589
- if (!response.body) return "";
2590
- const reader = response.body.getReader();
2591
- const chunks = [];
2592
- let total = 0;
2593
- let truncated = false;
2594
- const abort = () => void reader.cancel().catch(() => void 0);
2595
- if (signal.aborted) abort();
2596
- else signal.addEventListener("abort", abort, { once: true });
2597
- try {
2598
- while (true) {
2599
- const { done, value } = await reader.read();
2600
- if (done) break;
2601
- const remaining = maxBytes - total;
2602
- if (value.byteLength > remaining) {
2603
- if (remaining > 0) chunks.push(value.subarray(0, remaining));
2604
- total = maxBytes;
2605
- truncated = true;
2606
- await reader.cancel();
2607
- break;
2608
- }
2609
- chunks.push(value);
2610
- total += value.byteLength;
2611
- }
2612
- } finally {
2613
- signal.removeEventListener("abort", abort);
2614
- reader.releaseLock();
2615
- }
2616
- if (truncated && !truncateOverflow) {
2617
- throw new Error(`${description} response exceeded ${maxBytes} bytes.`);
2618
- }
2619
- const body = new Uint8Array(total);
2620
- let offset = 0;
2621
- for (const chunk of chunks) {
2622
- body.set(chunk, offset);
2623
- offset += chunk.byteLength;
2624
- }
2625
- const text = new TextDecoder().decode(body);
2626
- return truncated ? `${text}\u2026` : text;
2627
- }
2628
2670
  function resolveXaiUsageAuth(auth, model, salt, candidates) {
2629
2671
  const resolvedAccess = bearerToken(headerValue(auth.headers, "Authorization")) ?? auth.apiKey;
2630
2672
  if (!resolvedAccess) throw new Error("xAI runtime authentication was incomplete.");
@@ -2770,6 +2812,7 @@ function hasOfficialUrlOrigin(value, providerId) {
2770
2812
  return ["https://inference.baseten.co", "https://api.baseten.co"].includes(url.origin);
2771
2813
  }
2772
2814
  if (providerId === "openai-codex") return url.origin === "https://chatgpt.com";
2815
+ if (providerId === "openai") return url.origin === "https://api.openai.com";
2773
2816
  if (providerId === "deepseek") return url.origin === "https://api.deepseek.com";
2774
2817
  if (providerId === "fireworks") return url.origin === "https://api.fireworks.ai";
2775
2818
  if (providerId === "openrouter") return url.origin === "https://openrouter.ai";
@@ -2795,9 +2838,6 @@ function headerValue(headers, name) {
2795
2838
  const entry = Object.entries(headers ?? {}).find(([candidate]) => candidate.toLowerCase() === name.toLowerCase());
2796
2839
  return entry?.[1] ?? void 0;
2797
2840
  }
2798
- function hasHeader(headers, name) {
2799
- return Object.keys(headers).some((key) => key.toLowerCase() === name.toLowerCase());
2800
- }
2801
2841
  function validatedXaiUserId(value) {
2802
2842
  if (typeof value !== "string" || !/^[A-Za-z0-9._~-]{1,128}$/u.test(value)) {
2803
2843
  throw new Error("xAI consumer identity returned an unsafe canonical user ID.");
@@ -3999,7 +4039,7 @@ import { randomUUID as randomUUID2 } from "node:crypto";
3999
4039
 
4000
4040
  // src/codex-fast-runtime.ts
4001
4041
  var NO_FAST_REQUEST = /* @__PURE__ */ Symbol("no-fast-request");
4002
- var FAST_USAGE_WARNING = "Fast is about 1.5\xD7 faster and uses more of your plan allowance.";
4042
+ var FAST_USAGE_WARNING = "Fast uses priority routing and consumes more plan allowance or API credits.";
4003
4043
  function registerCodexFastMode(pi, settingsRuntime, refreshStatus, options = {}) {
4004
4044
  let sessionController = new AbortController();
4005
4045
  let generation = 0;
@@ -4036,7 +4076,7 @@ function registerCodexFastMode(pi, settingsRuntime, refreshStatus, options = {})
4036
4076
  if (!ctx.hasUI) throw new Error("/fast requires TUI or RPC mode.");
4037
4077
  const availability = codexFastAvailability(ctx.model, settingsRuntime.get().settings.codexFastMode);
4038
4078
  if (availability.kind === "not-codex") {
4039
- ctx.ui.notify("/fast is available only for the active OpenAI Codex model.", "warning");
4079
+ ctx.ui.notify("/fast requires a supported OpenAI or OpenAI Codex model.", "warning");
4040
4080
  return;
4041
4081
  }
4042
4082
  if (availability.kind === "unavailable") {