codex-multi-auth 2.19.0 → 2.20.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 (107) 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/dashboard-settings-data.js +2 -0
  20. package/dist/lib/codex-manager/formatters/subscription-status.d.ts +2 -2
  21. package/dist/lib/codex-manager/formatters/text-style.js +4 -1
  22. package/dist/lib/codex-manager/settings-hub/dashboard.d.ts +1 -1
  23. package/dist/lib/codex-manager/settings-hub/dashboard.js +4 -0
  24. package/dist/lib/codex-manager/settings-panels.d.ts +1 -0
  25. package/dist/lib/codex-manager/settings-panels.js +1 -0
  26. package/dist/lib/codex-manager/theme-settings-panel.d.ts +2 -1
  27. package/dist/lib/codex-manager/theme-settings-panel.js +39 -0
  28. package/dist/lib/codex-manager.js +2 -0
  29. package/dist/lib/constants.d.ts +2 -0
  30. package/dist/lib/constants.js +2 -0
  31. package/dist/lib/context-budget/model-context-windows.d.ts +1 -1
  32. package/dist/lib/context-budget/model-context-windows.js +4 -0
  33. package/dist/lib/context-budget-response.js +8 -8
  34. package/dist/lib/context-overflow.js +10 -10
  35. package/dist/lib/dashboard-settings.d.ts +2 -0
  36. package/dist/lib/dashboard-settings.js +18 -0
  37. package/dist/lib/luna-reserve.d.ts +26 -0
  38. package/dist/lib/luna-reserve.js +68 -0
  39. package/dist/lib/model-route-policy.d.ts +5 -5
  40. package/dist/lib/oauth-success.html +125 -125
  41. package/dist/lib/policy/runtime-policy.js +14 -0
  42. package/dist/lib/prompts/codex-host-bridge.js +138 -138
  43. package/dist/lib/prompts/codex.js +79 -79
  44. package/dist/lib/quota-readiness.d.ts +2 -2
  45. package/dist/lib/request/helpers/model-map.d.ts +1 -1
  46. package/dist/lib/request/helpers/model-map.js +12 -0
  47. package/dist/lib/request/response-outcome.d.ts +1 -1
  48. package/dist/lib/request/response-outcome.js +4 -1
  49. package/dist/lib/request/url-rewriting.js +3 -1
  50. package/dist/lib/runtime/account-model-catalog.d.ts +5 -5
  51. package/dist/lib/runtime/account-reset-credits.d.ts +2 -2
  52. package/dist/lib/runtime/account-status.js +2 -1
  53. package/dist/lib/runtime/luna-reserve-usage.d.ts +9 -0
  54. package/dist/lib/runtime/luna-reserve-usage.js +68 -0
  55. package/dist/lib/runtime/model-discovery-status.d.ts +11 -7
  56. package/dist/lib/runtime/model-discovery-status.js +13 -5
  57. package/dist/lib/runtime/reset-credit-routing.js +4 -1
  58. package/dist/lib/runtime/reset-credits.d.ts +7 -7
  59. package/dist/lib/runtime/resume-picker.js +1 -1
  60. package/dist/lib/runtime/rotation-server-types.d.ts +2 -0
  61. package/dist/lib/runtime/runtime-observability.d.ts +2 -0
  62. package/dist/lib/runtime/runtime-observability.js +1 -0
  63. package/dist/lib/runtime/workspace-model-scopes.d.ts +3 -3
  64. package/dist/lib/runtime-rotation-proxy.js +200 -32
  65. package/dist/lib/schemas.d.ts +65 -65
  66. package/dist/lib/ui/index.d.ts +1 -1
  67. package/dist/lib/ui/index.js +1 -1
  68. package/dist/lib/ui/runtime.d.ts +3 -1
  69. package/dist/lib/ui/runtime.js +11 -1
  70. package/dist/lib/ui/theme.d.ts +20 -1
  71. package/dist/lib/ui/theme.js +154 -30
  72. package/dist/lib/ui/ui-copy.d.ts +3 -2
  73. package/dist/lib/ui/ui-copy.js +3 -2
  74. package/dist/lib/usage/pricing.d.ts +1 -1
  75. package/dist/lib/usage/pricing.js +4 -0
  76. package/dist/lib/usage/usage-extraction.d.ts +2 -0
  77. package/dist/lib/usage/usage-extraction.js +25 -1
  78. package/dist/lib/utils.d.ts +1 -1
  79. package/dist/lib/utils.js +1 -1
  80. package/node_modules/@codex-ai/plugin/dist/index.d.ts +2 -2
  81. package/node_modules/@codex-ai/plugin/dist/index.js +2 -2
  82. package/node_modules/@codex-ai/plugin/dist/tool.d.ts +42 -42
  83. package/node_modules/@codex-ai/plugin/dist/tool.js +29 -29
  84. package/node_modules/@codex-ai/plugin/package.json +9 -9
  85. package/node_modules/@codex-ai/sdk/dist/index.d.ts +4 -4
  86. package/node_modules/@codex-ai/sdk/dist/index.js +2 -2
  87. package/node_modules/@codex-ai/sdk/package.json +8 -8
  88. package/package.json +207 -206
  89. package/scripts/codex-app-launcher.js +653 -653
  90. package/scripts/codex-app-router.js +374 -373
  91. package/scripts/codex-bin-resolver.js +332 -332
  92. package/scripts/codex-multi-auth.js +65 -65
  93. package/scripts/codex-routing.js +62 -62
  94. package/scripts/codex.js +7006 -7005
  95. package/scripts/install-codex-auth-utils.js +213 -213
  96. package/scripts/mcodex.js +367 -367
  97. package/scripts/postinstall.js +114 -114
  98. package/scripts/preuninstall.js +265 -265
  99. package/skills/codex-auth-setup/SKILL.md +73 -73
  100. package/vendor/codex-ai-plugin/dist/index.d.ts +2 -2
  101. package/vendor/codex-ai-plugin/dist/index.js +2 -2
  102. package/vendor/codex-ai-plugin/dist/tool.d.ts +42 -42
  103. package/vendor/codex-ai-plugin/dist/tool.js +29 -29
  104. package/vendor/codex-ai-plugin/package.json +9 -9
  105. package/vendor/codex-ai-sdk/dist/index.d.ts +4 -4
  106. package/vendor/codex-ai-sdk/dist/index.js +2 -2
  107. 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;