codex-multi-auth 2.19.1 → 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 (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
@@ -1,15 +1,15 @@
1
- {
2
- "$schema": "./schema/config.schema.json",
3
- "plugin": ["codex-multi-auth"],
4
-
5
- "provider": {
6
- "openai": {
7
- "options": {
8
- "store": false
9
- }
10
- }
11
- },
12
- "model": "openai/gpt-5.6-sol"
13
- }
14
-
15
-
1
+ {
2
+ "$schema": "./schema/config.schema.json",
3
+ "plugin": ["codex-multi-auth"],
4
+
5
+ "provider": {
6
+ "openai": {
7
+ "options": {
8
+ "store": false
9
+ }
10
+ }
11
+ },
12
+ "model": "openai/gpt-5.6-sol"
13
+ }
14
+
15
+
package/dist/index.js CHANGED
@@ -1043,7 +1043,7 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
1043
1043
  }
1044
1044
  let response;
1045
1045
  const fetchStart = performance.now();
1046
- // Merge user AbortSignal with timeout (Node 18 compatible - no AbortSignal.any)
1046
+ // Merge user AbortSignal with timeout without AbortSignal.any (keeps listeners bounded)
1047
1047
  const fetchController = new AbortController();
1048
1048
  const requestTimeoutMs = fetchTimeoutMs;
1049
1049
  let requestTimedOut = false;
@@ -2,6 +2,7 @@
2
2
  * Rate limiting utilities for account management.
3
3
  * Extracted from accounts.ts to reduce module size and improve cohesion.
4
4
  */
5
+ import { LUNA_RESERVE_MODEL } from "../constants.js";
5
6
  import { nowMs } from "../utils.js";
6
7
  export function parseRateLimitReason(code) {
7
8
  if (!code)
@@ -61,6 +62,10 @@ export function isRateLimitedForFamily(entity, family, model) {
61
62
  if (isRateLimitedForQuotaKey(entity, modelKey)) {
62
63
  return true;
63
64
  }
65
+ // Luna Reserve is independently metered. Ordinary Luna/Codex family
66
+ // exhaustion must not suppress a separately available reserve allowance.
67
+ if (model === LUNA_RESERVE_MODEL)
68
+ return false;
64
69
  }
65
70
  const baseKey = getQuotaKey(family);
66
71
  return isRateLimitedForQuotaKey(entity, baseKey);
@@ -9,7 +9,7 @@ import { MODEL_FAMILIES } from "./request/helpers/model-map.js";
9
9
  import { getHealthTracker, getTokenTracker, resetTrackers, selectHybridAccount, } from "./rotation.js";
10
10
  import { withRoutingMutex, } from "./routing-mutex.js";
11
11
  import { nowMs } from "./utils.js";
12
- import { ERROR_MESSAGES, HTTP_STATUS, MAX_RATE_LIMIT_DELAY_MS } from "./constants.js";
12
+ import { ERROR_MESSAGES, HTTP_STATUS, LUNA_RESERVE_MODEL, MAX_RATE_LIMIT_DELAY_MS } from "./constants.js";
13
13
  import { CodexAuthError } from "./errors.js";
14
14
  import { loadCodexCliState, } from "./codex-cli/state.js";
15
15
  import { syncAccountStorageFromCodexCli } from "./codex-cli/sync.js";
@@ -1062,13 +1062,14 @@ export class AccountManager {
1062
1062
  : undefined;
1063
1063
  const newLimits = { ...carried?.limits };
1064
1064
  const baseKey = getQuotaKey(family);
1065
- if (!model || reason === "quota" || reason === "unknown") {
1065
+ const independentReserve = model === LUNA_RESERVE_MODEL;
1066
+ if (!independentReserve && (!model || reason === "quota" || reason === "unknown")) {
1066
1067
  const currentResetAt = account.rateLimitResetTimes[baseKey] ?? 0;
1067
1068
  account.rateLimitResetTimes[baseKey] = Math.max(currentResetAt, resetAt);
1068
1069
  newLimits[baseKey] = Math.max(newLimits[baseKey] ?? 0, resetAt);
1069
1070
  }
1070
1071
  if (model &&
1071
- (reason === "tokens" || reason === "concurrent" || reason === "unknown")) {
1072
+ (independentReserve || reason === "tokens" || reason === "concurrent" || reason === "unknown")) {
1072
1073
  const modelKey = getQuotaKey(family, model);
1073
1074
  const currentResetAt = account.rateLimitResetTimes[modelKey] ?? 0;
1074
1075
  account.rateLimitResetTimes[modelKey] = Math.max(currentResetAt, resetAt);
@@ -1550,7 +1551,7 @@ export class AccountManager {
1550
1551
  for (const account of enabledAccounts) {
1551
1552
  const perAccountWaitTimes = [];
1552
1553
  const baseResetAt = account.rateLimitResetTimes[baseKey];
1553
- if (typeof baseResetAt === "number") {
1554
+ if (model !== LUNA_RESERVE_MODEL && typeof baseResetAt === "number") {
1554
1555
  perAccountWaitTimes.push(Math.max(0, baseResetAt - now));
1555
1556
  }
1556
1557
  if (modelKey) {
@@ -51,8 +51,8 @@ export async function fetchAuthorizedAccounts(accessToken, options = {}) {
51
51
  return null;
52
52
  const doFetch = options.fetch ?? globalThis.fetch;
53
53
  const timeout = AbortSignal.timeout(ACCOUNTS_CHECK_TIMEOUT_MS);
54
- // combineSignals wraps AbortSignal.any (available since Node 18.17, the
55
- // engines floor) with null-tolerance; the native composite also detaches
54
+ // combineSignals wraps AbortSignal.any (available on every Node the
55
+ // engines floor, `>=22.19`, permits) with null-tolerance; the native composite also detaches
56
56
  // from a long-lived caller signal once this request completes.
57
57
  const signal = options.signal
58
58
  ? combineSignals(options.signal, timeout)
@@ -0,0 +1,22 @@
1
+ interface ResolvedCodexBin {
2
+ path: string;
3
+ launchWithNode: boolean;
4
+ }
5
+ export interface CodexCliVersionDeps {
6
+ resolveBin?: () => Promise<ResolvedCodexBin | null>;
7
+ execFile?: (file: string, args: readonly string[], options: {
8
+ timeout: number;
9
+ }) => Promise<{
10
+ stdout: string;
11
+ }>;
12
+ }
13
+ export declare const CLIENT_VERSION_PATTERN: RegExp;
14
+ /** Extracts `0.160.1` from `codex --version` output such as `codex-cli 0.160.1`. */
15
+ export declare function parseCodexCliVersion(output: string): string | null;
16
+ /**
17
+ * Returns the installed Codex CLI version, or null when no binary resolves or
18
+ * the probe fails. Detection failure must never break a check: the caller just
19
+ * sends no `client_version`, exactly as before.
20
+ */
21
+ export declare function detectInstalledCodexCliVersion(deps?: CodexCliVersionDeps): Promise<string | null>;
22
+ export {};
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Detects the installed Codex CLI version for the `client_version` parameter
3
+ * the upstream models catalog requires (issue #733). The catalog filters by
4
+ * `minimal_client_version`, so the honest value is whatever `codex --version`
5
+ * reports on this host — never a hardcoded string.
6
+ */
7
+ import { execFile } from "node:child_process";
8
+ import { existsSync } from "node:fs";
9
+ import { fileURLToPath, pathToFileURL } from "node:url";
10
+ // scripts/codex-bin-resolver.js is plain shipped JS, so it loads by URL:
11
+ // dist/lib/codex-cli/ resolves ../../../scripts, the source tree ../../scripts.
12
+ const RESOLVER_CANDIDATES = [
13
+ fileURLToPath(new URL("../../../scripts/codex-bin-resolver.js", import.meta.url)),
14
+ fileURLToPath(new URL("../../scripts/codex-bin-resolver.js", import.meta.url)),
15
+ ];
16
+ async function loadBinResolver() {
17
+ for (const candidate of RESOLVER_CANDIDATES) {
18
+ if (!existsSync(candidate))
19
+ continue;
20
+ const resolverModule = (await import(pathToFileURL(candidate).href).catch(() => null));
21
+ if (typeof resolverModule?.resolveRealCodexBin === "function") {
22
+ return resolverModule;
23
+ }
24
+ }
25
+ return null;
26
+ }
27
+ async function defaultResolveBin() {
28
+ const resolverModule = await loadBinResolver();
29
+ const resolve = resolverModule?.resolveRealCodexBin;
30
+ if (!resolve)
31
+ return null;
32
+ const bin = resolve();
33
+ // PATH discovery can hand back npm's extensionless `codex` shim on Windows,
34
+ // which no spawn API can exec. The resolver knows the sibling package entry
35
+ // the shim points at; launch that through node instead.
36
+ if (bin &&
37
+ process.platform === "win32" &&
38
+ !bin.launchWithNode &&
39
+ resolverModule.isWindowsShimPath?.(bin.path)) {
40
+ const entry = resolverModule.resolveWindowsShimPackageEntry?.(bin.path);
41
+ if (entry)
42
+ return { path: entry, launchWithNode: true };
43
+ }
44
+ return bin;
45
+ }
46
+ const defaultExecFile = (file, args, options) => new Promise((resolve, reject) => {
47
+ execFile(file, [...args], { ...options, encoding: "utf8", windowsHide: true, maxBuffer: 1024 * 1024 }, (error, stdout) => {
48
+ if (error)
49
+ reject(error);
50
+ else
51
+ resolve({ stdout: String(stdout) });
52
+ });
53
+ });
54
+ export const CLIENT_VERSION_PATTERN = /^[0-9A-Za-z.+_-]{1,80}$/;
55
+ /** Extracts `0.160.1` from `codex --version` output such as `codex-cli 0.160.1`. */
56
+ export function parseCodexCliVersion(output) {
57
+ const match = /(?:^|\s)v?(\d+\.\d+\.\d+[-0-9A-Za-z.+_]*)/.exec(output);
58
+ const version = match?.[1];
59
+ return version && CLIENT_VERSION_PATTERN.test(version) ? version : null;
60
+ }
61
+ /**
62
+ * Returns the installed Codex CLI version, or null when no binary resolves or
63
+ * the probe fails. Detection failure must never break a check: the caller just
64
+ * sends no `client_version`, exactly as before.
65
+ */
66
+ export async function detectInstalledCodexCliVersion(deps = {}) {
67
+ try {
68
+ const bin = await (deps.resolveBin ?? defaultResolveBin)();
69
+ if (!bin || typeof bin.path !== "string" || bin.path.length === 0)
70
+ return null;
71
+ const exec = deps.execFile ?? defaultExecFile;
72
+ const { stdout } = await exec(bin.launchWithNode ? process.execPath : bin.path, bin.launchWithNode ? [bin.path, "--version"] : ["--version"], { timeout: 10_000 });
73
+ return parseCodexCliVersion(stdout);
74
+ }
75
+ catch {
76
+ return null;
77
+ }
78
+ }
@@ -1,11 +1,13 @@
1
1
  import { type ModelFamily } from "../../prompts/codex.js";
2
2
  import type { QuotaCacheData } from "../../quota-cache.js";
3
3
  import type { AccountStorageV3 } from "../../storage.js";
4
+ import type { LunaReserveSnapshot } from "../../luna-reserve.js";
4
5
  export interface LimitsCommandDeps {
5
6
  setStoragePath: (path: string | null) => void;
6
7
  loadAccounts: () => Promise<AccountStorageV3 | null>;
7
8
  loadQuotaCache: () => Promise<QuotaCacheData>;
8
9
  refreshQuotaCache: (storage: AccountStorageV3, cache: QuotaCacheData, maxAgeMs: number) => Promise<QuotaCacheData>;
10
+ refreshLunaReserveUsage?: (storage: AccountStorageV3) => Promise<Record<number, LunaReserveSnapshot>>;
9
11
  resolveActiveIndex: (storage: AccountStorageV3, family?: ModelFamily) => number;
10
12
  getNow?: () => number;
11
13
  logInfo?: (message: string) => void;
@@ -46,6 +46,32 @@ function publicQuotaEntry(entry) {
46
46
  secondary: publicWindow(entry.secondary),
47
47
  };
48
48
  }
49
+ /** Convert one Reserve window to the stable public JSON shape. */
50
+ function publicReserveWindow(window) {
51
+ return window
52
+ ? {
53
+ usedPercent: window.usedPercent,
54
+ remainingPercent: window.remainingPercent,
55
+ windowMinutes: window.windowMinutes,
56
+ resetAtMs: window.resetAtMs,
57
+ }
58
+ : null;
59
+ }
60
+ /** Convert a Reserve snapshot to public JSON while preserving explicit unknowns. */
61
+ function publicLunaReserve(snapshot) {
62
+ if (!snapshot)
63
+ return null;
64
+ return {
65
+ observedAt: snapshot.observedAt,
66
+ offered: snapshot.offered,
67
+ available: snapshot.available,
68
+ limitId: snapshot.limitId,
69
+ limitName: snapshot.limitName,
70
+ normalModelSlug: snapshot.normalModelSlug,
71
+ primary: publicReserveWindow(snapshot.primary),
72
+ secondary: publicReserveWindow(snapshot.secondary),
73
+ };
74
+ }
49
75
  /**
50
76
  * Emit configured accounts joined to safe cached quota records.
51
77
  *
@@ -86,8 +112,12 @@ export async function runLimitsCommand(args, deps) {
86
112
  return 0;
87
113
  }
88
114
  let cache = await deps.loadQuotaCache();
115
+ let lunaReserveByAccount = {};
89
116
  if (parsed.options.refresh) {
90
117
  cache = await deps.refreshQuotaCache(storage, cache, LIMITS_REFRESH_MAX_AGE_MS);
118
+ if (deps.refreshLunaReserveUsage) {
119
+ lunaReserveByAccount = await deps.refreshLunaReserveUsage(storage);
120
+ }
91
121
  }
92
122
  const generatedAt = deps.getNow?.() ?? Date.now();
93
123
  const selection = resolveSelection(storage, deps.resolveActiveIndex);
@@ -102,6 +132,7 @@ export async function runLimitsCommand(args, deps) {
102
132
  enabled: account.enabled !== false,
103
133
  current: index === selection.routedIndex,
104
134
  quota: quota ? publicQuotaEntry(quota) : null,
135
+ lunaReserve: publicLunaReserve(lunaReserveByAccount[index]),
105
136
  };
106
137
  });
107
138
  logInfo(JSON.stringify({
@@ -279,7 +279,7 @@ export async function runStatusCommand(deps) {
279
279
  runtimeSnapshot.serverBurstCooldownUntil > now
280
280
  ? formatWaitTime(runtimeSnapshot.serverBurstCooldownUntil - now)
281
281
  : null;
282
- logInfo(`Runtime: responses=${runtimeSnapshot.responsesRequests}, refresh=${runtimeSnapshot.authRefreshRequests}, probes=${runtimeSnapshot.diagnosticProbeRequests}, budgetExhaustions=${runtimeMetrics.requestAttemptBudgetExhaustions}`);
282
+ logInfo(`Runtime: responses=${runtimeSnapshot.responsesRequests}, refresh=${runtimeSnapshot.authRefreshRequests}, probes=${runtimeSnapshot.diagnosticProbeRequests}, reserveFallbacks=${runtimeSnapshot.lunaReserveFallbacks ?? 0}, budgetExhaustions=${runtimeMetrics.requestAttemptBudgetExhaustions}`);
283
283
  const lastRuntimeAccount = formatRuntimeLastAccount(runtimeSnapshot);
284
284
  if (lastRuntimeAccount) {
285
285
  logInfo(paint(`Last runtime account: ${lastRuntimeAccount}`, "accent"));
@@ -46,6 +46,7 @@ import { runUnpinCommand } from "./codex-manager/commands/unpin.js";
46
46
  import { runWorkspaceCommand } from "./codex-manager/commands/workspace.js";
47
47
  import { runUsageCommand } from "./codex-manager/commands/usage.js";
48
48
  import { refreshQuotaCacheForMenu } from "./codex-manager/login-menu-data.js";
49
+ import { refreshLunaReserveUsage } from "./runtime/luna-reserve-usage.js";
49
50
  import { printUsage } from "./codex-manager/help.js";
50
51
  import { availabilityTone, formatBackupSavedAt, formatCompactQuotaSnapshot, formatRateLimitEntry, formatResultSummary, normalizeFailureDetail, riskTone, styleAccountDetailText, stylePromptText, styleQuotaSummary, } from "./codex-manager/formatters/index.js";
51
52
  import { applyUiThemeFromDashboardSettings } from "./codex-manager/settings-hub.js";
@@ -307,6 +308,7 @@ const CLI_COMMAND_HANDLERS = new Map([
307
308
  loadAccounts,
308
309
  loadQuotaCache,
309
310
  refreshQuotaCache: refreshQuotaCacheForMenu,
311
+ refreshLunaReserveUsage,
310
312
  resolveActiveIndex,
311
313
  }),
312
314
  ],
@@ -6,6 +6,8 @@
6
6
  export declare const PLUGIN_NAME = "codex-multi-auth";
7
7
  /** Base URL for ChatGPT backend API */
8
8
  export declare const CODEX_BASE_URL = "https://chatgpt.com/backend-api";
9
+ /** Backend model used for the separately metered Luna Reserve allowance. */
10
+ export declare const LUNA_RESERVE_MODEL: "gpt-reserve";
9
11
  /** Dummy API key used for OpenAI SDK (actual auth via OAuth) */
10
12
  export declare const DUMMY_API_KEY = "chatgpt-oauth";
11
13
  /** Provider ID for UI display - shows under "OpenAI" in auth dropdown */
@@ -6,6 +6,8 @@
6
6
  export const PLUGIN_NAME = "codex-multi-auth";
7
7
  /** Base URL for ChatGPT backend API */
8
8
  export const CODEX_BASE_URL = "https://chatgpt.com/backend-api";
9
+ /** Backend model used for the separately metered Luna Reserve allowance. */
10
+ export const LUNA_RESERVE_MODEL = "gpt-reserve";
9
11
  /** Dummy API key used for OpenAI SDK (actual auth via OAuth) */
10
12
  export const DUMMY_API_KEY = "chatgpt-oauth";
11
13
  /** Provider ID for UI display - shows under "OpenAI" in auth dropdown */
@@ -6,7 +6,7 @@
6
6
  * behavior (see the v2.5.0 release notes) closely enough to launch that no
7
7
  * estimate here has been reviewed for it yet.
8
8
  */
9
- export declare const UNESTIMATED_ROUTABLE_MODELS: readonly ["gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna", "gpt-6-astra", "gpt-6-sol", "gpt-6-luna", "gpt-6.1-sol", "gpt-daybreak-blue-latest", "gpt-daybreak-red-latest"];
9
+ export declare const UNESTIMATED_ROUTABLE_MODELS: readonly ["gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna", "gpt-6-astra", "gpt-6-sol", "gpt-6-luna", "gpt-reserve", "gpt-6.1-sol", "gpt-daybreak-blue-latest", "gpt-daybreak-red-latest"];
10
10
  export interface EffectiveContextWindow {
11
11
  tokens: number;
12
12
  source: "override" | "estimate";
@@ -53,6 +53,10 @@ export const UNESTIMATED_ROUTABLE_MODELS = [
53
53
  // upstream catalog as Astra, so the same reasoning keeps them unestimated.
54
54
  "gpt-6-sol",
55
55
  "gpt-6-luna",
56
+ // Luna Reserve is a quota alias of Luna, but the backend does not publish
57
+ // a distinct transport context ceiling for the alias. Keep it fail-closed
58
+ // alongside Luna rather than copying an unverified API/catalog number.
59
+ "gpt-reserve",
56
60
  "gpt-6.1-sol",
57
61
  "gpt-daybreak-blue-latest",
58
62
  "gpt-daybreak-red-latest",
@@ -17,14 +17,14 @@ function buildPauseMessage(advisory) {
17
17
  const windowNote = advisory.windowSource === "override"
18
18
  ? "your configured window size"
19
19
  : "an estimated window size (unverified — set `contextBudgetGuardModelWindowOverrides` if you know the real limit)";
20
- return `[Plugin Notice] Context budget guard paused this session at ${formatPercent(advisory.percent)}% of ${windowNote} for \`${advisory.model}\`, before hitting the model's actual limit.
21
-
22
- Please use one of these commands to reduce context size:
23
-
24
- • **/compact** - Compress conversation history (recommended)
25
- • **/clear** - Start fresh with empty context
26
- • **/undo** - Remove recent messages
27
-
20
+ return `[Plugin Notice] Context budget guard paused this session at ${formatPercent(advisory.percent)}% of ${windowNote} for \`${advisory.model}\`, before hitting the model's actual limit.
21
+
22
+ Please use one of these commands to reduce context size:
23
+
24
+ • **/compact** - Compress conversation history (recommended)
25
+ • **/clear** - Start fresh with empty context
26
+ • **/undo** - Remove recent messages
27
+
28
28
  Then retry your request. This pause clears itself automatically once your context usage drops back below the threshold.`;
29
29
  }
30
30
  /**
@@ -33,16 +33,16 @@ export function isContextOverflowError(status, bodyText) {
33
33
  /**
34
34
  * The message shown to users when context overflow occurs
35
35
  */
36
- const CONTEXT_OVERFLOW_MESSAGE = `[Plugin Notice] Context is too long for this model.
37
-
38
- Please use one of these commands to reduce context size:
39
-
40
- • **/compact** - Compress conversation history (recommended)
41
- • **/clear** - Start fresh with empty context
42
- • **/undo** - Remove recent messages
43
-
44
- Then retry your request.
45
-
36
+ const CONTEXT_OVERFLOW_MESSAGE = `[Plugin Notice] Context is too long for this model.
37
+
38
+ Please use one of these commands to reduce context size:
39
+
40
+ • **/compact** - Compress conversation history (recommended)
41
+ • **/clear** - Start fresh with empty context
42
+ • **/undo** - Remove recent messages
43
+
44
+ Then retry your request.
45
+
46
46
  Alternatively, you can switch to a model with a larger context window.`;
47
47
  /**
48
48
  * Creates a synthetic SSE response for context overflow errors.
@@ -0,0 +1,26 @@
1
+ import { LUNA_RESERVE_MODEL } from "./constants.js";
2
+ export interface LunaReserveWindow {
3
+ usedPercent: number | null;
4
+ remainingPercent: number | null;
5
+ windowMinutes: number | null;
6
+ resetAtMs: number | null;
7
+ }
8
+ export interface LunaReserveSnapshot {
9
+ observedAt: number;
10
+ offered: boolean;
11
+ available: boolean | null;
12
+ limitId: string | null;
13
+ limitName: typeof LUNA_RESERVE_MODEL | null;
14
+ normalModelSlug: string | null;
15
+ primary: LunaReserveWindow | null;
16
+ secondary: LunaReserveWindow | null;
17
+ }
18
+ /**
19
+ * Parse the official Codex app-server `account/rateLimits/read` response for
20
+ * the separately metered Luna Reserve bucket.
21
+ *
22
+ * Absence is intentionally represented as `offered:false`, never as 0% left:
23
+ * the backend may omit Reserve when an account is not currently eligible or
24
+ * when the experiment is not active for that account.
25
+ */
26
+ export declare function parseLunaReserveRateLimits(value: unknown, observedAt?: number): LunaReserveSnapshot;
@@ -0,0 +1,68 @@
1
+ import { LUNA_RESERVE_MODEL } from "./constants.js";
2
+ import { isRecord } from "./utils.js";
3
+ /** Return a finite numeric value or null for malformed backend data. */
4
+ function finiteNumber(value) {
5
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
6
+ }
7
+ /** Normalize backend percentages into the inclusive 0-100 range. */
8
+ function clampPercent(value) {
9
+ const number = finiteNumber(value);
10
+ return number === null ? null : Math.max(0, Math.min(100, number));
11
+ }
12
+ /** Parse one Reserve usage window without inventing missing values. */
13
+ function parseWindow(value) {
14
+ if (!isRecord(value))
15
+ return null;
16
+ const usedPercent = clampPercent(value.usedPercent);
17
+ const duration = finiteNumber(value.windowDurationMins);
18
+ const resetSeconds = finiteNumber(value.resetsAt);
19
+ return {
20
+ usedPercent,
21
+ remainingPercent: usedPercent === null ? null : Math.max(0, 100 - usedPercent),
22
+ windowMinutes: duration === null || duration < 0 ? null : duration,
23
+ resetAtMs: resetSeconds === null || resetSeconds <= 0 ? null : resetSeconds * 1000,
24
+ };
25
+ }
26
+ /**
27
+ * Parse the official Codex app-server `account/rateLimits/read` response for
28
+ * the separately metered Luna Reserve bucket.
29
+ *
30
+ * Absence is intentionally represented as `offered:false`, never as 0% left:
31
+ * the backend may omit Reserve when an account is not currently eligible or
32
+ * when the experiment is not active for that account.
33
+ */
34
+ export function parseLunaReserveRateLimits(value, observedAt = Date.now()) {
35
+ const absent = {
36
+ observedAt,
37
+ offered: false,
38
+ available: null,
39
+ limitId: null,
40
+ limitName: null,
41
+ normalModelSlug: null,
42
+ primary: null,
43
+ secondary: null,
44
+ };
45
+ if (!isRecord(value) || !isRecord(value.rateLimitsByLimitId))
46
+ return absent;
47
+ for (const [limitId, candidate] of Object.entries(value.rateLimitsByLimitId)) {
48
+ if (!isRecord(candidate) || candidate.limitName !== LUNA_RESERVE_MODEL)
49
+ continue;
50
+ const primary = parseWindow(candidate.primary);
51
+ const secondary = parseWindow(candidate.secondary);
52
+ const knownWindows = [primary, secondary].filter((window) => window !== null && window.usedPercent !== null);
53
+ const exhausted = knownWindows.some((window) => (window.usedPercent ?? 0) >= 100);
54
+ return {
55
+ observedAt,
56
+ offered: true,
57
+ available: knownWindows.length === 0 ? null : !exhausted,
58
+ limitId: limitId || (typeof candidate.limitId === "string" ? candidate.limitId : null),
59
+ limitName: LUNA_RESERVE_MODEL,
60
+ normalModelSlug: typeof candidate.normalModelSlug === "string" && candidate.normalModelSlug.trim()
61
+ ? candidate.normalModelSlug.trim()
62
+ : null,
63
+ primary,
64
+ secondary,
65
+ };
66
+ }
67
+ return absent;
68
+ }