@narumitw/pi-usage 0.64.2 → 0.65.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/dist/index.ts CHANGED
@@ -47,21 +47,17 @@ import {
47
47
  runWithConcurrency,
48
48
  sanitizeDisplayText,
49
49
  usageAdapters
50
- } from "./chunks/chunk-ACPQJ2CM.ts";
50
+ } from "./chunks/chunk-N2ZD5X4K.ts";
51
51
 
52
52
  // src/codex-fast.ts
53
53
  import { calculateCost, hasApi } from "@earendil-works/pi-ai";
54
54
  var CODEX_FAST_SERVICE_TIER = "priority";
55
55
  var CODEX_STANDARD_SERVICE_TIER = "default";
56
- var CODEX_FAST_MODEL_IDS = /* @__PURE__ */ new Set([
57
- "gpt-5.5",
58
- "gpt-5.6-luna",
59
- "gpt-5.6-sol",
60
- "gpt-5.6-terra",
61
- "gpt-6-sol",
62
- "gpt-6.1-sol"
63
- ]);
64
- function codexFastAvailability(model, enabled) {
56
+ var UNKNOWN_CODEX_FAST_CAPABILITY = {
57
+ kind: "unknown",
58
+ reason: "Codex Fast capability information has not been fetched."
59
+ };
60
+ function codexFastAvailability(model, enabled, capability = UNKNOWN_CODEX_FAST_CAPABILITY) {
65
61
  if (model?.provider !== "openai-codex") return { kind: "not-codex" };
66
62
  if (!isOfficialCodexModel(model)) {
67
63
  return {
@@ -69,7 +65,8 @@ function codexFastAvailability(model, enabled) {
69
65
  reason: "Fast mode requires the official OpenAI Codex Responses endpoint."
70
66
  };
71
67
  }
72
- if (!CODEX_FAST_MODEL_IDS.has(model.id)) {
68
+ if (capability.kind === "unknown") return capability;
69
+ if (capability.kind === "unsupported") {
73
70
  return {
74
71
  kind: "unavailable",
75
72
  reason: `${model.id} does not advertise Codex Fast support.`
@@ -77,20 +74,20 @@ function codexFastAvailability(model, enabled) {
77
74
  }
78
75
  return { kind: "available", enabled };
79
76
  }
80
- function codexFastIsEffective(model, enabled) {
81
- return codexFastAvailability(model, enabled).kind === "available" && enabled;
77
+ function codexFastIsEffective(model, enabled, capability = UNKNOWN_CODEX_FAST_CAPABILITY) {
78
+ return codexFastAvailability(model, enabled, capability).kind === "available" && enabled;
82
79
  }
83
- function codexFastRequestTier(model, enabled) {
80
+ function codexFastRequestTier(model, enabled, capability = UNKNOWN_CODEX_FAST_CAPABILITY) {
84
81
  if (!isOfficialCodexModel(model)) return void 0;
85
- return enabled && CODEX_FAST_MODEL_IDS.has(model.id) ? CODEX_FAST_SERVICE_TIER : CODEX_STANDARD_SERVICE_TIER;
82
+ return enabled && capability.kind === "supported" ? CODEX_FAST_SERVICE_TIER : CODEX_STANDARD_SERVICE_TIER;
86
83
  }
87
- function rewriteCodexFastPayload(payload, model, enabled) {
88
- const serviceTier = codexFastRequestTier(model, enabled);
84
+ function rewriteCodexFastPayload(payload, model, enabled, capability = UNKNOWN_CODEX_FAST_CAPABILITY) {
85
+ const serviceTier = codexFastRequestTier(model, enabled, capability);
89
86
  if (!serviceTier || !isRecord(payload)) return void 0;
90
87
  return { ...payload, service_tier: serviceTier };
91
88
  }
92
- function correctCodexFastMessageCost(message, model, fastRequested) {
93
- if (!codexFastIsEffective(model, fastRequested) || !isRecord(message) || message.role !== "assistant" || message.provider !== model?.provider || message.model !== model?.id) {
89
+ function correctCodexFastMessageCost(message, model, fastRequested, capability = UNKNOWN_CODEX_FAST_CAPABILITY) {
90
+ if (!codexFastIsEffective(model, fastRequested, capability) || !isRecord(message) || message.role !== "assistant" || message.provider !== model?.provider || message.model !== model?.id) {
94
91
  return void 0;
95
92
  }
96
93
  const usage = isRecord(message.usage) ? message.usage : void 0;
@@ -1287,6 +1284,231 @@ function isProviderId(value) {
1287
1284
  // src/usage.ts
1288
1285
  import { randomUUID as randomUUID2 } from "node:crypto";
1289
1286
 
1287
+ // src/codex-fast-catalog.ts
1288
+ import { VERSION } from "@earendil-works/pi-coding-agent";
1289
+
1290
+ // src/usage-helpers.ts
1291
+ function configuredAdapters(ctx) {
1292
+ return usageAdapters().filter(
1293
+ (adapter) => adapter.id === ctx.model?.provider || providerIsConfigured(ctx, adapter.id)
1294
+ );
1295
+ }
1296
+ function providerDisplayName(ctx, providerId) {
1297
+ try {
1298
+ return sanitizeDisplayText(ctx.modelRegistry.getProviderDisplayName(providerId), 80);
1299
+ } catch {
1300
+ return sanitizeDisplayText(providerId, 80);
1301
+ }
1302
+ }
1303
+ function setBoundedMap(map, key, value, limit) {
1304
+ map.delete(key);
1305
+ while (map.size >= limit) {
1306
+ const oldest = map.keys().next().value;
1307
+ if (oldest === void 0) break;
1308
+ map.delete(oldest);
1309
+ }
1310
+ map.set(key, value);
1311
+ }
1312
+ function modelIdentity(model) {
1313
+ return model ? `${model.provider}/${model.id}` : void 0;
1314
+ }
1315
+ function isAbortError(error) {
1316
+ return error instanceof Error && error.name === "AbortError";
1317
+ }
1318
+ function isTimeoutError(error) {
1319
+ return error instanceof Error && error.name === "TimeoutError";
1320
+ }
1321
+
1322
+ // src/codex-fast-catalog.ts
1323
+ var CACHE_TTL_MS = 5 * 60 * 1e3;
1324
+ var FAILURE_BACKOFF_MS = 3e4;
1325
+ var MAX_ACCOUNT_STATES = 32;
1326
+ var MAX_DIRECTORY_BODY_BYTES = 8 * 1024 * 1024;
1327
+ var DIRECTORY_URL = new URL("https://chatgpt.com/backend-api/codex/models");
1328
+ DIRECTORY_URL.searchParams.set("client_version", VERSION);
1329
+ function createCodexFastCatalog(options = {}) {
1330
+ const accounts = /* @__PURE__ */ new Map();
1331
+ const requests = /* @__PURE__ */ new Map();
1332
+ const latestRequests = /* @__PURE__ */ new Map();
1333
+ const timeoutMs = options.timeoutMs ?? 15e3;
1334
+ let requestSequence = 0;
1335
+ let identitySequence = 0;
1336
+ let generation = 0;
1337
+ let active;
1338
+ let issue = UNKNOWN_CODEX_FAST_CAPABILITY.reason;
1339
+ const current = (scope, ctx, signal) => !signal.aborted && generation === scope.generation && active?.key === scope.key && active.fingerprint === scope.fingerprint && active.model === scope.model && active.session === scope.session && modelIdentity2(ctx.model) === scope.model && ctx.sessionManager.getSessionId() === scope.session;
1340
+ const capability = (model, key = active?.key) => {
1341
+ const entry = key ? accounts.get(key) : void 0;
1342
+ const supported = model ? entry?.models?.get(model.id) : void 0;
1343
+ if (supported !== void 0) return { kind: supported ? "supported" : "unsupported" };
1344
+ return {
1345
+ kind: "unknown",
1346
+ reason: entry?.issue ?? (entry?.models && model ? `The Codex model directory has no Fast capability information for ${model.id}.` : issue)
1347
+ };
1348
+ };
1349
+ const identify = async (ctx, signal) => {
1350
+ const identity = ++identitySequence;
1351
+ const owner = generation;
1352
+ const model = modelIdentity2(ctx.model);
1353
+ const session = ctx.sessionManager.getSessionId();
1354
+ if (!isOfficialCodexModel(ctx.model)) {
1355
+ active = void 0;
1356
+ return void 0;
1357
+ }
1358
+ try {
1359
+ const auth = await awaitWithDeadline(
1360
+ resolveCodexResetAuth(ctx, void 0, options.credentialReader, options.candidateReader),
1361
+ signal,
1362
+ timeoutMs,
1363
+ "resolving the current Codex model-directory account"
1364
+ );
1365
+ if (signal.aborted || owner !== generation || identity !== identitySequence || modelIdentity2(ctx.model) !== model || ctx.sessionManager.getSessionId() !== session) {
1366
+ return void 0;
1367
+ }
1368
+ const accountId = auth.headers["chatgpt-account-id"];
1369
+ if (!accountId) throw new Error("The active Codex account ID is unavailable.");
1370
+ const scope = {
1371
+ key: fingerprintResolvedAuth({ headers: { "chatgpt-account-id": accountId } }, AUTH_FINGERPRINT_SALT),
1372
+ fingerprint: auth.fingerprint,
1373
+ model,
1374
+ session,
1375
+ generation: owner
1376
+ };
1377
+ const changed = active?.key !== scope.key;
1378
+ active = scope;
1379
+ issue = "Codex Fast capability information has not been fetched for this account.";
1380
+ if (changed) options.onChanged?.(ctx);
1381
+ return { auth, scope };
1382
+ } catch (error) {
1383
+ if (signal.aborted || owner !== generation || identity !== identitySequence) return void 0;
1384
+ active = void 0;
1385
+ issue = `Codex Fast capability is unknown: ${redactUsageError(errorMessage(error))}`;
1386
+ options.onChanged?.(ctx);
1387
+ return void 0;
1388
+ }
1389
+ };
1390
+ const load = async (ctx, auth, scope, signal, force) => {
1391
+ const entry = accounts.get(scope.key);
1392
+ if (!force && (entry?.models && entry.fingerprint === auth.fingerprint && Date.now() - entry.fetchedAt < CACHE_TTL_MS || entry?.retryAt && Date.now() < entry.retryAt)) {
1393
+ return;
1394
+ }
1395
+ const before = capability(ctx.model, scope.key);
1396
+ const existing = requests.get(auth.fingerprint);
1397
+ let promise = existing?.promise;
1398
+ if (!promise) {
1399
+ const sequence = ++requestSequence;
1400
+ latestRequests.set(scope.key, sequence);
1401
+ const controller = new AbortController();
1402
+ const combined = AbortSignal.any([signal, controller.signal]);
1403
+ let status;
1404
+ promise = (async () => {
1405
+ try {
1406
+ const payload = await fetchProviderJson(
1407
+ DIRECTORY_URL.href,
1408
+ auth,
1409
+ combined,
1410
+ timeoutMs,
1411
+ "Codex model directory",
1412
+ {
1413
+ redirect: "error",
1414
+ maxSuccessBodyBytes: MAX_DIRECTORY_BODY_BYTES,
1415
+ responseError: (responseStatus) => {
1416
+ status = responseStatus;
1417
+ return void 0;
1418
+ }
1419
+ }
1420
+ );
1421
+ const models = normalizeDirectory(payload);
1422
+ if (combined.aborted || scope.generation !== generation || latestRequests.get(scope.key) !== sequence) return;
1423
+ setBoundedMap(
1424
+ accounts,
1425
+ scope.key,
1426
+ { models, fetchedAt: Date.now(), fingerprint: auth.fingerprint },
1427
+ MAX_ACCOUNT_STATES
1428
+ );
1429
+ } catch (error) {
1430
+ if (combined.aborted || scope.generation !== generation || latestRequests.get(scope.key) !== sequence) return;
1431
+ const previous = accounts.get(scope.key);
1432
+ const transient = status === void 0 || status >= 500 || status === 429;
1433
+ setBoundedMap(
1434
+ accounts,
1435
+ scope.key,
1436
+ {
1437
+ ...transient && previous?.models ? previous : { fetchedAt: 0 },
1438
+ retryAt: Date.now() + FAILURE_BACKOFF_MS,
1439
+ issue: `Codex Fast capability is unknown: ${redactUsageError(errorMessage(error), auth.secrets)}`
1440
+ },
1441
+ MAX_ACCOUNT_STATES
1442
+ );
1443
+ } finally {
1444
+ if (requests.get(auth.fingerprint)?.controller === controller) requests.delete(auth.fingerprint);
1445
+ if (latestRequests.get(scope.key) === sequence) latestRequests.delete(scope.key);
1446
+ }
1447
+ })();
1448
+ requests.set(auth.fingerprint, { controller, promise });
1449
+ }
1450
+ try {
1451
+ await awaitWithDeadline(promise, signal, timeoutMs, "fetching the Codex model directory");
1452
+ } catch (error) {
1453
+ if (signal.aborted) return;
1454
+ throw error;
1455
+ }
1456
+ const after = capability(ctx.model, scope.key);
1457
+ if (current(scope, ctx, signal) && JSON.stringify(before) !== JSON.stringify(after)) options.onChanged?.(ctx);
1458
+ };
1459
+ return {
1460
+ get(model) {
1461
+ return capability(model);
1462
+ },
1463
+ async read(ctx, signal, refresh = false, force = false) {
1464
+ const resolved = await identify(ctx, signal);
1465
+ if (!resolved) return void 0;
1466
+ const { auth, scope } = resolved;
1467
+ if (refresh) await load(ctx, auth, scope, signal, force || capability(ctx.model, scope.key).kind === "unknown");
1468
+ else void load(ctx, auth, scope, signal, false);
1469
+ return current(scope, ctx, signal) ? capability(ctx.model, scope.key) : void 0;
1470
+ },
1471
+ reset(clearCache = false) {
1472
+ generation += 1;
1473
+ identitySequence += 1;
1474
+ active = void 0;
1475
+ issue = UNKNOWN_CODEX_FAST_CAPABILITY.reason;
1476
+ for (const { controller } of requests.values()) controller.abort();
1477
+ requests.clear();
1478
+ latestRequests.clear();
1479
+ if (clearCache) accounts.clear();
1480
+ }
1481
+ };
1482
+ }
1483
+ function normalizeDirectory(payload) {
1484
+ if (!Array.isArray(payload.models)) throw new Error("The Codex model directory returned invalid models.");
1485
+ const models = /* @__PURE__ */ new Map();
1486
+ for (const item of payload.models) {
1487
+ if (!isRecord3(item) || typeof item.slug !== "string" || !item.slug.trim() || models.has(item.slug)) {
1488
+ throw new Error("The Codex model directory returned an invalid or duplicate model ID.");
1489
+ }
1490
+ const hasTiers = item.service_tiers !== void 0;
1491
+ const hasLegacy = item.additional_speed_tiers !== void 0;
1492
+ if (hasTiers && (!Array.isArray(item.service_tiers) || item.service_tiers.some((tier) => !isRecord3(tier) || typeof tier.id !== "string")) || hasLegacy && (!Array.isArray(item.additional_speed_tiers) || item.additional_speed_tiers.some((tier) => typeof tier !== "string"))) {
1493
+ throw new Error("The Codex model directory returned invalid service tiers.");
1494
+ }
1495
+ if (!hasTiers && !hasLegacy) {
1496
+ models.set(item.slug, void 0);
1497
+ continue;
1498
+ }
1499
+ const tiers = item.service_tiers ?? [];
1500
+ const legacy = item.additional_speed_tiers ?? [];
1501
+ models.set(item.slug, tiers.some((tier) => tier.id === "priority") || legacy.includes("fast"));
1502
+ }
1503
+ return models;
1504
+ }
1505
+ function modelIdentity2(model) {
1506
+ return model ? `${model.provider}/${model.id}` : "";
1507
+ }
1508
+ function isRecord3(value) {
1509
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1510
+ }
1511
+
1290
1512
  // src/codex-fast-runtime.ts
1291
1513
  var NO_FAST_REQUEST = /* @__PURE__ */ Symbol("no-fast-request");
1292
1514
  var FAST_USAGE_WARNING = "Fast is about 1.5\xD7 faster and uses more of your plan allowance.";
@@ -1294,14 +1516,58 @@ function registerCodexFastMode(pi, settingsRuntime, refreshStatus, options = {})
1294
1516
  let sessionController = new AbortController();
1295
1517
  let generation = 0;
1296
1518
  const pendingFastRequests = /* @__PURE__ */ new Map();
1519
+ const catalog = createCodexFastCatalog({
1520
+ credentialReader: options.credentialReader,
1521
+ candidateReader: options.candidateReader ?? createOAuthCredentialCandidateReader(pi, options.credentialReader),
1522
+ timeoutMs: options.timeoutMs,
1523
+ onChanged(ctx) {
1524
+ if (sessionController.signal.aborted || !settingsRuntime.get().settings.codexFastMode) return;
1525
+ try {
1526
+ refreshStatus(ctx);
1527
+ } catch (error) {
1528
+ if (!isStaleExtensionContextError(error)) throw error;
1529
+ }
1530
+ }
1531
+ });
1297
1532
  const toggle = async (ctx, enabled, callerSignal) => {
1298
1533
  const ownerGeneration = generation;
1299
1534
  const sessionId = ctx.sessionManager.getSessionId();
1300
1535
  const signal = callerSignal ? AbortSignal.any([callerSignal, sessionController.signal]) : sessionController.signal;
1536
+ if (ctx.model?.provider !== "openai-codex") {
1537
+ ctx.ui.notify("/fast is available only for the active OpenAI Codex model.", "warning");
1538
+ return false;
1539
+ }
1540
+ if (!isOfficialCodexModel(ctx.model)) {
1541
+ ctx.ui.notify("Fast mode requires the official OpenAI Codex Responses endpoint.", "warning");
1542
+ return false;
1543
+ }
1544
+ if (settingsRuntime.get().kind === "invalid") {
1545
+ ctx.ui.notify("pi-usage.json is invalid; repair it and run /reload before changing Fast mode.", "error");
1546
+ return false;
1547
+ }
1548
+ if (enabled) {
1549
+ const capability = await catalog.read(ctx, signal, true, catalog.get(ctx.model).kind !== "supported");
1550
+ if (signal.aborted || ownerGeneration !== generation) return false;
1551
+ if (!capability) {
1552
+ const latest = catalog.get(ctx.model);
1553
+ ctx.ui.notify(
1554
+ latest.kind === "unknown" ? latest.reason : "The active Codex account changed while checking Fast capability.",
1555
+ "warning"
1556
+ );
1557
+ return false;
1558
+ }
1559
+ const availability = codexFastAvailability(ctx.model, false, capability);
1560
+ if (availability.kind !== "available") {
1561
+ if (availability.kind === "unknown" || availability.kind === "unavailable") {
1562
+ ctx.ui.notify(availability.reason, "warning");
1563
+ }
1564
+ return false;
1565
+ }
1566
+ }
1301
1567
  try {
1302
1568
  await settingsRuntime.update({ codexFastMode: enabled }, signal);
1303
1569
  } catch (error) {
1304
- if (isAbortError(error) || isStaleExtensionContextError(error)) return false;
1570
+ if (isAbortError2(error) || isStaleExtensionContextError(error)) return false;
1305
1571
  ctx.ui.notify(`Could not save pi-usage.json: ${errorMessage(error)}`, "error");
1306
1572
  return false;
1307
1573
  }
@@ -1324,26 +1590,18 @@ function registerCodexFastMode(pi, settingsRuntime, refreshStatus, options = {})
1324
1590
  return;
1325
1591
  }
1326
1592
  if (!ctx.hasUI) throw new Error("/fast requires TUI or RPC mode.");
1327
- const availability = codexFastAvailability(ctx.model, settingsRuntime.get().settings.codexFastMode);
1328
- if (availability.kind === "not-codex") {
1329
- ctx.ui.notify("/fast is available only for the active OpenAI Codex model.", "warning");
1330
- return;
1331
- }
1332
- if (availability.kind === "unavailable") {
1333
- ctx.ui.notify(availability.reason, "warning");
1334
- return;
1335
- }
1336
- if (settingsRuntime.get().kind === "invalid") {
1337
- ctx.ui.notify("pi-usage.json is invalid; repair it and run /reload before changing Fast mode.", "error");
1338
- return;
1339
- }
1340
- await toggle(ctx, !availability.enabled);
1593
+ await toggle(ctx, !settingsRuntime.get().settings.codexFastMode);
1341
1594
  }
1342
1595
  });
1596
+ const refreshCapabilities = (ctx, callerSignal, force = false) => {
1597
+ const signal = callerSignal ? AbortSignal.any([callerSignal, sessionController.signal]) : sessionController.signal;
1598
+ return catalog.read(ctx, signal, true, force);
1599
+ };
1343
1600
  const prepareSession = (ctx) => {
1344
1601
  const sessionId = ctx.sessionManager.getSessionId();
1345
1602
  generation += 1;
1346
1603
  sessionController.abort();
1604
+ catalog.reset();
1347
1605
  pendingFastRequests.clear();
1348
1606
  sessionController = new AbortController();
1349
1607
  const ownerGeneration = generation;
@@ -1365,18 +1623,28 @@ function registerCodexFastMode(pi, settingsRuntime, refreshStatus, options = {})
1365
1623
  ctx.ui.notify(`Invalid pi-usage.json; using defaults without overwriting it. ${state.issue}`, "warning");
1366
1624
  }
1367
1625
  refreshStatus(ctx);
1626
+ if (state.kind !== "invalid" && isOfficialCodexModel(ctx.model)) void refreshCapabilities(ctx);
1368
1627
  })();
1369
1628
  };
1370
1629
  if (options.registerSessionStart !== false) {
1371
1630
  pi.on("session_start", async (_event, ctx) => prepareSession(ctx));
1372
1631
  }
1373
- pi.on("before_provider_request", (event, ctx) => {
1374
- const rewritten = rewriteCodexFastPayload(event.payload, ctx.model, settingsRuntime.get().settings.codexFastMode);
1375
- const key = activeRequestKey(ctx);
1376
- if (key && ctx.model) {
1632
+ pi.on("model_select", (_event, ctx) => {
1633
+ void refreshCapabilities(ctx);
1634
+ });
1635
+ pi.on("before_provider_request", async (event, ctx) => {
1636
+ const ownerGeneration = generation;
1637
+ const model = ctx.model;
1638
+ const enabled = settingsRuntime.get().settings.codexFastMode;
1639
+ const capability = enabled && isOfficialCodexModel(model) ? await catalog.read(ctx, sessionController.signal) ?? UNKNOWN_CODEX_FAST_CAPABILITY : UNKNOWN_CODEX_FAST_CAPABILITY;
1640
+ if (sessionController.signal.aborted || ownerGeneration !== generation) return void 0;
1641
+ const rewritten = rewriteCodexFastPayload(event.payload, model, enabled, capability);
1642
+ const key = activeRequestKey(ctx, model);
1643
+ if (key && model) {
1377
1644
  pendingFastRequests.set(key, {
1378
- fastRequested: isRecord3(rewritten) && rewritten.service_tier === "priority",
1379
- model: ctx.model
1645
+ fastRequested: isRecord4(rewritten) && rewritten.service_tier === "priority",
1646
+ model,
1647
+ capability
1380
1648
  });
1381
1649
  }
1382
1650
  return rewritten;
@@ -1384,32 +1652,43 @@ function registerCodexFastMode(pi, settingsRuntime, refreshStatus, options = {})
1384
1652
  pi.on("message_end", (event, ctx) => {
1385
1653
  const request = consumeFastRequest(ctx, event.message, pendingFastRequests);
1386
1654
  if (request === NO_FAST_REQUEST) return void 0;
1387
- const message = correctCodexFastMessageCost(event.message, request.model, request.fastRequested);
1655
+ const message = correctCodexFastMessageCost(
1656
+ event.message,
1657
+ request.model,
1658
+ request.fastRequested,
1659
+ request.capability
1660
+ );
1388
1661
  return message ? { message } : void 0;
1389
1662
  });
1390
1663
  pi.on("session_shutdown", async () => {
1391
1664
  generation += 1;
1392
1665
  sessionController.abort();
1666
+ catalog.reset(true);
1393
1667
  pendingFastRequests.clear();
1394
1668
  await settingsRuntime.flush();
1395
1669
  });
1396
1670
  return {
1397
1671
  prepareSession,
1672
+ refreshCapabilities,
1398
1673
  availability(model) {
1399
- return codexFastAvailability(model, settingsRuntime.get().settings.codexFastMode);
1674
+ return codexFastAvailability(model, settingsRuntime.get().settings.codexFastMode, catalog.get(model));
1400
1675
  },
1401
1676
  decorateStatus(model, status) {
1402
- return codexFastStatusLabel(status, codexFastIsEffective(model, settingsRuntime.get().settings.codexFastMode));
1677
+ const enabled = settingsRuntime.get().settings.codexFastMode;
1678
+ const capability = catalog.get(model);
1679
+ if (enabled && codexFastAvailability(model, enabled, capability).kind === "unknown" && /^codex(?:\s|$)/u.test(status)) {
1680
+ return `codex (Fast unknown)${status.slice("codex".length)}`;
1681
+ }
1682
+ return codexFastStatusLabel(status, codexFastIsEffective(model, enabled, capability));
1403
1683
  },
1404
1684
  toggle
1405
1685
  };
1406
1686
  }
1407
- function activeRequestKey(ctx) {
1408
- const model = ctx.model;
1687
+ function activeRequestKey(ctx, model = ctx.model) {
1409
1688
  return model ? `${ctx.sessionManager.getSessionId()}:${model.provider}/${model.id}` : void 0;
1410
1689
  }
1411
1690
  function consumeFastRequest(ctx, message, pending) {
1412
- if (!isRecord3(message) || message.role !== "assistant") return NO_FAST_REQUEST;
1691
+ if (!isRecord4(message) || message.role !== "assistant") return NO_FAST_REQUEST;
1413
1692
  const key = messageRequestKey(ctx, message);
1414
1693
  if (!key) return NO_FAST_REQUEST;
1415
1694
  const request = pending.get(key);
@@ -1420,44 +1699,12 @@ function messageRequestKey(ctx, message) {
1420
1699
  if (typeof message.provider !== "string" || typeof message.model !== "string") return void 0;
1421
1700
  return `${ctx.sessionManager.getSessionId()}:${message.provider}/${message.model}`;
1422
1701
  }
1423
- function isRecord3(value) {
1702
+ function isRecord4(value) {
1424
1703
  return typeof value === "object" && value !== null && !Array.isArray(value);
1425
1704
  }
1426
- function isAbortError(error) {
1427
- return error instanceof Error && error.name === "AbortError";
1428
- }
1429
-
1430
- // src/usage-helpers.ts
1431
- function configuredAdapters(ctx) {
1432
- return usageAdapters().filter(
1433
- (adapter) => adapter.id === ctx.model?.provider || providerIsConfigured(ctx, adapter.id)
1434
- );
1435
- }
1436
- function providerDisplayName(ctx, providerId) {
1437
- try {
1438
- return sanitizeDisplayText(ctx.modelRegistry.getProviderDisplayName(providerId), 80);
1439
- } catch {
1440
- return sanitizeDisplayText(providerId, 80);
1441
- }
1442
- }
1443
- function setBoundedMap(map, key, value, limit) {
1444
- map.delete(key);
1445
- while (map.size >= limit) {
1446
- const oldest = map.keys().next().value;
1447
- if (oldest === void 0) break;
1448
- map.delete(oldest);
1449
- }
1450
- map.set(key, value);
1451
- }
1452
- function modelIdentity(model) {
1453
- return model ? `${model.provider}/${model.id}` : void 0;
1454
- }
1455
1705
  function isAbortError2(error) {
1456
1706
  return error instanceof Error && error.name === "AbortError";
1457
1707
  }
1458
- function isTimeoutError(error) {
1459
- return error instanceof Error && error.name === "TimeoutError";
1460
- }
1461
1708
 
1462
1709
  // src/usage-settings-ui.ts
1463
1710
  import { stripVTControlCharacters } from "node:util";
@@ -1597,13 +1844,13 @@ async function showUsageSettings(ctx, settingsRuntime, parentSignal, isCurrent,
1597
1844
  }
1598
1845
 
1599
1846
  // src/usage.ts
1600
- var CACHE_TTL_MS = 5 * 60 * 1e3;
1847
+ var CACHE_TTL_MS2 = 5 * 60 * 1e3;
1601
1848
  var STATUS_COUNTDOWN_REFRESH_MS = 60 * 1e3;
1602
1849
  var DEFAULT_TIMEOUT_MS = 15e3;
1603
1850
  var COMMAND_CODE_REVALIDATION_RESERVE_MS = 1e3;
1604
1851
  var ALL_PROVIDER_CONCURRENCY = 2;
1605
- var FAILURE_BACKOFF_MS = 3e4;
1606
- var MAX_ACCOUNT_STATES = 32;
1852
+ var FAILURE_BACKOFF_MS2 = 3e4;
1853
+ var MAX_ACCOUNT_STATES2 = 32;
1607
1854
  var STATUS_KEY = "usage";
1608
1855
  var REFRESH_CURRENT = "Refresh current usage";
1609
1856
  var VIEW_ANOTHER = "View another configured provider\u2026";
@@ -1620,7 +1867,7 @@ function usageExtension(pi, dependencies = {}) {
1620
1867
  const createRedemptionId = dependencies.createRedemptionId ?? randomUUID2;
1621
1868
  const settingsRuntime = dependencies.settingsRuntime ?? createUsageSettingsRuntime();
1622
1869
  const resolveUsageAuth2 = resolveUsageAuth;
1623
- const cache = new UsageCache(CACHE_TTL_MS);
1870
+ const cache = new UsageCache(CACHE_TTL_MS2);
1624
1871
  const failureBackoff = /* @__PURE__ */ new Map();
1625
1872
  const latestQueries = /* @__PURE__ */ new Map();
1626
1873
  const activeControllers = /* @__PURE__ */ new Set();
@@ -1669,7 +1916,7 @@ function usageExtension(pi, dependencies = {}) {
1669
1916
  statusRefreshTimer = void 0;
1670
1917
  if (!sessionActive || generation !== statusGeneration) return;
1671
1918
  startStatusRefresh(ctx, model, true);
1672
- }, CACHE_TTL_MS);
1919
+ }, CACHE_TTL_MS2);
1673
1920
  statusRefreshTimer.unref?.();
1674
1921
  };
1675
1922
  const publishStatus = (ctx, outcome, model, shouldSchedule) => {
@@ -1752,7 +1999,7 @@ function usageExtension(pi, dependencies = {}) {
1752
1999
  `resolving ${adapter.displayName} runtime auth`
1753
2000
  );
1754
2001
  } catch (error) {
1755
- if (isStaleExtensionContextError(error) || isAbortError2(error)) throw error;
2002
+ if (isStaleExtensionContextError(error) || isAbortError(error)) throw error;
1756
2003
  if (adapter.id === "openai") invalidateProviderState(adapter.id);
1757
2004
  const authState = error instanceof UnsupportedOpenAIUsageAuthError ? "unsupported" : adapter.id === "openai" && !isTimeoutError(error) ? "failed" : void 0;
1758
2005
  if (displayState === "current") {
@@ -1901,7 +2148,7 @@ function usageExtension(pi, dependencies = {}) {
1901
2148
  failureBackoff.delete(failureKey);
1902
2149
  querySequence += 1;
1903
2150
  queryId = querySequence;
1904
- setBoundedMap(latestQueries, failureKey, queryId, MAX_ACCOUNT_STATES);
2151
+ setBoundedMap(latestQueries, failureKey, queryId, MAX_ACCOUNT_STATES2);
1905
2152
  const queryTimeoutMs = Math.max(
1906
2153
  1,
1907
2154
  deadlineAt - Date.now() - (adapter.id === "command-code" ? COMMAND_CODE_REVALIDATION_RESERVE_MS : 0)
@@ -1932,7 +2179,7 @@ function usageExtension(pi, dependencies = {}) {
1932
2179
  rememberedTargetId: expectedTargetId
1933
2180
  };
1934
2181
  } catch (error) {
1935
- if (isStaleExtensionContextError(error) || isAbortError2(error)) throw error;
2182
+ if (isStaleExtensionContextError(error) || isAbortError(error)) throw error;
1936
2183
  if (retryableAuthChanged && authRetry === 0 && !signal.aborted && !requestContextChanged() && Date.now() < deadlineAt) {
1937
2184
  if (queryId !== void 0 && latestQueries.get(failureKey) === queryId) {
1938
2185
  latestQueries.delete(failureKey);
@@ -1946,7 +2193,7 @@ function usageExtension(pi, dependencies = {}) {
1946
2193
  }
1947
2194
  if (queryId === void 0 || latestQueries.get(failureKey) === queryId) {
1948
2195
  if (adapter.invalidateCacheOnFailure) cache.delete(adapter.id, queryFingerprint);
1949
- setBoundedMap(failureBackoff, failureKey, { until: now + FAILURE_BACKOFF_MS, message }, MAX_ACCOUNT_STATES);
2196
+ setBoundedMap(failureBackoff, failureKey, { until: now + FAILURE_BACKOFF_MS2, message }, MAX_ACCOUNT_STATES2);
1950
2197
  }
1951
2198
  return {
1952
2199
  state: {
@@ -2063,7 +2310,7 @@ function usageExtension(pi, dependencies = {}) {
2063
2310
  };
2064
2311
  const startStatusRefresh = (ctx, model, force) => {
2065
2312
  void refreshCurrentStatus(ctx, model, force).catch((error) => {
2066
- if (isStaleExtensionContextError(error) || isAbortError2(error)) return;
2313
+ if (isStaleExtensionContextError(error) || isAbortError(error)) return;
2067
2314
  const message = errorMessage(error);
2068
2315
  console.error("[pi-usage] refresh failed:", message);
2069
2316
  safeSetStatus(ctx, `usage err: ${message.slice(0, 50)}`);
@@ -2107,7 +2354,7 @@ function usageExtension(pi, dependencies = {}) {
2107
2354
  );
2108
2355
  return generation === statusGeneration && modelIdentity(ctx.model) === modelIdentity(model) && (!adapter.targets || settingsRuntime.get().settings.selectedTargets[adapter.id] === outcome.rememberedTargetId) && outcome.authState === "unavailable" && auth === void 0;
2109
2356
  } catch (error) {
2110
- if (isAbortError2(error) || isStaleExtensionContextError(error)) throw error;
2357
+ if (isAbortError(error) || isStaleExtensionContextError(error)) throw error;
2111
2358
  const unchangedFailure = outcome.authState === "unsupported" && error instanceof UnsupportedOpenAIUsageAuthError || outcome.authState === "failed" && outcome.state.status === "auth-unavailable" && !(error instanceof UnsupportedOpenAIUsageAuthError) && !isTimeoutError(error) && errorMessage(error) === outcome.state.message;
2112
2359
  return unchangedFailure && generation === statusGeneration && modelIdentity(ctx.model) === modelIdentity(model) && (!adapter.targets || settingsRuntime.get().settings.selectedTargets[adapter.id] === outcome.rememberedTargetId);
2113
2360
  }
@@ -2123,7 +2370,7 @@ function usageExtension(pi, dependencies = {}) {
2123
2370
  );
2124
2371
  return generation === statusGeneration && modelIdentity(ctx.model) === modelIdentity(model) && (!adapter.targets || settingsRuntime.get().settings.selectedTargets[adapter.id] === outcome.rememberedTargetId) && auth?.fingerprint === outcome.fingerprint;
2125
2372
  } catch (error) {
2126
- if (isAbortError2(error) || isStaleExtensionContextError(error)) throw error;
2373
+ if (isAbortError(error) || isStaleExtensionContextError(error)) throw error;
2127
2374
  return false;
2128
2375
  }
2129
2376
  };
@@ -2163,14 +2410,26 @@ function usageExtension(pi, dependencies = {}) {
2163
2410
  };
2164
2411
  const showMenu = async (ctx) => {
2165
2412
  if (!ctx.hasUI) throw new Error("/usage requires TUI or RPC mode.");
2166
- statusGeneration += 1;
2167
- const menuGeneration = statusGeneration;
2168
- statusController?.abort();
2169
- statusController = void 0;
2170
- clearStatusTimers();
2171
2413
  const controller = new AbortController();
2172
2414
  activeControllers.add(controller);
2173
2415
  try {
2416
+ if (isOfficialCodexModel(ctx.model)) {
2417
+ const refreshed = await runMenuOperation(
2418
+ ctx,
2419
+ "Checking Codex Fast capability\u2026",
2420
+ controller.signal,
2421
+ async (signal) => {
2422
+ await fastRuntime.refreshCapabilities(ctx, signal);
2423
+ return true;
2424
+ }
2425
+ );
2426
+ if (!refreshed || controller.signal.aborted) return;
2427
+ }
2428
+ statusGeneration += 1;
2429
+ const menuGeneration = statusGeneration;
2430
+ statusController?.abort();
2431
+ statusController = void 0;
2432
+ clearStatusTimers();
2174
2433
  let stableCurrent = await queryStableCurrent(ctx, false, controller, "Checking current usage\u2026");
2175
2434
  if (!stableCurrent) return;
2176
2435
  publishStableCurrent(ctx, stableCurrent);
@@ -2275,7 +2534,7 @@ function usageExtension(pi, dependencies = {}) {
2275
2534
  const fastAvailability = fastRuntime.availability(ctx.model);
2276
2535
  const targetState = actionableTargetState();
2277
2536
  const targetAdapter = adapterForProvider(targetState?.providerId);
2278
- const fastLines = fastAvailability.kind === "available" ? [`Fast mode: ${fastAvailability.enabled ? "On" : "Off"}`, FAST_USAGE_WARNING] : fastAvailability.kind === "unavailable" ? [`Fast mode: Unavailable \xB7 ${fastAvailability.reason}`] : [];
2537
+ const fastLines = fastAvailability.kind === "available" ? [`Fast mode: ${fastAvailability.enabled ? "On" : "Off"}`, FAST_USAGE_WARNING] : fastAvailability.kind === "unavailable" ? [`Fast mode: Unavailable \xB7 ${fastAvailability.reason}`] : fastAvailability.kind === "unknown" ? [`Fast mode: Unknown \xB7 ${fastAvailability.reason}`] : [];
2279
2538
  return {
2280
2539
  kind: "actions",
2281
2540
  title: "Provider usage",
@@ -2290,10 +2549,10 @@ function usageExtension(pi, dependencies = {}) {
2290
2549
  action: "target"
2291
2550
  }
2292
2551
  ] : [],
2293
- ...fastAvailability.kind === "available" ? [
2552
+ ...fastAvailability.kind === "available" || fastState.settings.codexFastMode && isOfficialCodexModel(ctx.model) ? [
2294
2553
  {
2295
2554
  id: "toggle-fast",
2296
- label: fastAvailability.enabled ? "Turn Fast mode off" : "Turn Fast mode on",
2555
+ label: fastState.settings.codexFastMode ? "Turn Fast mode off" : "Turn Fast mode on",
2297
2556
  description: fastState.kind === "invalid" ? "Repair pi-usage.json and reload before changing Fast mode." : FAST_USAGE_WARNING,
2298
2557
  disabled: fastState.kind === "invalid",
2299
2558
  action: "toggle-fast"
@@ -2422,7 +2681,7 @@ function usageExtension(pi, dependencies = {}) {
2422
2681
  ];
2423
2682
  return { kind: "stay" };
2424
2683
  } catch (error) {
2425
- if (isAbortError2(error) || isStaleExtensionContextError(error)) {
2684
+ if (isAbortError(error) || isStaleExtensionContextError(error)) {
2426
2685
  return { kind: "stay" };
2427
2686
  }
2428
2687
  ctx.ui.notify(`Could not select target: ${errorMessage(error)}`, "error");
@@ -2452,10 +2711,11 @@ function usageExtension(pi, dependencies = {}) {
2452
2711
  },
2453
2712
  "toggle-fast": async () => {
2454
2713
  const availability = fastRuntime.availability(ctx.model);
2455
- if (availability.kind !== "available" || fastState.kind === "invalid") {
2714
+ const enabled = settingsRuntime.get().settings.codexFastMode;
2715
+ if (availability.kind !== "available" && !enabled || fastState.kind === "invalid") {
2456
2716
  return { kind: "rejected" };
2457
2717
  }
2458
- const changed = await fastRuntime.toggle(ctx, !availability.enabled, controller.signal);
2718
+ const changed = await fastRuntime.toggle(ctx, !enabled, controller.signal);
2459
2719
  if (!changed) return { kind: "rejected" };
2460
2720
  fastState = settingsRuntime.get();
2461
2721
  return { kind: "stay" };
@@ -2479,7 +2739,7 @@ function usageExtension(pi, dependencies = {}) {
2479
2739
  try {
2480
2740
  availability = await listCodexResetCredits(auth, signal, DEFAULT_TIMEOUT_MS);
2481
2741
  } catch (error) {
2482
- if (isAbortError2(error) || summaryCount === void 0 || summaryCount <= 0) {
2742
+ if (isAbortError(error) || summaryCount === void 0 || summaryCount <= 0) {
2483
2743
  throw error;
2484
2744
  }
2485
2745
  availability = {
@@ -2512,7 +2772,7 @@ function usageExtension(pi, dependencies = {}) {
2512
2772
  screen: loaded.availability.availableCount > 0 ? "reset-picker" : "reset-result"
2513
2773
  };
2514
2774
  } catch (error) {
2515
- if (isAbortError2(error) || isStaleExtensionContextError(error)) {
2775
+ if (isAbortError(error) || isStaleExtensionContextError(error)) {
2516
2776
  return { kind: "stay" };
2517
2777
  }
2518
2778
  ctx.ui.notify(`Couldn't load usage limit resets: ${errorMessage(error)}`, "error");
@@ -2574,7 +2834,7 @@ function usageExtension(pi, dependencies = {}) {
2574
2834
  }
2575
2835
  return { kind: "to", screen: "reset-result" };
2576
2836
  } catch (error) {
2577
- if (isAbortError2(error) || isStaleExtensionContextError(error)) {
2837
+ if (isAbortError(error) || isStaleExtensionContextError(error)) {
2578
2838
  return { kind: "close" };
2579
2839
  }
2580
2840
  resetFailure = `Couldn't reset usage: ${errorMessage(error)}. Try again with the same request.`;
@@ -2728,7 +2988,7 @@ function usageExtension(pi, dependencies = {}) {
2728
2988
  try {
2729
2989
  await showMenu(ctx);
2730
2990
  } catch (error) {
2731
- if (isStaleExtensionContextError(error) || isAbortError2(error)) return;
2991
+ if (isStaleExtensionContextError(error) || isAbortError(error)) return;
2732
2992
  throw error;
2733
2993
  }
2734
2994
  }
@@ -2774,15 +3034,18 @@ function usageExtension(pi, dependencies = {}) {
2774
3034
  safeSetStatus(ctx, void 0);
2775
3035
  });
2776
3036
  fastRuntime = registerCodexFastMode(pi, settingsRuntime, (ctx) => startStatusRefresh(ctx, ctx.model, false), {
2777
- registerSessionStart: false
3037
+ registerSessionStart: false,
3038
+ credentialReader,
3039
+ candidateReader: credentialCandidates,
3040
+ timeoutMs: DEFAULT_TIMEOUT_MS
2778
3041
  });
2779
3042
  }
2780
3043
  export {
2781
- CODEX_FAST_MODEL_IDS,
2782
3044
  CODEX_FAST_SERVICE_TIER,
2783
3045
  CODEX_STANDARD_SERVICE_TIER,
2784
3046
  DEFAULT_USAGE_SETTINGS,
2785
3047
  SUPPORTED_ADAPTERS,
3048
+ UNKNOWN_CODEX_FAST_CAPABILITY,
2786
3049
  UsageCache,
2787
3050
  XAI_ADAPTER,
2788
3051
  abortError,
@@ -2805,6 +3068,7 @@ export {
2805
3068
  formatUsageReport,
2806
3069
  formatUsageStatusline,
2807
3070
  isBoundedTargetId,
3071
+ isOfficialCodexModel,
2808
3072
  isStaleExtensionContextError,
2809
3073
  listCodexResetCredits,
2810
3074
  listUsageTargets,