codex-multi-auth 2.19.1 → 2.21.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.
Files changed (79) hide show
  1. package/.codex-plugin/plugin.json +9 -9
  2. package/LICENSE +21 -21
  3. package/README.md +149 -148
  4. package/assets/codex-multi-auth-icon.svg +39 -39
  5. package/assets/readme-hero.svg +31 -31
  6. package/config/README.md +31 -31
  7. package/config/codex-legacy.json +1021 -1021
  8. package/config/codex-modern.json +347 -347
  9. package/config/minimal-codex.json +15 -15
  10. package/dist/index.js +1 -1
  11. package/dist/lib/accounts/rate-limits.js +5 -0
  12. package/dist/lib/accounts.js +5 -4
  13. package/dist/lib/auth/account-access.js +2 -2
  14. package/dist/lib/codex-cli/version.d.ts +22 -0
  15. package/dist/lib/codex-cli/version.js +78 -0
  16. package/dist/lib/codex-manager/commands/limits.d.ts +2 -0
  17. package/dist/lib/codex-manager/commands/limits.js +31 -0
  18. package/dist/lib/codex-manager/commands/status.js +1 -1
  19. package/dist/lib/codex-manager.js +2 -0
  20. package/dist/lib/constants.d.ts +2 -0
  21. package/dist/lib/constants.js +2 -0
  22. package/dist/lib/context-budget/model-context-windows.d.ts +1 -1
  23. package/dist/lib/context-budget/model-context-windows.js +4 -0
  24. package/dist/lib/context-budget-response.js +8 -8
  25. package/dist/lib/context-overflow.js +10 -10
  26. package/dist/lib/luna-reserve.d.ts +26 -0
  27. package/dist/lib/luna-reserve.js +68 -0
  28. package/dist/lib/oauth-success.html +125 -125
  29. package/dist/lib/policy/runtime-policy.js +14 -0
  30. package/dist/lib/prompts/codex-host-bridge.js +138 -138
  31. package/dist/lib/prompts/codex.js +79 -79
  32. package/dist/lib/request/helpers/model-map.d.ts +1 -1
  33. package/dist/lib/request/helpers/model-map.js +12 -0
  34. package/dist/lib/request/response-outcome.d.ts +1 -1
  35. package/dist/lib/request/response-outcome.js +4 -1
  36. package/dist/lib/runtime/account-status.js +2 -1
  37. package/dist/lib/runtime/luna-reserve-usage.d.ts +9 -0
  38. package/dist/lib/runtime/luna-reserve-usage.js +68 -0
  39. package/dist/lib/runtime/model-discovery-status.d.ts +6 -2
  40. package/dist/lib/runtime/model-discovery-status.js +13 -5
  41. package/dist/lib/runtime/reset-credit-routing.js +4 -1
  42. package/dist/lib/runtime/rotation-server-types.d.ts +2 -0
  43. package/dist/lib/runtime/runtime-observability.d.ts +2 -0
  44. package/dist/lib/runtime/runtime-observability.js +1 -0
  45. package/dist/lib/runtime-rotation-proxy.js +200 -32
  46. package/dist/lib/usage/pricing.d.ts +1 -1
  47. package/dist/lib/usage/pricing.js +4 -0
  48. package/dist/lib/usage/usage-extraction.d.ts +2 -0
  49. package/dist/lib/usage/usage-extraction.js +25 -1
  50. package/dist/lib/utils.d.ts +1 -1
  51. package/dist/lib/utils.js +1 -1
  52. package/node_modules/@codex-ai/plugin/dist/index.d.ts +2 -2
  53. package/node_modules/@codex-ai/plugin/dist/index.js +2 -2
  54. package/node_modules/@codex-ai/plugin/dist/tool.d.ts +42 -42
  55. package/node_modules/@codex-ai/plugin/dist/tool.js +29 -29
  56. package/node_modules/@codex-ai/plugin/package.json +9 -9
  57. package/node_modules/@codex-ai/sdk/dist/index.d.ts +4 -4
  58. package/node_modules/@codex-ai/sdk/dist/index.js +2 -2
  59. package/node_modules/@codex-ai/sdk/package.json +8 -8
  60. package/package.json +207 -207
  61. package/scripts/codex-app-launcher.js +653 -653
  62. package/scripts/codex-app-router.js +374 -373
  63. package/scripts/codex-bin-resolver.js +332 -332
  64. package/scripts/codex-multi-auth.js +65 -65
  65. package/scripts/codex-routing.js +62 -62
  66. package/scripts/codex.js +7006 -7005
  67. package/scripts/install-codex-auth-utils.js +213 -213
  68. package/scripts/mcodex.js +367 -367
  69. package/scripts/postinstall.js +114 -114
  70. package/scripts/preuninstall.js +265 -265
  71. package/skills/codex-auth-setup/SKILL.md +73 -73
  72. package/vendor/codex-ai-plugin/dist/index.d.ts +2 -2
  73. package/vendor/codex-ai-plugin/dist/index.js +2 -2
  74. package/vendor/codex-ai-plugin/dist/tool.d.ts +42 -42
  75. package/vendor/codex-ai-plugin/dist/tool.js +29 -29
  76. package/vendor/codex-ai-plugin/package.json +9 -9
  77. package/vendor/codex-ai-sdk/dist/index.d.ts +4 -4
  78. package/vendor/codex-ai-sdk/dist/index.js +2 -2
  79. package/vendor/codex-ai-sdk/package.json +8 -8
@@ -28,8 +28,9 @@ import { createServer } from "node:http";
28
28
  import * as zlib from "node:zlib";
29
29
  import { AccountManager, AUTH_INVALIDATION_MARKER, extractAccountId, } from "./accounts.js";
30
30
  import { withRoutingMutex } from "./routing-mutex.js";
31
+ import { getQuotaKey } from "./accounts/rate-limits.js";
31
32
  import { getFetchTimeoutMs, getNetworkErrorCooldownMs, getRetryAllAccountsMaxRetries, getServerErrorCooldownMs, getSessionAffinity, getSessionAffinityMaxEntries, getSessionAffinityTtlMs, getStreamStallTimeoutMs, getMinRotationIntervalMs, getTokenInvalidationCooldownMs, getTokenRefreshSkewMs, getPidOffsetEnabled, getPreemptiveQuotaEnabled, getPreemptiveQuotaMaxDeferralMs, getPreemptiveQuotaRemainingPercent5h, getPreemptiveQuotaRemainingPercent7d, getContextBudgetGuardEnabled, getContextBudgetGuardSoftPercent, getContextBudgetGuardHardPercent, getContextBudgetGuardModelWindowOverrides, getRoutingMutexMode, getSchedulingStrategy, loadPluginConfig, } from "./config.js";
32
- import { CODEX_BASE_URL, HTTP_STATUS, OPENAI_HEADERS, OPENAI_HEADER_VALUES, URL_PATHS, } from "./constants.js";
33
+ import { CODEX_BASE_URL, HTTP_STATUS, LUNA_RESERVE_MODEL, MAX_RATE_LIMIT_DELAY_MS, OPENAI_HEADERS, OPENAI_HEADER_VALUES, URL_PATHS, } from "./constants.js";
33
34
  import { getModelFamily } from "./prompts/codex.js";
34
35
  import { CURRENT_CODEX_MODEL } from "./request/helpers/model-map.js";
35
36
  import { recordRuntimeInferenceRequest, flushRuntimeInferenceActivity, mutateRuntimeObservabilitySnapshot, recordRuntimeAccountRecovery, recordRuntimePoolExhaustion, } from "./runtime/runtime-observability.js";
@@ -697,6 +698,38 @@ function buildResponsesRequestContext(req, body) {
697
698
  stableSessionKey: resolveStableSessionKey(headers, parsedBody),
698
699
  };
699
700
  }
701
+ const CONFIRMED_QUOTA_EXHAUSTION_PREFIX = "__confirmed-quota-exhaustion:";
702
+ function confirmedQuotaExhaustionKey(family, model) {
703
+ return `${CONFIRMED_QUOTA_EXHAUSTION_PREFIX}${getQuotaKey(family, model)}`;
704
+ }
705
+ /** Rewrite one ordinary Luna request to the separately metered Reserve model. */
706
+ function switchRequestToLunaReserve(context) {
707
+ if (context.model !== "gpt-6-luna" && context.model !== "gpt-5.6-luna")
708
+ return false;
709
+ const body = parseRequestBody(context.body);
710
+ if (!body)
711
+ return false;
712
+ body.model = LUNA_RESERVE_MODEL;
713
+ // Reserve availability is independent from paid Fast service-tier access.
714
+ // Let the backend choose its supported tier instead of carrying a Luna-only
715
+ // preference into the fallback request.
716
+ delete body.service_tier;
717
+ context.body = Buffer.from(JSON.stringify(body));
718
+ context.model = LUNA_RESERVE_MODEL;
719
+ context.family = getModelFamily(LUNA_RESERVE_MODEL);
720
+ return true;
721
+ }
722
+ /** True only for 429s that identify subscription usage/quota exhaustion. */
723
+ function isSubscriptionQuota429(status, bodyText) {
724
+ if (status !== HTTP_STATUS.TOO_MANY_REQUESTS)
725
+ return false;
726
+ const code = extractErrorCodeFromBody(bodyText)?.toLowerCase() ?? "";
727
+ if (code.includes("usage_limit") || code.includes("quota"))
728
+ return true;
729
+ if (code)
730
+ return false;
731
+ return /\busage[ _-]+limit[ _-]+(?:reached|exceeded|exhausted)\b|\bquota[ _-]+(?:reached|exceeded|exhausted)\b/i.test(bodyText);
732
+ }
700
733
  function buildImageRequestContext(req, body, pathname) {
701
734
  return {
702
735
  ...buildResponsesRequestContext(req, body),
@@ -1198,13 +1231,14 @@ async function handleRequestInner(state, req, res, traceId) {
1198
1231
  }
1199
1232
  const requestStartedAt = state.now();
1200
1233
  let policyDecision = null;
1234
+ let runtimePolicyState = null;
1201
1235
  let projectKey = null;
1202
1236
  let policyError = null;
1203
1237
  try {
1204
- const policyState = await loadRuntimePolicyState();
1205
- projectKey = policyState.project.projectKey;
1238
+ runtimePolicyState = await loadRuntimePolicyState();
1239
+ projectKey = runtimePolicyState.project.projectKey;
1206
1240
  policyDecision = await evaluateRuntimePolicy({
1207
- state: policyState,
1241
+ state: runtimePolicyState,
1208
1242
  accounts: accountManager.getAccountsSnapshot(),
1209
1243
  model: context.model,
1210
1244
  now: requestStartedAt,
@@ -1220,7 +1254,8 @@ async function handleRequestInner(state, req, res, traceId) {
1220
1254
  policyError = error instanceof Error ? error.message : String(error);
1221
1255
  state.status.lastError = policyError;
1222
1256
  }
1223
- usageRecorder = createRuntimeUsageRecorder({
1257
+ /** Build a recorder for the model that actually serves this request. */
1258
+ const createUsageRecorderForModel = (model) => createRuntimeUsageRecorder({
1224
1259
  source: "runtime-proxy",
1225
1260
  operation: isModelsRequest
1226
1261
  ? "models"
@@ -1229,11 +1264,12 @@ async function handleRequestInner(state, req, res, traceId) {
1229
1264
  : isImageRequest
1230
1265
  ? "images"
1231
1266
  : "responses",
1232
- model: context.model,
1267
+ model,
1233
1268
  projectKey,
1234
1269
  requestId: traceId,
1235
1270
  startedAt: requestStartedAt,
1236
1271
  });
1272
+ usageRecorder = createUsageRecorderForModel(context.model);
1237
1273
  if (policyError) {
1238
1274
  await usageRecorder.record({
1239
1275
  outcome: "failure",
@@ -1340,11 +1376,11 @@ async function handleRequestInner(state, req, res, traceId) {
1340
1376
  snapshot.lastAccountUpdatedAt = state.now();
1341
1377
  });
1342
1378
  });
1343
- const responseOutcome = new ResponseOutcome(upstream.headers.get("content-type")?.includes("text/event-stream") === true);
1344
1379
  const scanner = createUsageStreamScanner({
1345
1380
  contentType: upstream.headers.get("content-type"),
1346
1381
  onEvent: event => responseOutcome.observe(event),
1347
1382
  });
1383
+ const responseOutcome = new ResponseOutcome(() => scanner.isSse());
1348
1384
  const forwarded = await forwardStreamingResponse(upstream, res, state.status, () => controller.abort(), state.streamStallTimeoutMs, scanner.push, state.catalogEtag ? { "x-models-etag": state.catalogEtag } : undefined, () => { scanner.result(); return responseOutcome.finish(); });
1349
1385
  const attemptedCredential = attemptedCredentialIndex === undefined ? undefined : apiRoutes[attemptedCredentialIndex];
1350
1386
  if (!forwarded && attemptedCredential)
@@ -1483,6 +1519,35 @@ async function handleRequestInner(state, req, res, traceId) {
1483
1519
  // silently serving from another account.
1484
1520
  const pinnedIndex = state.forcedAccountIndex ?? storageMeta.pinnedAccountIndex;
1485
1521
  const isPinned = typeof pinnedIndex === "number";
1522
+ let requestModelCatalog;
1523
+ /** Rebuild native routing scopes after Luna is rewritten to separately metered Reserve. */
1524
+ const rebuildReserveWorkspaceCandidates = async () => {
1525
+ const modelCatalog = requestModelCatalog;
1526
+ if (!(state.nativeOpenai && isResponsesRequest && context.model === LUNA_RESERVE_MODEL && modelCatalog))
1527
+ return;
1528
+ const body = parseRequestBody(context.body);
1529
+ const effort = isRecord(body?.reasoning) && typeof body.reasoning.effort === "string" ? body.reasoning.effort : undefined;
1530
+ const tier = typeof body?.service_tier === "string" ? body.service_tier : undefined;
1531
+ if (!state.capabilityFailures)
1532
+ state.capabilityFailures = new RuntimeCapabilityFailures(state.now);
1533
+ const failures = state.capabilityFailures;
1534
+ const eligibleAccounts = accountManager.getAccountsSnapshot().filter((account) => account.enabled !== false && (!isPinned || account.index === pinnedIndex) && !policyDecision?.blockedAccountIndexes.has(account.index));
1535
+ const routableScopes = eligibleAccounts.flatMap(workspaceModelScopes).filter((scope) => scope.routable);
1536
+ await modelCatalog.prepareRouting(routableScopes.map((scope) => scope.id), context.model, effort, tier, (key) => failures.supports(key, context.model ?? LUNA_RESERVE_MODEL, effort, tier));
1537
+ workspaceCandidates.clear();
1538
+ catalogEligibleKeys = new Set();
1539
+ for (const key of Object.keys(subscriptionQuotaByAccount))
1540
+ delete subscriptionQuotaByAccount[Number(key)];
1541
+ for (const account of eligibleAccounts) {
1542
+ const candidates = workspaceModelScopes(account).filter((scope) => scope.routable)
1543
+ .sort((a, b) => Number(b.selected) - Number(a.selected) || Number(b.bound) - Number(a.bound))
1544
+ .filter((scope) => failures.supports(scope.id, context.model ?? LUNA_RESERVE_MODEL, effort, tier) && modelCatalog.supportsForRouting(scope.id, context.model ?? LUNA_RESERVE_MODEL, effort, tier));
1545
+ if (!candidates.length)
1546
+ continue;
1547
+ workspaceCandidates.set(account.index, candidates);
1548
+ catalogEligibleKeys.add(catalogAccountKey(account));
1549
+ }
1550
+ };
1486
1551
  if (state.nativeOpenai && (isModelsRequest || (isResponsesRequest && context.model))) {
1487
1552
  const requestedVersion = incomingUrl.searchParams.get("client_version") ?? incomingHeaders.get("version");
1488
1553
  const catalogClientVersion = requestedVersion && /^[0-9A-Za-z.+_-]{1,80}$/.test(requestedVersion) ? requestedVersion : undefined;
@@ -1557,6 +1622,7 @@ async function handleRequestInner(state, req, res, traceId) {
1557
1622
  return JSON.parse(Buffer.concat(chunks).toString("utf8"));
1558
1623
  }, state.now);
1559
1624
  const modelCatalog = state.modelCatalog;
1625
+ requestModelCatalog = modelCatalog;
1560
1626
  if (catalogsByVersion.size >= 4 && !catalogsByVersion.has(versionKey))
1561
1627
  catalogsByVersion.delete(catalogsByVersion.keys().next().value ?? "");
1562
1628
  catalogsByVersion.set(versionKey, modelCatalog);
@@ -1700,9 +1766,11 @@ async function handleRequestInner(state, req, res, traceId) {
1700
1766
  candidates.push(scope);
1701
1767
  }
1702
1768
  if (candidates.length) {
1703
- candidates.sort((a, b) => compareSubscriptionQuota(subscriptionQuotaPreference(quotaForScope(account, a), state.now()), subscriptionQuotaPreference(quotaForScope(account, b), state.now())));
1704
- const first = candidates[0];
1705
- subscriptionQuotaByAccount[account.index] = subscriptionQuotaPreference(first ? quotaForScope(account, first) : null, state.now());
1769
+ if (requestedModel !== LUNA_RESERVE_MODEL) {
1770
+ candidates.sort((a, b) => compareSubscriptionQuota(subscriptionQuotaPreference(quotaForScope(account, a), state.now()), subscriptionQuotaPreference(quotaForScope(account, b), state.now())));
1771
+ const first = candidates[0];
1772
+ subscriptionQuotaByAccount[account.index] = subscriptionQuotaPreference(first ? quotaForScope(account, first) : null, state.now());
1773
+ }
1706
1774
  workspaceCandidates.set(account.index, candidates);
1707
1775
  catalogEligibleKeys.add(catalogAccountKey(account));
1708
1776
  supported++;
@@ -1713,7 +1781,7 @@ async function handleRequestInner(state, req, res, traceId) {
1713
1781
  return;
1714
1782
  }
1715
1783
  }
1716
- if (state.nativeOpenai && isResponsesRequest && context.model && resetCreditState?.lastRedemptionAt !== undefined) {
1784
+ if (state.nativeOpenai && isResponsesRequest && context.model && context.model !== LUNA_RESERVE_MODEL && resetCreditState?.lastRedemptionAt !== undefined) {
1717
1785
  for (const [index, scopes] of workspaceCandidates) {
1718
1786
  const account = accountManager.getAccountByIndex(index);
1719
1787
  if (!account)
@@ -1724,7 +1792,7 @@ async function handleRequestInner(state, req, res, traceId) {
1724
1792
  observations: state.subscriptionQuotaObservations ??= new Map(), now: state.now(), clearQuotaScheduler: a => state.preemptiveQuotaScheduler.clear(buildQuotaScheduleKey(a, context.family, context.model)) });
1725
1793
  }
1726
1794
  }
1727
- if (state.nativeOpenai && isResponsesRequest && context.model && !isPinned) {
1795
+ if (state.nativeOpenai && isResponsesRequest && context.model && context.model !== LUNA_RESERVE_MODEL && !isPinned) {
1728
1796
  try {
1729
1797
  await recoverResetQuota({ model: context.model, family: context.family, native: true, pinned: false, manager: accountManager,
1730
1798
  scopes: workspaceCandidates, quotaForScope, priorityByAccount: policyDecision?.priorityByAccount, preferredIndex: storageMeta.pinnedAccountIndex, service: createResetCreditService(accountManager),
@@ -1738,6 +1806,52 @@ async function handleRequestInner(state, req, res, traceId) {
1738
1806
  // Recovery may have redeemed and written new reset state; the next request re-reads it.
1739
1807
  state.readCache?.delete("reset-credits");
1740
1808
  }
1809
+ // A pinned Luna request can arrive after an earlier turn already proved
1810
+ // ordinary subscription quota exhausted. That persisted family marker
1811
+ // would make chooseAccount reject the hard pin before any upstream call,
1812
+ // so move the request to the independently metered Reserve model first.
1813
+ if (isPinned &&
1814
+ (context.model === "gpt-6-luna" || context.model === "gpt-5.6-luna")) {
1815
+ const pinnedAccount = accountManager.getAccountByIndex(pinnedIndex);
1816
+ const now = state.now();
1817
+ const familyResetAt = pinnedAccount?.rateLimitResetTimes[getQuotaKey(context.family)];
1818
+ const modelResetAt = pinnedAccount?.rateLimitResetTimes[getQuotaKey(context.family, context.model)];
1819
+ const confirmedQuotaResetAt = pinnedAccount?.rateLimitResetTimes[confirmedQuotaExhaustionKey(context.family, context.model)];
1820
+ const persistedQuotaExhausted = typeof familyResetAt === "number" &&
1821
+ familyResetAt > now &&
1822
+ typeof confirmedQuotaResetAt === "number" &&
1823
+ confirmedQuotaResetAt > now &&
1824
+ !(typeof modelResetAt === "number" && modelResetAt > now);
1825
+ if (persistedQuotaExhausted && switchRequestToLunaReserve(context)) {
1826
+ usageRecorder = createUsageRecorderForModel(context.model);
1827
+ if (runtimePolicyState) {
1828
+ try {
1829
+ const reservePolicy = await evaluateRuntimePolicy({ state: runtimePolicyState, accounts: accountManager.getAccountsSnapshot(), model: context.model, now });
1830
+ policyDecision = reservePolicy;
1831
+ mutateRuntimeObservabilitySnapshot((snapshot) => {
1832
+ snapshot.policyBlockedIndexes = [...reservePolicy.blockedAccountIndexes];
1833
+ snapshot.policyBlockedReasons = Object.fromEntries([...reservePolicy.blockedAccountIndexes].map((index) => [String(index), "policy-blocked"]));
1834
+ });
1835
+ if (!reservePolicy.allowed) {
1836
+ await usageRecorder.record({ outcome: "blocked", statusCode: reservePolicy.statusCode, errorCode: reservePolicy.errorCode });
1837
+ writeJson(res, reservePolicy.statusCode, { error: { message: "Runtime policy blocked Luna Reserve fallback.", code: reservePolicy.errorCode ?? "policy_blocked", reasons: reservePolicy.reasons } });
1838
+ return;
1839
+ }
1840
+ }
1841
+ catch {
1842
+ await usageRecorder.record({ outcome: "failure", statusCode: HTTP_STATUS.SERVICE_UNAVAILABLE, errorCode: "runtime_policy_unavailable" });
1843
+ writeJson(res, HTTP_STATUS.SERVICE_UNAVAILABLE, { error: { message: "Runtime policy could not be loaded for Luna Reserve fallback.", code: "runtime_policy_unavailable" } });
1844
+ return;
1845
+ }
1846
+ }
1847
+ attemptedIndexes.clear();
1848
+ accountSkipReasons.clear();
1849
+ await rebuildReserveWorkspaceCandidates();
1850
+ state.status.lunaReserveFallbacks = (state.status.lunaReserveFallbacks ?? 0) + 1;
1851
+ mutateRuntimeObservabilitySnapshot((snapshot) => { snapshot.lunaReserveFallbacks = (snapshot.lunaReserveFallbacks ?? 0) + 1; });
1852
+ proxyLog.info("persisted Luna quota exhausted; routing pinned request through Luna Reserve", { traceId });
1853
+ }
1854
+ }
1741
1855
  // The token bucket spreads load across a selectable pool. A pin has no
1742
1856
  // alternative account, so exhausting that local heuristic can only reject a
1743
1857
  // request that the pinned account could serve. Keep circuit-breaker admission
@@ -1868,14 +1982,16 @@ async function handleRequestInner(state, req, res, traceId) {
1868
1982
  // calls inside `chooseAccount` are used unchanged and no lock is taken,
1869
1983
  // so default behavior and perf are identical to before.
1870
1984
  const selectAccount = () => {
1871
- for (const [index, scopes] of workspaceCandidates) {
1872
- const account = accountManager.getAccountByIndex(index);
1873
- if (!account)
1874
- continue;
1875
- scopes.sort((a, b) => compareSubscriptionQuota(subscriptionQuotaPreference(quotaForScope(account, a), state.now()), subscriptionQuotaPreference(quotaForScope(account, b), state.now())));
1876
- const scope = scopes[0];
1877
- if (scope)
1878
- subscriptionQuotaByAccount[index] = subscriptionQuotaPreference(quotaForScope(account, scope), state.now());
1985
+ if (context.model !== LUNA_RESERVE_MODEL) {
1986
+ for (const [index, scopes] of workspaceCandidates) {
1987
+ const account = accountManager.getAccountByIndex(index);
1988
+ if (!account)
1989
+ continue;
1990
+ scopes.sort((a, b) => compareSubscriptionQuota(subscriptionQuotaPreference(quotaForScope(account, a), state.now()), subscriptionQuotaPreference(quotaForScope(account, b), state.now())));
1991
+ const scope = scopes[0];
1992
+ if (scope)
1993
+ subscriptionQuotaByAccount[index] = subscriptionQuotaPreference(quotaForScope(account, scope), state.now());
1994
+ }
1879
1995
  }
1880
1996
  const result = chooseAccount({
1881
1997
  accountManager,
@@ -1888,7 +2004,7 @@ async function handleRequestInner(state, req, res, traceId) {
1888
2004
  policy: policyDecision,
1889
2005
  pinnedIndex,
1890
2006
  preferredIndex,
1891
- subscriptionQuotaByAccount: state.nativeOpenai && isResponsesRequest ? subscriptionQuotaByAccount : undefined,
2007
+ subscriptionQuotaByAccount: state.nativeOpenai && isResponsesRequest && context.model !== LUNA_RESERVE_MODEL ? subscriptionQuotaByAccount : undefined,
1892
2008
  fallbackPinnedIndex: state.nativeOpenai ? storageMeta.pinnedAccountIndex : null,
1893
2009
  skipReasons: accountSkipReasons,
1894
2010
  stickyBoostByAccount: rotationStickyBoost,
@@ -1964,7 +2080,9 @@ async function handleRequestInner(state, req, res, traceId) {
1964
2080
  attemptedIndexes.add(selected.index);
1965
2081
  const quotaScheduleKey = buildQuotaScheduleKey(selected, context.family, context.model);
1966
2082
  const preemptiveDeferral = state.preemptiveQuotaScheduler.getDeferral(quotaScheduleKey, state.now());
1967
- if (preemptiveDeferral.defer && preemptiveDeferral.waitMs > 0) {
2083
+ if (context.model !== LUNA_RESERVE_MODEL &&
2084
+ preemptiveDeferral.defer &&
2085
+ preemptiveDeferral.waitMs > 0) {
1968
2086
  accountSkipReasons.set(selected.index, preemptiveDeferral.reason ?? "quota-near-exhaustion");
1969
2087
  exhaustionReason = "rate-limit";
1970
2088
  capabilityRejectionIsLatest = false;
@@ -2253,8 +2371,9 @@ async function handleRequestInner(state, req, res, traceId) {
2253
2371
  }
2254
2372
  };
2255
2373
  const quotaSnapshot = readQuotaSchedulerSnapshot(upstream.headers, upstream.status, state.now());
2256
- if (quotaSnapshot)
2374
+ if (quotaSnapshot && context.model !== LUNA_RESERVE_MODEL) {
2257
2375
  observeQuota(quotaSnapshot, upstream.headers.get("x-codex-plan-type") ?? undefined);
2376
+ }
2258
2377
  if (isResponsesRequest && context.model && [400, 403, 404].includes(upstream.status)) {
2259
2378
  const errorBody = await readErrorBody(upstream, state.streamStallTimeoutMs, 65536);
2260
2379
  let data;
@@ -2309,7 +2428,9 @@ async function handleRequestInner(state, req, res, traceId) {
2309
2428
  // A capacity 429 is not this account's quota. Marking it rate
2310
2429
  // limited would take a healthy account out of the pool for the
2311
2430
  // retry-after window on an outage that affects every account.
2312
- if (isModelAtCapacityError(upstream.status, bodyText)) {
2431
+ const modelAtCapacity = isModelAtCapacityError(upstream.status, bodyText);
2432
+ const subscriptionQuotaExceeded = isSubscriptionQuota429(upstream.status, bodyText);
2433
+ if (modelAtCapacity) {
2313
2434
  const outcome = await waitOutModelCapacity(retryAfterHintMs, refreshed.account);
2314
2435
  if (outcome === "client-gone") {
2315
2436
  await usageRecorder.record({
@@ -2323,12 +2444,55 @@ async function handleRequestInner(state, req, res, traceId) {
2323
2444
  if (outcome === "retry")
2324
2445
  continue;
2325
2446
  }
2326
- state.preemptiveQuotaScheduler.markRateLimited(quotaScheduleKey, retryAfterMs, state.now());
2327
- // A 429 is the upstream quota signal for the attempted account, so
2328
- // keep the consumed runtime token drained.
2447
+ if (context.model !== LUNA_RESERVE_MODEL && subscriptionQuotaExceeded) {
2448
+ state.preemptiveQuotaScheduler.markRateLimited(quotaScheduleKey, retryAfterMs, state.now());
2449
+ }
2450
+ // Every 429 still drains this account for its retry window, but only an
2451
+ // explicit subscription-usage signal is persisted as quota exhaustion.
2329
2452
  accountManager.recordRateLimit(refreshed.account, context.family, context.model);
2330
- accountManager.markRateLimitedWithReason(refreshed.account, retryAfterMs, context.family, "quota", context.model);
2453
+ accountManager.markRateLimitedWithReason(refreshed.account, retryAfterMs, context.family, subscriptionQuotaExceeded ? "quota" : "unknown", context.model);
2454
+ if (subscriptionQuotaExceeded &&
2455
+ (context.model === "gpt-6-luna" || context.model === "gpt-5.6-luna")) {
2456
+ const confirmedKey = confirmedQuotaExhaustionKey(context.family, context.model);
2457
+ const confirmedResetAt = state.now() +
2458
+ Math.min(Math.max(0, Math.floor(retryAfterMs)), MAX_RATE_LIMIT_DELAY_MS);
2459
+ refreshed.account.rateLimitResetTimes[confirmedKey] = Math.max(refreshed.account.rateLimitResetTimes[confirmedKey] ?? 0, confirmedResetAt);
2460
+ }
2331
2461
  accountManager.saveToDiskDebounced();
2462
+ if (!modelAtCapacity && subscriptionQuotaExceeded && switchRequestToLunaReserve(context)) {
2463
+ usageRecorder = createUsageRecorderForModel(context.model);
2464
+ if (runtimePolicyState) {
2465
+ try {
2466
+ const reservePolicy = await evaluateRuntimePolicy({ state: runtimePolicyState, accounts: accountManager.getAccountsSnapshot(), model: context.model, now: state.now() });
2467
+ policyDecision = reservePolicy;
2468
+ mutateRuntimeObservabilitySnapshot((snapshot) => { snapshot.policyBlockedIndexes = [...reservePolicy.blockedAccountIndexes]; snapshot.policyBlockedReasons = Object.fromEntries([...reservePolicy.blockedAccountIndexes].map((index) => [String(index), "policy-blocked"])); });
2469
+ if (!reservePolicy.allowed) {
2470
+ await usageRecorder.record({ outcome: "blocked", statusCode: reservePolicy.statusCode, errorCode: reservePolicy.errorCode });
2471
+ writeJson(res, reservePolicy.statusCode, { error: { message: "Runtime policy blocked Luna Reserve fallback.", code: reservePolicy.errorCode ?? "policy_blocked", reasons: reservePolicy.reasons } });
2472
+ return;
2473
+ }
2474
+ }
2475
+ catch {
2476
+ await usageRecorder.record({ outcome: "failure", statusCode: HTTP_STATUS.SERVICE_UNAVAILABLE, errorCode: "runtime_policy_unavailable" });
2477
+ writeJson(res, HTTP_STATUS.SERVICE_UNAVAILABLE, { error: { message: "Runtime policy could not be loaded for Luna Reserve fallback.", code: "runtime_policy_unavailable" } });
2478
+ return;
2479
+ }
2480
+ }
2481
+ attemptedIndexes.clear();
2482
+ accountSkipReasons.clear();
2483
+ rejectedWorkspaces.clear();
2484
+ await rebuildReserveWorkspaceCandidates();
2485
+ state.status.lunaReserveFallbacks = (state.status.lunaReserveFallbacks ?? 0) + 1;
2486
+ mutateRuntimeObservabilitySnapshot((snapshot) => { snapshot.lunaReserveFallbacks = (snapshot.lunaReserveFallbacks ?? 0) + 1; });
2487
+ proxyLog.info("ordinary Luna quota exhausted; retrying with Luna Reserve", {
2488
+ traceId,
2489
+ pinned: isPinned,
2490
+ });
2491
+ capabilityRejectionIsLatest = false;
2492
+ exhaustionReason = "no-account";
2493
+ state.status.retries += 1;
2494
+ continue;
2495
+ }
2332
2496
  exhaustionReason = "rate-limit";
2333
2497
  capabilityRejectionIsLatest = false;
2334
2498
  transientAttempts += 1;
@@ -2544,9 +2708,11 @@ async function handleRequestInner(state, req, res, traceId) {
2544
2708
  // the forecast keeps reporting this working account as unavailable.
2545
2709
  // No-op when no reason is recorded, so the hot path stays write-free.
2546
2710
  const quotaDeferral = state.preemptiveQuotaScheduler.getDeferral(quotaScheduleKey, state.now());
2547
- const nearExhaustionWaitMs = quotaDeferral.defer
2548
- ? quotaDeferral.waitMs
2549
- : 0;
2711
+ const nearExhaustionWaitMs = context.model === LUNA_RESERVE_MODEL
2712
+ ? 0
2713
+ : quotaDeferral.defer
2714
+ ? quotaDeferral.waitMs
2715
+ : 0;
2550
2716
  if (nearExhaustionWaitMs > 0) {
2551
2717
  accountManager.markRateLimitedWithReason(refreshed.account, nearExhaustionWaitMs, context.family, "quota", context.model);
2552
2718
  state.sessionAffinityStore?.forgetSession(context.sessionKey);
@@ -2565,11 +2731,12 @@ async function handleRequestInner(state, req, res, traceId) {
2565
2731
  // evaluateBudgetGuard compares `0 >= limit` for maxTokens/maxCostUsd
2566
2732
  // and those caps never fire — `budget set --cost 50` would allow
2567
2733
  // unlimited spend, with only --requests actually enforced.
2568
- const responseOutcome = new ResponseOutcome(isResponsesRequest && upstream.headers.get("content-type")?.includes("text/event-stream") === true);
2569
2734
  const usageScanner = createUsageStreamScanner({
2570
2735
  contentType: upstream.headers.get("content-type"),
2571
2736
  onEvent: (event) => {
2572
2737
  responseOutcome.observe(event);
2738
+ if (context.model === LUNA_RESERVE_MODEL)
2739
+ return;
2573
2740
  const snapshot = readSubscriptionQuotaEvent(event, state.now());
2574
2741
  if (!snapshot)
2575
2742
  return;
@@ -2578,6 +2745,7 @@ async function handleRequestInner(state, req, res, traceId) {
2578
2745
  state.status.lastStreamQuotaUpdateAt = snapshot.updatedAt;
2579
2746
  },
2580
2747
  });
2748
+ const responseOutcome = new ResponseOutcome(() => isResponsesRequest && usageScanner.isSse());
2581
2749
  let streamErrored = false;
2582
2750
  const forwarded = await forwardStreamingResponse(upstream, res, state.status, () => {
2583
2751
  streamErrored = true;
@@ -51,7 +51,7 @@ export declare const SHORT_CONTEXT_MAX_INPUT_TOKENS = 272000;
51
51
  * `test/usage-pricing-coverage.test.ts` fails if a NEW routable model appears
52
52
  * in neither list.
53
53
  */
54
- export declare const UNPRICED_ROUTABLE_MODELS: readonly ["gpt-daybreak-blue-latest", "gpt-daybreak-red-latest"];
54
+ export declare const UNPRICED_ROUTABLE_MODELS: readonly ["gpt-reserve", "gpt-daybreak-blue-latest", "gpt-daybreak-red-latest"];
55
55
  export declare function getUsageModelPricing(model: string | null | undefined): UsageModelPricing | null;
56
56
  export declare function estimateUsageCostUsd(model: string | null | undefined, tokens: UsageTokenCounts): number | null;
57
57
  export declare function listUsageModelPricing(): Record<string, UsageModelPricing>;
@@ -168,6 +168,10 @@ const MODEL_PRICING = {
168
168
  * in neither list.
169
169
  */
170
170
  export const UNPRICED_ROUTABLE_MODELS = [
171
+ // Luna Reserve is a separately metered subscription allowance. The backend
172
+ // exposes usage percentage, not a public per-token dollar rate; cost budgets
173
+ // therefore remain fail-closed instead of borrowing Luna API pricing.
174
+ "gpt-reserve",
171
175
  // The Daybreak cyber models are sold under a separate controlled-access
172
176
  // agreement with no public per-token rate.
173
177
  "gpt-daybreak-blue-latest",
@@ -22,6 +22,8 @@ export declare function extractUsageTokenCounts(usage: unknown): UsageTokenCount
22
22
  */
23
23
  export declare function extractResponsesUsage(payload: unknown): UsageTokenCounts | null;
24
24
  export interface UsageStreamScanner {
25
+ /** The shared format decision, updated as the bounded body prefix arrives. */
26
+ isSse: () => boolean;
25
27
  /** Feed one forwarded chunk. Never throws. */
26
28
  push: (chunk: Uint8Array) => void;
27
29
  /** Flush any trailing partial data and return the final counts, if any. */
@@ -9,6 +9,8 @@
9
9
  */
10
10
  /** Cap on retained bytes so a hostile or unterminated body cannot grow forever. */
11
11
  const MAX_BUFFERED_BYTES = 1_048_576;
12
+ /** Only inspect this many leading bytes when the media type does not identify SSE. */
13
+ const MAX_SNIFF_BYTES = 4_096;
12
14
  /** Terminal Responses stream events that carry the final `usage` object. */
13
15
  const TERMINAL_EVENT_TYPES = new Set([
14
16
  "response.completed",
@@ -132,7 +134,11 @@ export function extractResponsesUsage(payload) {
132
134
  * bound.
133
135
  */
134
136
  export function createUsageStreamScanner(options) {
135
- const sse = (options.contentType ?? "").toLowerCase().includes("text/event-stream");
137
+ let sse = (options.contentType ?? "").toLowerCase().includes("text/event-stream");
138
+ let sniffing = !sse;
139
+ let sniffedBytes = 0;
140
+ let sniffPrefix = "";
141
+ const sniffDecoder = new TextDecoder("utf-8");
136
142
  const decoder = new TextDecoder("utf-8");
137
143
  let pending = "";
138
144
  let bufferedBytes = 0;
@@ -190,11 +196,29 @@ export function createUsageStreamScanner(options) {
190
196
  }
191
197
  eventData.push(value);
192
198
  };
199
+ const sniffFormat = (chunk) => {
200
+ if (!sniffing)
201
+ return;
202
+ const prefix = chunk.subarray(0, MAX_SNIFF_BYTES - sniffedBytes);
203
+ sniffedBytes += prefix.byteLength;
204
+ sniffPrefix += sniffDecoder.decode(prefix, { stream: true });
205
+ // Match only at the start (after blank lines/BOM), never a data: string
206
+ // inside JSON. A field or comment identifies SSE before a possibly huge
207
+ // event arrives, so streaming never depends on buffering its whole frame.
208
+ sse = /^(?:\r?\n)*(?::|(?:data|event|id|retry):)/.test(sniffPrefix);
209
+ const first = sniffPrefix.trimStart()[0];
210
+ if (sse || first === "{" || first === "[" || sniffedBytes >= MAX_SNIFF_BYTES) {
211
+ sniffing = false;
212
+ sniffPrefix = "";
213
+ }
214
+ };
193
215
  return {
216
+ isSse: () => sse,
194
217
  push: (chunk) => {
195
218
  try {
196
219
  if (overflowed || chunk.byteLength === 0)
197
220
  return;
221
+ sniffFormat(chunk);
198
222
  bufferedBytes += chunk.byteLength;
199
223
  pending += decoder.decode(chunk, { stream: true });
200
224
  if (!sse) {
@@ -18,7 +18,7 @@ export declare function isAbortError(error: unknown): boolean;
18
18
  * Combines two abort signals into one that aborts when the FIRST input does.
19
19
  *
20
20
  * This is a thin null-tolerant wrapper over `AbortSignal.any` — available
21
- * since Node 18.17.0, which the engines floor already requires. Using the
21
+ * on every Node version the engines floor (`>=22.19`) permits. Using the
22
22
  * native composite matters beyond brevity: a manual addEventListener pair
23
23
  * stays registered on BOTH inputs until one fires, so a caller that combines
24
24
  * a long-lived signal (e.g. a proxy-lifetime abort) once per request would
package/dist/lib/utils.js CHANGED
@@ -25,7 +25,7 @@ export function isAbortError(error) {
25
25
  * Combines two abort signals into one that aborts when the FIRST input does.
26
26
  *
27
27
  * This is a thin null-tolerant wrapper over `AbortSignal.any` — available
28
- * since Node 18.17.0, which the engines floor already requires. Using the
28
+ * on every Node version the engines floor (`>=22.19`) permits. Using the
29
29
  * native composite matters beyond brevity: a manual addEventListener pair
30
30
  * stays registered on BOTH inputs until one fires, so a caller that combines
31
31
  * a long-lived signal (e.g. a proxy-lifetime abort) once per request would
@@ -1,2 +1,2 @@
1
- export type PluginInput = any;
2
- export type Plugin = any;
1
+ export type PluginInput = any;
2
+ export type Plugin = any;
@@ -1,2 +1,2 @@
1
- export {};
2
-
1
+ export {};
2
+
@@ -1,42 +1,42 @@
1
- export interface ToolContext {
2
- [key: string]: any;
3
- directory: string;
4
- worktree: string;
5
- ask: (request: any) => Promise<any> | any;
6
- }
7
-
8
- export interface ToolDefinition<TArgs = any, TResult = any> {
9
- description?: string;
10
- args?: Record<string, any>;
11
- parameters?: unknown;
12
- execute: (args: TArgs, context: ToolContext) => Promise<TResult> | TResult;
13
- [key: string]: any;
14
- }
15
-
16
- export interface SchemaNode {
17
- optional: () => SchemaNode;
18
- describe: (_text: string) => SchemaNode;
19
- min: (_value: number) => SchemaNode;
20
- max: (_value: number) => SchemaNode;
21
- default: (_value: unknown) => SchemaNode;
22
- int: () => SchemaNode;
23
- array: (_node?: unknown) => SchemaNode;
24
- [key: string]: any;
25
- }
26
-
27
- export interface ToolFactory {
28
- <TArgs = any, TResult = any>(definition: ToolDefinition<TArgs, TResult>): ToolDefinition<TArgs, TResult>;
29
- schema: {
30
- string: () => SchemaNode;
31
- number: () => SchemaNode;
32
- boolean: () => SchemaNode;
33
- object: (shape: Record<string, unknown>) => SchemaNode;
34
- enum: (values: readonly unknown[]) => SchemaNode;
35
- literal: (value: unknown) => SchemaNode;
36
- array: (node: unknown) => SchemaNode;
37
- any: () => SchemaNode;
38
- unknown: () => SchemaNode;
39
- };
40
- }
41
-
42
- export declare const tool: ToolFactory;
1
+ export interface ToolContext {
2
+ [key: string]: any;
3
+ directory: string;
4
+ worktree: string;
5
+ ask: (request: any) => Promise<any> | any;
6
+ }
7
+
8
+ export interface ToolDefinition<TArgs = any, TResult = any> {
9
+ description?: string;
10
+ args?: Record<string, any>;
11
+ parameters?: unknown;
12
+ execute: (args: TArgs, context: ToolContext) => Promise<TResult> | TResult;
13
+ [key: string]: any;
14
+ }
15
+
16
+ export interface SchemaNode {
17
+ optional: () => SchemaNode;
18
+ describe: (_text: string) => SchemaNode;
19
+ min: (_value: number) => SchemaNode;
20
+ max: (_value: number) => SchemaNode;
21
+ default: (_value: unknown) => SchemaNode;
22
+ int: () => SchemaNode;
23
+ array: (_node?: unknown) => SchemaNode;
24
+ [key: string]: any;
25
+ }
26
+
27
+ export interface ToolFactory {
28
+ <TArgs = any, TResult = any>(definition: ToolDefinition<TArgs, TResult>): ToolDefinition<TArgs, TResult>;
29
+ schema: {
30
+ string: () => SchemaNode;
31
+ number: () => SchemaNode;
32
+ boolean: () => SchemaNode;
33
+ object: (shape: Record<string, unknown>) => SchemaNode;
34
+ enum: (values: readonly unknown[]) => SchemaNode;
35
+ literal: (value: unknown) => SchemaNode;
36
+ array: (node: unknown) => SchemaNode;
37
+ any: () => SchemaNode;
38
+ unknown: () => SchemaNode;
39
+ };
40
+ }
41
+
42
+ export declare const tool: ToolFactory;