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
@@ -381,83 +381,83 @@ export function prewarmCodexInstructions(models = []) {
381
381
  /**
382
382
  * Tool remapping instructions for host runtime tools
383
383
  */
384
- export const TOOL_REMAP_MESSAGE = `<user_instructions priority="0">
385
- <environment_override priority="0">
386
- YOU ARE IN A DIFFERENT ENVIRONMENT. These instructions override ALL previous tool references.
387
- </environment_override>
388
-
389
- <tool_replacements priority="0">
390
- <critical_rule priority="0">
391
- apply_patch/applyPatch are Codex names, but host tool names vary by version.
392
- - Inspect the actual tool list before editing.
393
- - If edit exists: use edit for precise in-place string replacements and hashline edits.
394
- - If edit is absent and apply_patch exists: use apply_patch for those precise/hashline edits in this plugin.
395
- - For diff-style or multi-line structural edits: use patch if available, otherwise use apply_patch.
396
- - In this plugin, edit/apply_patch support hashline refs (lineRef, endLineRef, operation, content)
397
- </critical_rule>
398
-
399
- <critical_rule priority="0">
400
- UPDATE_PLAN DOES NOT EXIST -> USE "todowrite" INSTEAD
401
- - NEVER use: update_plan, updatePlan
402
- - ALWAYS use: todowrite for ALL task/plan operations
403
- - Use todoread to read current plan
404
- - Before plan operations: Verify you're using "todowrite", NOT "update_plan"
405
- </critical_rule>
406
- </tool_replacements>
407
-
408
- <available_tools priority="0">
409
- File Operations:
410
- - write - Create new files (if available)
411
- - edit - Modify existing files with string replacement (version-dependent)
412
- - oldString must be literal text from the current file; never pass unresolved placeholders like \${TARGET_SNIPPET}
413
- - apply_patch - May be the edit/patch tool name in newer host builds (version-dependent)
414
- - hashline_read - Read lines with hashline refs (L<line>#<hash>) for deterministic edits
415
- - patch - Apply diff patches (version-dependent)
416
- - read - Read file contents
417
-
418
- Search/Discovery:
419
- - grep - Search file contents
420
- - glob - Find files by pattern
421
- - list - List directories (if available)
422
-
423
- Execution:
424
- - bash - Run shell commands
425
-
426
- Network:
427
- - webfetch - Fetch web content
428
-
429
- Task Management:
430
- - todowrite - Manage tasks/plans (REPLACES update_plan)
431
- - todoread - Read current plan
432
- </available_tools>
433
-
434
- <substitution_rules priority="0">
435
- Base instruction says: You MUST use instead:
436
- apply_patch -> patch (preferred if available), otherwise edit/apply_patch based on actual tool list
437
- update_plan -> todowrite
438
- read_plan -> todoread
439
- absolute paths -> relative paths
440
- </substitution_rules>
441
-
442
- <verification_checklist priority="0">
443
- Before file/plan modifications:
444
- 1. Am I using the actual available edit tool name (edit, patch, or apply_patch)?
445
- 2. Am I using "todowrite" NOT "update_plan"?
446
- 3. Is this tool in the approved list above?
447
- 4. Am I using relative paths?
448
-
449
- If ANY answer is NO -> STOP and correct before proceeding.
450
- </verification_checklist>
451
-
452
- <hashline_workflow priority="0">
453
- When hashline_read is available:
454
- 1. call hashline_read on the file
455
- 2. edit/apply_patch using lineRef/endLineRef + operation + content
456
- 3. use oldString/newString only as fallback, and only with literal file text (no template placeholders)
457
- </hashline_workflow>
458
-
459
- <safety_rules priority="0">
460
- - Never run destructive git commands (\`git reset --hard\`, \`git checkout --\`) unless explicitly requested by the user.
461
- - Never call \`request_user_input\` unless collaboration mode is explicitly Plan mode.
462
- </safety_rules>
384
+ export const TOOL_REMAP_MESSAGE = `<user_instructions priority="0">
385
+ <environment_override priority="0">
386
+ YOU ARE IN A DIFFERENT ENVIRONMENT. These instructions override ALL previous tool references.
387
+ </environment_override>
388
+
389
+ <tool_replacements priority="0">
390
+ <critical_rule priority="0">
391
+ apply_patch/applyPatch are Codex names, but host tool names vary by version.
392
+ - Inspect the actual tool list before editing.
393
+ - If edit exists: use edit for precise in-place string replacements and hashline edits.
394
+ - If edit is absent and apply_patch exists: use apply_patch for those precise/hashline edits in this plugin.
395
+ - For diff-style or multi-line structural edits: use patch if available, otherwise use apply_patch.
396
+ - In this plugin, edit/apply_patch support hashline refs (lineRef, endLineRef, operation, content)
397
+ </critical_rule>
398
+
399
+ <critical_rule priority="0">
400
+ UPDATE_PLAN DOES NOT EXIST -> USE "todowrite" INSTEAD
401
+ - NEVER use: update_plan, updatePlan
402
+ - ALWAYS use: todowrite for ALL task/plan operations
403
+ - Use todoread to read current plan
404
+ - Before plan operations: Verify you're using "todowrite", NOT "update_plan"
405
+ </critical_rule>
406
+ </tool_replacements>
407
+
408
+ <available_tools priority="0">
409
+ File Operations:
410
+ - write - Create new files (if available)
411
+ - edit - Modify existing files with string replacement (version-dependent)
412
+ - oldString must be literal text from the current file; never pass unresolved placeholders like \${TARGET_SNIPPET}
413
+ - apply_patch - May be the edit/patch tool name in newer host builds (version-dependent)
414
+ - hashline_read - Read lines with hashline refs (L<line>#<hash>) for deterministic edits
415
+ - patch - Apply diff patches (version-dependent)
416
+ - read - Read file contents
417
+
418
+ Search/Discovery:
419
+ - grep - Search file contents
420
+ - glob - Find files by pattern
421
+ - list - List directories (if available)
422
+
423
+ Execution:
424
+ - bash - Run shell commands
425
+
426
+ Network:
427
+ - webfetch - Fetch web content
428
+
429
+ Task Management:
430
+ - todowrite - Manage tasks/plans (REPLACES update_plan)
431
+ - todoread - Read current plan
432
+ </available_tools>
433
+
434
+ <substitution_rules priority="0">
435
+ Base instruction says: You MUST use instead:
436
+ apply_patch -> patch (preferred if available), otherwise edit/apply_patch based on actual tool list
437
+ update_plan -> todowrite
438
+ read_plan -> todoread
439
+ absolute paths -> relative paths
440
+ </substitution_rules>
441
+
442
+ <verification_checklist priority="0">
443
+ Before file/plan modifications:
444
+ 1. Am I using the actual available edit tool name (edit, patch, or apply_patch)?
445
+ 2. Am I using "todowrite" NOT "update_plan"?
446
+ 3. Is this tool in the approved list above?
447
+ 4. Am I using relative paths?
448
+
449
+ If ANY answer is NO -> STOP and correct before proceeding.
450
+ </verification_checklist>
451
+
452
+ <hashline_workflow priority="0">
453
+ When hashline_read is available:
454
+ 1. call hashline_read on the file
455
+ 2. edit/apply_patch using lineRef/endLineRef + operation + content
456
+ 3. use oldString/newString only as fallback, and only with literal file text (no template placeholders)
457
+ </hashline_workflow>
458
+
459
+ <safety_rules priority="0">
460
+ - Never run destructive git commands (\`git reset --hard\`, \`git checkout --\`) unless explicitly requested by the user.
461
+ - Never call \`request_user_input\` unless collaboration mode is explicitly Plan mode.
462
+ </safety_rules>
463
463
  </user_instructions>`;
@@ -6,7 +6,7 @@
6
6
  * tool-surface metadata so routing logic stays consistent across the request
7
7
  * transformer, prompt selection, and CLI diagnostics.
8
8
  */
9
- import type { ModelReasoningEffort, WireReasoningEffort } from "../../constants.js";
9
+ import { type ModelReasoningEffort, type WireReasoningEffort } from "../../constants.js";
10
10
  export type { ModelReasoningEffort, WireReasoningEffort };
11
11
  export type PromptModelFamily = "gpt-5-codex" | "codex-max" | "codex" | "gpt-5.2" | "gpt-5.1";
12
12
  /**
@@ -6,6 +6,10 @@
6
6
  * tool-surface metadata so routing logic stays consistent across the request
7
7
  * transformer, prompt selection, and CLI diagnostics.
8
8
  */
9
+ // The effort union lives in the leaf constants module so the base types layer
10
+ // (`lib/types.ts`) can depend on it without importing this file, which would
11
+ // close a cycle through `lib/schemas.ts`. Re-exported here for existing callers.
12
+ import { LUNA_RESERVE_MODEL, } from "../../constants.js";
9
13
  /**
10
14
  * All supported model families
11
15
  * Used for per-family account rotation and rate limit tracking
@@ -301,6 +305,13 @@ export const MODEL_PROFILES = {
301
305
  supportedReasoningEfforts: GPT_6_LUNA_EFFORTS,
302
306
  capabilities: TOOL_CAPABILITIES.full,
303
307
  },
308
+ [LUNA_RESERVE_MODEL]: {
309
+ normalizedModel: LUNA_RESERVE_MODEL,
310
+ promptFamily: "gpt-5.2",
311
+ defaultReasoningEffort: "medium",
312
+ supportedReasoningEfforts: GPT_6_LUNA_EFFORTS,
313
+ capabilities: TOOL_CAPABILITIES.full,
314
+ },
304
315
  [DAYBREAK_BLUE_MODEL]: {
305
316
  normalizedModel: DAYBREAK_BLUE_MODEL,
306
317
  promptFamily: "gpt-5.2",
@@ -392,6 +403,7 @@ function addGpt6Aliases() {
392
403
  // user's model generation without asking.
393
404
  addEffortAliases(GPT_6_SOL_MODEL, GPT_6_SOL_MODEL, GPT_6_SOL_EFFORTS);
394
405
  addEffortAliases(GPT_6_LUNA_MODEL, GPT_6_LUNA_MODEL, GPT_6_LUNA_EFFORTS);
406
+ addEffortAliases(LUNA_RESERVE_MODEL, LUNA_RESERVE_MODEL, GPT_6_LUNA_EFFORTS);
395
407
  addEffortAliases(GPT_6_1_SOL_MODEL, GPT_6_1_SOL_MODEL, GPT_6_1_SOL_EFFORTS);
396
408
  addEffortAliases(GPT_6_1_FLAGSHIP_ALIAS, GPT_6_1_SOL_MODEL, GPT_6_1_SOL_EFFORTS);
397
409
  }
@@ -13,7 +13,7 @@ export declare class ResponseOutcome {
13
13
  param?: string;
14
14
  };
15
15
  } | undefined;
16
- constructor(requireTerminal: boolean);
16
+ constructor(requireTerminal: boolean | (() => boolean));
17
17
  observe(value: unknown): void;
18
18
  /**
19
19
  * `response.incomplete` (e.g. max_output_tokens) is a delivered response, not an
@@ -38,7 +38,10 @@ export class ResponseOutcome {
38
38
  * type survives only as the usage annotation in `errorCode`.
39
39
  */
40
40
  finish() {
41
- const missingTerminal = this.requireTerminal && !this.terminal;
41
+ const requireTerminal = typeof this.requireTerminal === "function"
42
+ ? this.requireTerminal()
43
+ : this.requireTerminal;
44
+ const missingTerminal = requireTerminal && !this.terminal;
42
45
  const success = !missingTerminal && (!this.terminal || this.terminal === "completed" || this.terminal === "incomplete");
43
46
  return {
44
47
  success,
@@ -1,4 +1,5 @@
1
1
  import { getQuotaKey } from "../accounts/rate-limits.js";
2
+ import { LUNA_RESERVE_MODEL } from "../constants.js";
2
3
  export function resolveActiveIndex(storage, family = "codex") {
3
4
  const total = storage.accounts.length;
4
5
  if (total === 0)
@@ -58,7 +59,7 @@ function laterBound(a, b) {
58
59
  export function getAccountRecoveryBoundsForFamily(account, now, family, model) {
59
60
  const times = account.rateLimitResetTimes;
60
61
  const rateLimitAtMs = times
61
- ? laterBound(activeBound(times[getQuotaKey(family)], now), model ? activeBound(times[getQuotaKey(family, model)], now) : null)
62
+ ? laterBound(model === LUNA_RESERVE_MODEL ? null : activeBound(times[getQuotaKey(family)], now), model ? activeBound(times[getQuotaKey(family, model)], now) : null)
62
63
  : null;
63
64
  return {
64
65
  rateLimitAtMs,
@@ -0,0 +1,9 @@
1
+ import { type LunaReserveSnapshot } from "../luna-reserve.js";
2
+ import type { AccountStorageV3 } from "../storage.js";
3
+ export type LunaReserveUsageByAccount = Record<number, LunaReserveSnapshot>;
4
+ /**
5
+ * Read Luna Reserve usage through the same isolated native app-server bridge
6
+ * used for reset credits. Failures are per-account: one stale/revoked account
7
+ * never prevents other rows from reporting their Reserve percentage.
8
+ */
9
+ export declare function refreshLunaReserveUsage(storage: AccountStorageV3, now?: () => number): Promise<LunaReserveUsageByAccount>;
@@ -0,0 +1,68 @@
1
+ import { AccountManager } from "../accounts.js";
2
+ import { LUNA_RESERVE_MODEL } from "../constants.js";
3
+ import { parseLunaReserveRateLimits, } from "../luna-reserve.js";
4
+ import { codexCliAccountIdFor } from "../auth/token-utils.js";
5
+ import { isRecord } from "../utils.js";
6
+ import { workspaceModelScopes } from "./workspace-model-scopes.js";
7
+ import { nativeRateLimitsRpc } from "./native-rate-limits.js";
8
+ import { ensureFreshAccessToken } from "./rotation-token-refresh.js";
9
+ /**
10
+ * Read Luna Reserve usage through the same isolated native app-server bridge
11
+ * used for reset credits. Failures are per-account: one stale/revoked account
12
+ * never prevents other rows from reporting their Reserve percentage.
13
+ */
14
+ export async function refreshLunaReserveUsage(storage, now = Date.now) {
15
+ const manager = new AccountManager(undefined, storage);
16
+ const result = {};
17
+ try {
18
+ for (let index = 0; index < storage.accounts.length; index += 1) {
19
+ const stored = storage.accounts[index];
20
+ const account = manager.getAccountByIndex(index);
21
+ if (!stored || !account || account.enabled === false)
22
+ continue;
23
+ const scopes = workspaceModelScopes(account);
24
+ const targetScope = account.workspaces?.length
25
+ ? scopes.find((scope) => scope.selected && scope.routable)
26
+ : scopes.find((scope) => scope.bound && scope.routable);
27
+ if (!targetScope)
28
+ continue;
29
+ try {
30
+ const fresh = await ensureFreshAccessToken({
31
+ accountManager: manager,
32
+ account,
33
+ family: "gpt-5.2",
34
+ model: LUNA_RESERVE_MODEL,
35
+ now: now(),
36
+ tokenRefreshSkewMs: 60_000,
37
+ tokenInvalidationCooldownMs: 300_000,
38
+ });
39
+ if (!fresh.ok)
40
+ continue;
41
+ const auth = {
42
+ accessToken: fresh.accessToken,
43
+ accountId: targetScope.accountId,
44
+ expiresAt: fresh.account.expires ?? 0,
45
+ codexCliMirror: account.codexCliMirror,
46
+ };
47
+ const sentAccountId = codexCliAccountIdFor(auth, fresh.accessToken) ?? targetScope.accountId;
48
+ const reply = await nativeRateLimitsRpc(auth, "account/rateLimits/read", {
49
+ supportsLunaReserve: true,
50
+ excludeResetCreditDetails: true,
51
+ });
52
+ if (isRecord(reply) &&
53
+ typeof reply.accountId === "string" &&
54
+ reply.accountId !== sentAccountId) {
55
+ continue;
56
+ }
57
+ result[index] = parseLunaReserveRateLimits(reply, now());
58
+ }
59
+ catch {
60
+ // Best-effort usage reporting; callers still emit normal quota.
61
+ }
62
+ }
63
+ return result;
64
+ }
65
+ finally {
66
+ await manager.flushPendingSave();
67
+ }
68
+ }
@@ -1,6 +1,6 @@
1
1
  import { z } from "zod";
2
- import { type AccountStorageV3 } from "../storage.js";
3
- import { type ApiRouteCredential, updateApiModelDiscovery } from "../api-route-store.js";
2
+ import { type AccountStorageV3, loadAccounts } from "../storage.js";
3
+ import { type ApiRouteCredential, loadApiRoutes, updateApiModelDiscovery } from "../api-route-store.js";
4
4
  declare const schema: z.ZodObject<{
5
5
  apiConfigurationUnavailable: z.ZodOptional<z.ZodBoolean>;
6
6
  version: z.ZodLiteral<1>;
@@ -90,9 +90,13 @@ export declare function refreshAndPrintModelInventory(log: (message: string) =>
90
90
  }>;
91
91
  fetchImpl?: typeof fetch;
92
92
  loadInventory?: typeof loadModelInventory;
93
+ loadAccounts?: typeof loadAccounts;
94
+ loadApiRoutes?: typeof loadApiRoutes;
93
95
  now?: () => number;
94
96
  /** Only an explicit `check capabilities` bypasses the 15-minute probe cache. */
95
97
  forceProbes?: boolean;
98
+ /** Installed `codex --version` probe, used when the inventory stores no clientVersion. */
99
+ detectClientVersion?: () => Promise<string | null>;
96
100
  }): Promise<boolean>;
97
101
  /** Compare stable credential/workspace scopes, keeping the last successful baseline through outages. */
98
102
  export declare function withInventoryChanges(current: ModelInventory, previous: ModelInventory | null): ModelInventory;
@@ -15,6 +15,7 @@ import { loadApiRoutes, updateApiModelDiscovery, } from "../api-route-store.js";
15
15
  import { ApiModelRuntime, boundedJson } from "./api-model-runtime.js";
16
16
  import { AccountModelCatalog } from "./account-model-catalog.js";
17
17
  import { ensureFreshAccessToken } from "./rotation-token-refresh.js";
18
+ import { CLIENT_VERSION_PATTERN, detectInstalledCodexCliVersion, } from "../codex-cli/version.js";
18
19
  import { createCodexHeaders } from "../request/headers.js";
19
20
  import { CODEX_BASE_URL } from "../constants.js";
20
21
  import { getCodexMultiAuthDir } from "../runtime-paths.js";
@@ -103,7 +104,7 @@ export async function discoverModelInventory(storage, routes, options = {}) {
103
104
  throw Error("Identity unavailable");
104
105
  const url = new URL(`${CODEX_BASE_URL}/codex/models`);
105
106
  const version = options.clientVersion;
106
- if (version && /^[0-9A-Za-z.+_-]{1,80}$/.test(version))
107
+ if (version && CLIENT_VERSION_PATTERN.test(version))
107
108
  url.searchParams.set("client_version", version);
108
109
  const response = await fetcher(url, {
109
110
  headers: createCodexHeaders({
@@ -250,6 +251,11 @@ export async function refreshAndPrintModelInventory(log, deps = {}) {
250
251
  try {
251
252
  const load = deps.loadInventory ?? loadModelInventory;
252
253
  const previous = await load();
254
+ // The catalog requires client_version (issue #733): the stored inventory
255
+ // version wins, otherwise probe the installed `codex --version` so a fresh
256
+ // inventory seeds one and saves it for the next check.
257
+ const detectClientVersion = async () => (await (deps.detectClientVersion ?? detectInstalledCodexCliVersion)().catch(() => null)) ?? undefined;
258
+ const clientVersion = previous?.clientVersion;
253
259
  const binding = await (deps.getBinding ?? getAppBindStatus)();
254
260
  if (binding.running && binding.state?.nativeOpenai) {
255
261
  const started = (deps.now ?? Date.now)();
@@ -266,8 +272,9 @@ export async function refreshAndPrintModelInventory(log, deps = {}) {
266
272
  url.searchParams.set("refresh_capabilities", "1");
267
273
  if (deps.forceProbes)
268
274
  url.searchParams.set("force_probes", "1");
269
- if (previous?.clientVersion)
270
- url.searchParams.set("client_version", previous.clientVersion);
275
+ const proxyClientVersion = clientVersion ?? (await detectClientVersion());
276
+ if (proxyClientVersion)
277
+ url.searchParams.set("client_version", proxyClientVersion);
271
278
  const clientApiKey = binding.state.clientApiKey;
272
279
  const current = await withCheckProgress("Refreshing workspace catalogs and API/ZDR probes", async () => {
273
280
  const response = await (deps.fetchImpl ?? fetch)(url, {
@@ -294,8 +301,9 @@ export async function refreshAndPrintModelInventory(log, deps = {}) {
294
301
  }
295
302
  const value = await withCheckProgress("Discovering workspace models and API/ZDR capabilities", async () => {
296
303
  let apiConfigurationUnavailable = false;
297
- const value = await discoverModelInventory(await loadAccounts(), await loadApiRoutes().catch(() => { apiConfigurationUnavailable = true; log("API configuration unavailable; checking subscription models only."); return []; }), {
298
- clientVersion: previous?.clientVersion,
304
+ const value = await discoverModelInventory(await (deps.loadAccounts ?? loadAccounts)(), await (deps.loadApiRoutes ?? loadApiRoutes)().catch(() => { apiConfigurationUnavailable = true; log("API configuration unavailable; checking subscription models only."); return []; }), {
305
+ clientVersion: clientVersion ?? (await detectClientVersion()),
306
+ fetchImpl: deps.fetchImpl,
299
307
  updateApiDiscovery: updateApiModelDiscovery,
300
308
  forceProbes: deps.forceProbes === true,
301
309
  persistProbes: true,
@@ -1,9 +1,10 @@
1
1
  import { extractAccountId } from '../accounts.js';
2
+ import { LUNA_RESERVE_MODEL } from '../constants.js';
2
3
  import { parseModelRoute } from '../model-route-policy.js';
3
4
  import { subscriptionQuotaPreference } from './subscription-quota-order.js';
4
5
  import { resetSnapshotQuota } from './reset-credits.js';
5
6
  export async function recoverResetQuota(options) {
6
- if (!options.native || options.pinned || parseModelRoute(options.model).kind !== 'oauth')
7
+ if (options.model === LUNA_RESERVE_MODEL || !options.native || options.pinned || parseModelRoute(options.model).kind !== 'oauth')
7
8
  return false;
8
9
  const family = options.family ?? 'codex';
9
10
  const candidates = [...options.scopes].flatMap(([index, scopes]) => { const account = options.manager.getAccountByIndex(index); return account && !account.authInvalidatedAt ? scopes.map(scope => ({ account, scope })) : []; });
@@ -50,6 +51,8 @@ export async function recoverResetQuota(options) {
50
51
  }
51
52
  /** Apply a confirmed earned reset once per workspace/model observation. */
52
53
  export function applyConfirmedReset(options) {
54
+ if (options.model === LUNA_RESERVE_MODEL)
55
+ return false;
53
56
  const s = options.snapshot;
54
57
  if (!s || options.lastRedemptionAt === undefined || s.updatedAt < options.lastRedemptionAt || s.ordinaryUsageAllowed !== true || s.updatedAt <= (options.previous?.updatedAt ?? 0) || options.now - s.updatedAt > 60000 || s.updatedAt > options.now)
55
58
  return false;
@@ -26,6 +26,8 @@ export interface RuntimeRotationProxyStatus {
26
26
  totalRequests: number;
27
27
  upstreamRequests: number;
28
28
  retries: number;
29
+ /** Number of ordinary GPT-6 Luna requests retried through Luna Reserve. */
30
+ lunaReserveFallbacks?: number;
29
31
  rotations: number;
30
32
  streamsStarted: number;
31
33
  lastError: string | null;
@@ -35,6 +35,8 @@ export interface RuntimeObservabilitySnapshot {
35
35
  responsesRequests: number;
36
36
  authRefreshRequests: number;
37
37
  diagnosticProbeRequests: number;
38
+ /** Number of ordinary Luna requests that switched to the separately metered Reserve model. */
39
+ lunaReserveFallbacks: number;
38
40
  poolExhaustionCooldownUntil: number | null;
39
41
  serverBurstCooldownUntil: number | null;
40
42
  lastAccountIndex?: number | null;
@@ -27,6 +27,7 @@ function createDefaultSnapshot() {
27
27
  responsesRequests: 0,
28
28
  authRefreshRequests: 0,
29
29
  diagnosticProbeRequests: 0,
30
+ lunaReserveFallbacks: 0,
30
31
  poolExhaustionCooldownUntil: null,
31
32
  serverBurstCooldownUntil: null,
32
33
  lastAccountIndex: null,