@bitkyc08/opencodex 2.80.0 → 2.81.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 (227) hide show
  1. package/README.md +3 -3
  2. package/gui/dist/assets/App-7yhoys8a.js +54 -0
  3. package/gui/dist/assets/Tray-EwAOsP0S.js +1 -0
  4. package/gui/dist/assets/index-RsoaAxXY.js +94 -0
  5. package/gui/dist/assets/quota-summary-D2lN8tDm.js +1 -0
  6. package/gui/dist/index.html +1 -1
  7. package/gui/dist/provider-icons/claude-green.svg +1 -0
  8. package/package.json +3 -2
  9. package/src/adapters/anthropic-model-contract.ts +8 -2
  10. package/src/adapters/anthropic.ts +7 -2
  11. package/src/adapters/codebuddy/mcp-server.ts +2 -25
  12. package/src/adapters/codebuddy/tool-bridge.ts +1 -7
  13. package/src/adapters/coding-agent/mcp-server.ts +33 -0
  14. package/src/adapters/coding-agent/protocol.ts +122 -17
  15. package/src/adapters/coding-agent/tool-catalog.ts +75 -0
  16. package/src/adapters/coding-agent/turn.ts +101 -44
  17. package/src/adapters/cursor/catalog.ts +8 -0
  18. package/src/adapters/cursor/effort-map.ts +2 -0
  19. package/src/adapters/devin/live-models.ts +2 -0
  20. package/src/adapters/kiro-retry.ts +9 -4
  21. package/src/adapters/openai-chat/messages.ts +7 -6
  22. package/src/adapters/physical-send.ts +8 -5
  23. package/src/adapters/qoder/adapter.ts +41 -6
  24. package/src/adapters/qoder/scaffold-guard.ts +2 -3
  25. package/src/adapters/zed.ts +16 -1
  26. package/src/claude/inbound-cache-stabilize.ts +4 -9
  27. package/src/claude/inbound.ts +1 -0
  28. package/src/claude/intercept/model-bindings.ts +1 -0
  29. package/src/claude/intercept/picker-listener.ts +71 -4
  30. package/src/cli/account-anthropic-threshold.ts +14 -5
  31. package/src/cli/account-api.ts +3 -1
  32. package/src/cli/account-extended.ts +13 -9
  33. package/src/cli/account-policy-dto.ts +1 -1
  34. package/src/cli/account-policy.ts +13 -4
  35. package/src/cli/account.ts +3 -3
  36. package/src/cli/capabilities-accounts.ts +2 -2
  37. package/src/cli/capabilities-agents-routing.ts +24 -0
  38. package/src/cli/capabilities-integrations.ts +16 -0
  39. package/src/cli/codex-shim-autorestore.ts +3 -0
  40. package/src/cli/dispatch.ts +10 -1
  41. package/src/cli/help.ts +4 -1
  42. package/src/cli/index.ts +11 -5
  43. package/src/cli/integration-plan-dto.ts +1 -0
  44. package/src/cli/integration-preview.ts +8 -1
  45. package/src/cli/integrations.ts +32 -3
  46. package/src/cli/message-args.ts +34 -0
  47. package/src/cli/message-command.ts +43 -0
  48. package/src/cli/message-runtime.ts +6 -0
  49. package/src/cli/observe.ts +3 -1
  50. package/src/cli/provider.ts +29 -6
  51. package/src/cli/registry.ts +24 -2
  52. package/src/clients/config-export/commandcode.ts +131 -0
  53. package/src/clients/config-export/contracts.ts +12 -0
  54. package/src/clients/config-export.ts +64 -1
  55. package/src/codex/auth-api/account-list.ts +11 -3
  56. package/src/codex/auth-api/main-account-probe.ts +25 -8
  57. package/src/codex/auth-api/pool-mode-gate.ts +33 -9
  58. package/src/codex/auth-api/pool-quota-probe.ts +15 -1
  59. package/src/codex/auth-api/routes.ts +1 -0
  60. package/src/codex/catalog/auto-review.ts +13 -6
  61. package/src/codex/catalog/build-entries.ts +15 -7
  62. package/src/codex/catalog/control-plane.ts +45 -0
  63. package/src/codex/catalog/effort.ts +11 -1
  64. package/src/codex/catalog/gather-capture.ts +5 -3
  65. package/src/codex/catalog/parsing.ts +2 -1
  66. package/src/codex/catalog/provider-models.ts +15 -9
  67. package/src/codex/control-plane-models.ts +6 -0
  68. package/src/combos/jev-decision-contract.ts +25 -2
  69. package/src/combos/jev.ts +3 -2
  70. package/src/combos/types.ts +1 -1
  71. package/src/config/diagnostics.ts +27 -16
  72. package/src/config/load-degrade.ts +34 -0
  73. package/src/config/schema/anthropic-account-pool.ts +143 -0
  74. package/src/config/schema/config-schema.ts +7 -1
  75. package/src/config/schema/leaf-validators.ts +4 -0
  76. package/src/config.ts +2 -0
  77. package/src/generated/compatibility-version.json +320 -184
  78. package/src/generated/model-metadata.ts +5 -4
  79. package/src/images/loop.ts +2 -1
  80. package/src/integrations/catalog-refresh.ts +1 -1
  81. package/src/integrations/mutation-plan.ts +7 -0
  82. package/src/integrations/registry.ts +8 -0
  83. package/src/integrations/state.ts +9 -2
  84. package/src/integrations/writer.ts +8 -2
  85. package/src/lib/lab-live-route-production.ts +9 -1
  86. package/src/lib/request-execution-budget.ts +236 -58
  87. package/src/lib/spend-pool-alias-validation.ts +79 -0
  88. package/src/lib/spend-pool-continuity.ts +34 -0
  89. package/src/lib/spend-reservation-ledger.ts +412 -50
  90. package/src/lib/state-store-registrations.ts +20 -3
  91. package/src/lib/upstream-retry.ts +17 -5
  92. package/src/lib/workflow-budget.ts +53 -11
  93. package/src/messaging/budget.ts +45 -0
  94. package/src/messaging/discovery.ts +57 -0
  95. package/src/messaging/envelope.ts +49 -0
  96. package/src/messaging/input.ts +37 -0
  97. package/src/messaging/rpc.ts +196 -0
  98. package/src/messaging/send.ts +58 -0
  99. package/src/messaging/socket.ts +72 -0
  100. package/src/messaging/types.ts +29 -0
  101. package/src/oauth/anthropic-account-refusal.ts +51 -17
  102. package/src/oauth/anthropic-account-threshold.ts +8 -2
  103. package/src/oauth/anthropic-continuity.ts +15 -1
  104. package/src/oauth/anthropic-model-quota.ts +120 -48
  105. package/src/oauth/anthropic-model-routes.ts +11 -3
  106. package/src/oauth/anthropic-oauth-definitions.ts +98 -0
  107. package/src/oauth/anthropic-pool-config.ts +30 -0
  108. package/src/oauth/anthropic-rate-limit-policy.ts +64 -9
  109. package/src/oauth/anthropic-routing.ts +1202 -866
  110. package/src/oauth/anthropic-send-ownership.ts +41 -0
  111. package/src/oauth/anthropic.ts +4 -1
  112. package/src/oauth/generic-account-failover.ts +3 -1
  113. package/src/oauth/health.ts +8 -6
  114. package/src/oauth/index.ts +90 -173
  115. package/src/oauth/kiro.ts +22 -9
  116. package/src/oauth/login-cli.ts +2 -0
  117. package/src/oauth/model-discovery-auth.ts +43 -0
  118. package/src/oauth/pool-kernel.ts +11 -5
  119. package/src/oauth/pool-settings-capability.ts +5 -3
  120. package/src/oauth/store-anthropic-instance.ts +87 -0
  121. package/src/oauth/store.ts +67 -9
  122. package/src/oauth/token-guardian.ts +2 -0
  123. package/src/oauth/types.ts +2 -0
  124. package/src/protocols/plan-snapshot.ts +5 -0
  125. package/src/protocols/settings.ts +20 -5
  126. package/src/providers/anthropic-instance-id.ts +47 -0
  127. package/src/providers/anthropic-instance.ts +48 -0
  128. package/src/providers/anthropic-reset-grant-ledger.ts +11 -1
  129. package/src/providers/derive.ts +2 -0
  130. package/src/providers/kiro-models.ts +3 -0
  131. package/src/providers/label.ts +1 -1
  132. package/src/providers/model-rename-fields.ts +2 -0
  133. package/src/providers/quota/account-cache.ts +37 -11
  134. package/src/providers/quota/anthropic-account-quota.ts +93 -0
  135. package/src/providers/quota/anthropic-cooldown-recovery.ts +222 -111
  136. package/src/providers/quota/report-cache.ts +24 -1
  137. package/src/providers/quota/vendor-probes-oauth.ts +46 -14
  138. package/src/providers/quota.ts +22 -32
  139. package/src/providers/registry/entries-core.ts +35 -30
  140. package/src/providers/registry/model-ids.ts +1 -0
  141. package/src/providers/registry/model-seeds.ts +7 -4
  142. package/src/providers/registry/types.ts +3 -0
  143. package/src/providers/registry.ts +3 -3
  144. package/src/responses/spill-store.ts +5 -1
  145. package/src/responses/state/metrics.ts +87 -0
  146. package/src/responses/state/snapshot-codec.ts +6 -0
  147. package/src/responses/state/spill-queue.ts +1 -0
  148. package/src/responses/state/unforced-store-false.ts +55 -0
  149. package/src/responses/state.ts +50 -66
  150. package/src/router.ts +28 -4
  151. package/src/routing/compatibility/assemble.ts +1 -1
  152. package/src/routing/quota.ts +7 -3
  153. package/src/server/auth-cors.ts +5 -0
  154. package/src/server/chat-completions.ts +6 -1
  155. package/src/server/chat-native.ts +45 -25
  156. package/src/server/claude-messages.ts +16 -2
  157. package/src/server/gui-static.ts +38 -4
  158. package/src/server/index/spend-ledger-lifecycle.ts +10 -4
  159. package/src/server/index.ts +4 -2
  160. package/src/server/inference/context.ts +1 -1
  161. package/src/server/lifecycle.ts +1 -1
  162. package/src/server/management/agent-settings-routes.ts +30 -6
  163. package/src/server/management/anthropic-account-threshold.ts +12 -9
  164. package/src/server/management/anthropic-pool-settings.ts +72 -0
  165. package/src/server/management/anthropic-reset-grant-routes.ts +98 -39
  166. package/src/server/management/config-routes.ts +27 -7
  167. package/src/server/management/decision-routes.ts +2 -1
  168. package/src/server/management/integration-routes.ts +25 -2
  169. package/src/server/management/oauth-account-routes.ts +79 -98
  170. package/src/server/management/provider-instance-ownership.ts +44 -0
  171. package/src/server/management/provider-routes.ts +21 -28
  172. package/src/server/management/route-registry.ts +3 -1
  173. package/src/server/management/routing-profile-routes.ts +4 -4
  174. package/src/server/management/system-restart.ts +44 -14
  175. package/src/server/management/system-routes.ts +24 -2
  176. package/src/server/management/vision-sidecar-options.ts +9 -6
  177. package/src/server/management/web-search-sidecar-options.ts +51 -2
  178. package/src/server/management-auth.ts +6 -0
  179. package/src/server/messages-native-eligibility.ts +3 -2
  180. package/src/server/messages-native-oauth.ts +86 -41
  181. package/src/server/messages-native-selector.ts +39 -0
  182. package/src/server/messages-native.ts +84 -25
  183. package/src/server/request-log.ts +5 -0
  184. package/src/server/responses/adapter-continuation.ts +29 -17
  185. package/src/server/responses/adapter-dispatch.ts +47 -17
  186. package/src/server/responses/antigravity-validation-refusal.ts +20 -6
  187. package/src/server/responses/compact.ts +25 -8
  188. package/src/server/responses/compaction-recovery.ts +1 -1
  189. package/src/server/responses/core-combo-native.ts +4 -1
  190. package/src/server/responses/core-combo.ts +16 -11
  191. package/src/server/responses/core-normalize.ts +1 -0
  192. package/src/server/responses/core-options.ts +3 -1
  193. package/src/server/responses/core-replay.ts +5 -0
  194. package/src/server/responses/passthrough-delivery.ts +7 -3
  195. package/src/server/responses/passthrough-dispatch.ts +21 -8
  196. package/src/server/responses/request-prepare.ts +37 -5
  197. package/src/server/responses/request-send-budget.ts +74 -37
  198. package/src/server/responses/request-spend.ts +325 -33
  199. package/src/server/responses/request-transport.ts +132 -52
  200. package/src/server/responses/run-turn-execution.ts +11 -3
  201. package/src/server/responses/sidecar-execution.ts +96 -14
  202. package/src/server/responses-undeclared-tool-guard.ts +5 -1
  203. package/src/server/workflow-refusal.ts +30 -0
  204. package/src/service/ownership-mutation-lease.d.mts +1 -1
  205. package/src/service/ownership-mutation-lease.mjs +5 -7
  206. package/src/sidecar/anthropic-binding.ts +79 -0
  207. package/src/sidecar/auth.ts +76 -2
  208. package/src/types/anthropic-account-pool.ts +33 -0
  209. package/src/types/config.ts +12 -28
  210. package/src/types/provider.ts +5 -0
  211. package/src/types/tools.ts +7 -0
  212. package/src/types.ts +4 -0
  213. package/src/usage/cost.ts +3 -2
  214. package/src/usage/expected-prices.ts +66 -14
  215. package/src/vision/anthropic-describe.ts +16 -3
  216. package/src/vision/index.ts +3 -0
  217. package/src/vision/plan.ts +19 -6
  218. package/src/web-search/alpha-search.ts +7 -1
  219. package/src/web-search/anthropic-executor.ts +16 -4
  220. package/src/web-search/index.ts +7 -3
  221. package/src/web-search/loop.ts +27 -5
  222. package/src/web-search/passthrough-bridge.ts +4 -1
  223. package/src/web-search/sidecar-providers.ts +4 -3
  224. package/gui/dist/assets/App-MbDVfUeC.js +0 -54
  225. package/gui/dist/assets/Tray-CzkxK82A.js +0 -1
  226. package/gui/dist/assets/index-UJt6npXk.js +0 -94
  227. package/gui/dist/assets/quota-summary-DU3sD6Z-.js +0 -1
@@ -146,6 +146,11 @@ export async function fetchMainAccountInfo(forceRefresh = false, config?: OcxCon
146
146
  return (await fetchMainAccountInfoSnapshot(forceRefresh, config)).info;
147
147
  }
148
148
 
149
+ export interface MainAccountInfoProbeOptions {
150
+ /** Metadata-only renewal never sets or clears the traffic reauth quarantine. */
151
+ passive?: boolean;
152
+ }
153
+
149
154
  export const EMPTY_MAIN_ACCOUNT_INFO: MainAccountInfo = { email: null, plan: null, quota: null };
150
155
 
151
156
  export async function retryMainAccountInfoIfIdentityChanged(
@@ -154,12 +159,13 @@ export async function retryMainAccountInfoIfIdentityChanged(
154
159
  nativeMainLease: AdmissionLease,
155
160
  explicitRefresh: boolean,
156
161
  paced: boolean,
162
+ options: MainAccountInfoProbeOptions = {},
157
163
  ): Promise<MainAccountInfoFetchResult | null> {
158
164
  const currentAccountId = getMainChatgptAccountId();
159
165
  if (currentAccountId === null || currentAccountId === requestAccountId) return null;
160
166
  reconcileMainCodexAccountRuntimeState();
161
167
  return retriesRemaining > 0
162
- ? fetchMainAccountInfoWhileOwned(true, retriesRemaining - 1, nativeMainLease, explicitRefresh, false, paced)
168
+ ? fetchMainAccountInfoWhileOwned(true, retriesRemaining - 1, nativeMainLease, explicitRefresh, false, paced, options)
163
169
  : { info: EMPTY_MAIN_ACCOUNT_INFO, credentialChecked: true, hasCredential: true };
164
170
  }
165
171
 
@@ -171,6 +177,7 @@ export async function fetchMainAccountInfoAttempt(
171
177
  explicitRefresh: boolean = forceRefresh,
172
178
  postReset = false,
173
179
  config?: OcxConfig,
180
+ options: MainAccountInfoProbeOptions = {},
174
181
  ): Promise<MainAccountInfoFetchResult> {
175
182
  const nativeMainLease = existingNativeMainLease ?? tryAcquireNativeMainProfileClaim();
176
183
  if (!nativeMainLease) {
@@ -184,7 +191,7 @@ export async function fetchMainAccountInfoAttempt(
184
191
  try {
185
192
  const paced = sharedMainQuotaPacingEnabled(config ?? loadConfig());
186
193
  const operation = async () => ({
187
- ...await fetchMainAccountInfoWhileOwned(forceRefresh, retriesRemaining, nativeMainLease, explicitRefresh, postReset, paced),
194
+ ...await fetchMainAccountInfoWhileOwned(forceRefresh, retriesRemaining, nativeMainLease, explicitRefresh, postReset, paced, options),
188
195
  identityGeneration: captureMainAccountIdentityGeneration(),
189
196
  });
190
197
  if (nativeMainSharedClaimHeld) return await operation();
@@ -225,6 +232,7 @@ export async function fetchMainAccountInfoWhileOwned(
225
232
  /** Reset-credit consume needs a fresh post-spend observation, not an earlier read. */
226
233
  postReset = false,
227
234
  paced = true,
235
+ options: MainAccountInfoProbeOptions = {},
228
236
  ): Promise<MainAccountInfoFetchResult> {
229
237
  const writerGeneration = captureConfigGeneration();
230
238
  reconcileMainCodexAccountRuntimeState();
@@ -287,7 +295,16 @@ export async function fetchMainAccountInfoWhileOwned(
287
295
  const current = credentialIsCurrent() && writerGeneration === captureConfigGeneration();
288
296
  if (!current) return { info: getMainAccountInfoCache() ?? EMPTY_MAIN_ACCOUNT_INFO,
289
297
  credentialChecked: true, hasCredential: true };
290
- if (explicitRefresh && (admission.result.quotaRefresh?.status === "ok"
298
+ // Single-flight shares evidence, but each caller retains its own auth behavior.
299
+ if (!options.passive && admission.result.terminalAuthFailure) {
300
+ const diagnosticStillLive = admission.result.quotaRefreshGeneration !== undefined
301
+ && isMainAccountIdentityGenerationLive(admission.result.quotaRefreshGeneration);
302
+ clearMainAccountInfoCache();
303
+ markAccountNeedsReauth(MAIN_CODEX_ACCOUNT_ID, writerGeneration);
304
+ return { ...admission.result, ...(diagnosticStillLive
305
+ ? { quotaRefreshGeneration: captureMainAccountIdentityGeneration() } : {}) };
306
+ }
307
+ if (!options.passive && explicitRefresh && (admission.result.quotaRefresh?.status === "ok"
291
308
  || admission.result.quotaRefresh?.status === "not_reported")) clearAccountNeedsReauth(MAIN_CODEX_ACCOUNT_ID);
292
309
  return admission.result;
293
310
  }
@@ -297,13 +314,13 @@ export async function fetchMainAccountInfoWhileOwned(
297
314
  if (!resp.ok) {
298
315
  const authFailure = await classifyMainAuthResponse(resp, isMainAccountTokenVerifiablyLive());
299
316
  const terminalAuthFailure = authFailure.terminal;
300
- const retried = await retryMainAccountInfoIfIdentityChanged(requestAccountId, retriesRemaining, nativeMainLease, explicitRefresh, paced);
317
+ const retried = await retryMainAccountInfoIfIdentityChanged(requestAccountId, retriesRemaining, nativeMainLease, explicitRefresh, paced, options);
301
318
  if (retried) return retried;
302
319
  if (!isQuotaDispatchCurrent(dispatchSequence) || !credentialIsCurrent()) {
303
320
  return { info: getMainAccountInfoCache() ?? EMPTY_MAIN_ACCOUNT_INFO,
304
321
  credentialChecked: true, hasCredential: true };
305
322
  }
306
- if (terminalAuthFailure) {
323
+ if (terminalAuthFailure && !options.passive) {
307
324
  // Account for this attempt's own synchronous invalidation, never prior external drift.
308
325
  const diagnosticStillLive = isMainAccountIdentityGenerationLive(quotaRefreshGeneration);
309
326
  clearMainAccountInfoCache();
@@ -324,7 +341,7 @@ export async function fetchMainAccountInfoWhileOwned(
324
341
  quotaPhase = "body";
325
342
  const data = (await resp.json()) as WhamUsageResponse;
326
343
  quotaPhase = "publish";
327
- const retried = await retryMainAccountInfoIfIdentityChanged(requestAccountId, retriesRemaining, nativeMainLease, explicitRefresh, paced);
344
+ const retried = await retryMainAccountInfoIfIdentityChanged(requestAccountId, retriesRemaining, nativeMainLease, explicitRefresh, paced, options);
328
345
  if (retried) return retried;
329
346
  quotaPhase = "decode";
330
347
  if (data === null || typeof data !== "object" || Array.isArray(data)) {
@@ -376,7 +393,7 @@ export async function fetchMainAccountInfoWhileOwned(
376
393
  // never settled and the dashboard kept showing nothing — the symptom #327 reported.
377
394
  // An explicit refresh is an operator asking to re-evaluate, normally right after
378
395
  // signing in again, so it stays authoritative.
379
- if (explicitRefresh) clearAccountNeedsReauth(MAIN_CODEX_ACCOUNT_ID);
396
+ if (explicitRefresh && !options.passive) clearAccountNeedsReauth(MAIN_CODEX_ACCOUNT_ID);
380
397
  // Mirror main quota + plan into the shared stores so the rotation engine can
381
398
  // score and auto-switch the main account exactly like a pool account (Option A).
382
399
  setMainAccountPlan(result.plan);
@@ -398,7 +415,7 @@ export async function fetchMainAccountInfoWhileOwned(
398
415
  ...(freshResetCredits !== undefined ? { freshResetCredits } : {}),
399
416
  };
400
417
  } catch (error) {
401
- const retried = await retryMainAccountInfoIfIdentityChanged(requestAccountId, retriesRemaining, nativeMainLease, explicitRefresh, paced);
418
+ const retried = await retryMainAccountInfoIfIdentityChanged(requestAccountId, retriesRemaining, nativeMainLease, explicitRefresh, paced, options);
402
419
  if (retried) return retried;
403
420
  if (!credentialIsCurrent()) {
404
421
  return { info: getMainAccountInfoCache() ?? EMPTY_MAIN_ACCOUNT_INFO,
@@ -1,9 +1,11 @@
1
- import { codexAccountUsesCreditsAfterLimit } from "../account-credit-use";
1
+ import { codexAccountUsesCreditsAfterLimit, codexUsageLimitResetAt } from "../account-credit-use";
2
2
  import { nextCodexUsageQueryAt, nextQuotaQueryDelay, pruneRemovedCodexPoolUsageAccounts } from "../quota-query-backoff";
3
3
  import { CODEX_PRIORITY_FAILBACK_REFRESH_MS } from "../account-priority";
4
4
  import { codexQuotaHasFreshUsage } from "../quota-observation-freshness";
5
5
  import { getCodexAccountCredential, getValidCodexToken, readCodexAccountRecord } from "../account-store";
6
- import { getAccountQuota, isCompleteCodexQuotaRecoverySnapshot } from "../quota";
6
+ import { getAccountQuota, getMainPolicyQuota, isCompleteCodexQuotaRecoverySnapshot } from "../quota";
7
+ import { isCodexAccountPaused } from "../account-pause";
8
+ import { hasSpendableCodexCredits } from "../quota-types";
7
9
  import { reconcileMainCodexAccountRuntimeState } from "../account-lifecycle";
8
10
  import { claimDueCodexQuotaRecoveryProbes, settleCodexQuotaRecoveryProbe } from "../routing";
9
11
  import { readCodexTokens } from "../auth-collision";
@@ -73,17 +75,33 @@ export async function runCodexCooldownRecoveryProbes(config: OcxConfig, now = Da
73
75
  return cooldownRecoveryInFlight;
74
76
  }
75
77
 
78
+ const MAIN_CREDIT_RENEWAL_AGE_MS = 3 * 60_000;
79
+
76
80
  let mainHardLockRecoveryInFlight: Promise<void> | null = null;
77
81
  let mainHardLockRecoveryAttempt: { identity: number; credential: number; after: number; delay: number } | undefined;
78
82
 
79
83
  /** Metadata-only recovery on the existing sweep; failures retain the observed policy block. */
80
84
  export async function runMainAccountHardLockRecovery(config: OcxConfig): Promise<void> {
81
85
  if (mainHardLockRecoveryInFlight) return mainHardLockRecoveryInFlight;
86
+ /** Schedule a real credit observation while leaving actual-time admission unchanged. */
87
+ const creditRecoveryNeeded = (): boolean => {
88
+ const now = Date.now();
89
+ const quota = getMainPolicyQuota();
90
+ const observedAt = quota?.credits?.observedAt;
91
+ // getMainPolicyQuota binds evidence to the observed physical identity. Only renew
92
+ // valid formerly spendable funds; admission retains the actual five-minute clock.
93
+ return codexAccountUsesCreditsAfterLimit(config, MAIN_CODEX_ACCOUNT_ID)
94
+ && !isCodexAccountPaused(config, MAIN_CODEX_ACCOUNT_ID)
95
+ && codexUsageLimitResetAt(quota, undefined, now) !== undefined
96
+ && observedAt !== undefined && observedAt > 0
97
+ && now - observedAt >= MAIN_CREDIT_RENEWAL_AGE_MS
98
+ && hasSpendableCodexCredits(quota, observedAt);
99
+ };
82
100
  const status = getMainAccountHardLockStatus(config);
83
- if (status.state !== "blocked") { mainHardLockRecoveryAttempt = undefined; return; }
101
+ if (status.state !== "blocked" && !creditRecoveryNeeded()) { mainHardLockRecoveryAttempt = undefined; return; }
84
102
  if (isAccountNeedsReauth(MAIN_CODEX_ACCOUNT_ID)) return;
85
103
  // A predicted reset is a scheduling hint, never proof that the hard lock can be lifted.
86
- if (status.resetAt !== undefined && status.resetAt > Date.now()) return;
104
+ if (status.state === "blocked" && status.resetAt !== undefined && status.resetAt > Date.now()) return;
87
105
  // Reconcile the owned physical credential before skipping work for an older one.
88
106
  reconcileMainCodexAccountRuntimeState();
89
107
  const physical = readCodexTokens();
@@ -98,9 +116,10 @@ export async function runMainAccountHardLockRecovery(config: OcxConfig): Promise
98
116
  if (!lease) return;
99
117
  mainHardLockRecoveryInFlight = (async () => {
100
118
  reconcileMainCodexAccountRuntimeState();
101
- if (getMainAccountHardLockStatus(config).state !== "blocked"
119
+ if ((getMainAccountHardLockStatus(config).state !== "blocked" && !creditRecoveryNeeded())
102
120
  || isAccountNeedsReauth(MAIN_CODEX_ACCOUNT_ID)) return;
103
121
  const identityGeneration = captureMainAccountIdentityGeneration();
122
+ const passive = getMainAccountHardLockStatus(config).state !== "blocked";
104
123
  try {
105
124
  // Refresh can require an exclusive credential claim: never hold WHAM's shared
106
125
  // claim while obtaining a valid token. The runtime lease spans both operations.
@@ -111,7 +130,12 @@ export async function runMainAccountHardLockRecovery(config: OcxConfig): Promise
111
130
  // Native refresh retains its own grant-scoped refusal; global quarantine would outlive it.
112
131
  return;
113
132
  }
114
- if (isAccountNeedsReauth(MAIN_CODEX_ACCOUNT_ID)) return;
133
+ const currentStatus = getMainAccountHardLockStatus(config);
134
+ if (isAccountNeedsReauth(MAIN_CODEX_ACCOUNT_ID)
135
+ || !isMainAccountIdentityGenerationLive(identityGeneration)
136
+ || (passive && !creditRecoveryNeeded())
137
+ || (currentStatus.state === "blocked" && currentStatus.resetAt !== undefined
138
+ && currentStatus.resetAt > Date.now())) return;
115
139
  const credential = getMainQuotaCredentialGeneration();
116
140
  // Reconcile the physical bearer before consulting the previous credential's delay.
117
141
  if (previous?.identity === identityGeneration && previous.credential === credential
@@ -123,7 +147,7 @@ export async function runMainAccountHardLockRecovery(config: OcxConfig): Promise
123
147
  after: currentQueryAfter };
124
148
  return;
125
149
  }
126
- const result = await fetchMainAccountInfoAttempt(true, 1, lease, false, false, false, config);
150
+ const result = await fetchMainAccountInfoAttempt(true, 1, lease, false, false, false, config, { passive });
127
151
  // Never charge a replacement credential for a late result from its predecessor.
128
152
  if (isMainAccountIdentityGenerationLive(identityGeneration)
129
153
  && credential === getMainQuotaCredentialGeneration()) {
@@ -134,11 +158,11 @@ export async function runMainAccountHardLockRecovery(config: OcxConfig): Promise
134
158
  const authStatus = result.quotaRefresh.status === "http_error"
135
159
  ? result.quotaRefresh.httpStatus : undefined;
136
160
  const transientAuth = (authStatus === 401 || authStatus === 403)
137
- && !isAccountNeedsReauth(MAIN_CODEX_ACCOUNT_ID);
161
+ && !result.terminalAuthFailure && !isAccountNeedsReauth(MAIN_CODEX_ACCOUNT_ID);
138
162
  if (transientAuth) mainHardLockRecoveryAttempt = undefined;
139
163
  else {
140
164
  const delay = nextQuotaQueryDelay(previousDelay || undefined);
141
- mainHardLockRecoveryAttempt = getMainAccountHardLockStatus(config).state === "blocked"
165
+ mainHardLockRecoveryAttempt = getMainAccountHardLockStatus(config).state === "blocked" || creditRecoveryNeeded()
142
166
  ? { identity: identityGeneration, credential, delay, after: Math.max(Date.now() + delay, queryAfter) }
143
167
  : undefined;
144
168
  }
@@ -495,6 +495,7 @@ export async function fetchPoolAccountQuota(
495
495
  validatePending = false,
496
496
  afterDispatchSequence?: number,
497
497
  recoveryProbeNow?: number,
498
+ explicitRefresh = false,
498
499
  ): Promise<PoolQuotaResult> {
499
500
  const existing = getAccountQuota(accountId);
500
501
  if (afterDispatchSequence === undefined && !forceRefresh && existing && Date.now() - existing.updatedAt < POOL_CACHE_TTL) {
@@ -504,10 +505,23 @@ export async function fetchPoolAccountQuota(
504
505
  credentialGeneration: readCodexAccountRecord(accountId)?.generation,
505
506
  };
506
507
  }
508
+ const record = readCodexAccountRecord(accountId);
509
+ // The token endpoint already called this grant revoked or expired. Only a re-login or a
510
+ // completed validation rewrites the record and drops the marker, so until then a passive read
511
+ // (dashboard poll, listing, priming, recovery probe) would only POST the dead refresh token again
512
+ // and log the same 401. Report the stored verdict the failed refresh would have produced. An
513
+ // operator's explicit refresh command from any principal, a dashboard validation, and a post-reset
514
+ // readback keep their probe, and a source-linked credential spends no grant of its own, so none
515
+ // of them is held here.
516
+ if (!explicitRefresh && !validatePending && afterDispatchSequence === undefined && record && record.deletedAt == null
517
+ && record.credential && !record.credential.sourceAuthPath
518
+ && record.lastCodexValidationTerminal === true && record.lastCodexValidationStatus === "failed") {
519
+ markAccountNeedsReauth(accountId, captureConfigGeneration(), record.generation);
520
+ return { quota: existing ?? null, needsReauth: true, reauthReason: "refresh_failed", credentialGeneration: record.generation };
521
+ }
507
522
  // A token refresh may increment the generation (and rotate the refresh token) before WHAM
508
523
  // completes. Join a flight whose starting or resolved generation is still current, but let a
509
524
  // replacement credential with the same pool id start its own request.
510
- const record = readCodexAccountRecord(accountId);
511
525
  const flights = poolQuotaRefreshInFlight.get(accountId);
512
526
  const current = flights && [...flights].find(flight => {
513
527
  const generation = flight.state.resolvedCredentialGeneration
@@ -43,6 +43,7 @@ export async function handleCodexAuthAPI(
43
43
  // required by AGENTS_INSTALL.md. Raw-admin/CLI refreshes remain observational.
44
44
  return jsonResponse({ accounts: await listCodexAuthAccounts(config, true, {
45
45
  validatePending: principal === "gui-session",
46
+ explicitRefresh: true,
46
47
  }) });
47
48
  }
48
49
 
@@ -1,3 +1,4 @@
1
+ import { isCodexControlPlaneModel } from "../control-plane-models";
1
2
  import { redactSecretString } from "../../lib/redact";
2
3
  import type { OcxConfig } from "../../types";
3
4
  import { encodeRoutedModelId } from "../../providers/slug-codec";
@@ -34,6 +35,7 @@ export type AutoReviewModelOverrideResult = "absent" | "applied" | "invalid" | "
34
35
 
35
36
  /** True when a catalog row was synthesized by opencodex instead of coming from upstream. */
36
37
  function isRoutedCatalogEntry(entry: RawEntry): boolean {
38
+ if (isCodexControlPlaneModel(entry.slug)) return false;
37
39
  const slug = typeof entry.slug === "string" ? entry.slug : "";
38
40
  return slug.includes("/")
39
41
  || (typeof entry.description === "string" && entry.description.startsWith("Routed via opencodex → "));
@@ -57,6 +59,7 @@ function clearAutoReviewOverrideValue(entry: RawEntry): void {
57
59
  * Returns the stamped values when the observed rows match that shape.
58
60
  */
59
61
  function legacyRootStampValues(observedModels: readonly RawEntry[]): ReadonlySet<string> | undefined {
62
+ observedModels = observedModels.filter(entry => !isCodexControlPlaneModel(entry?.slug));
60
63
  if (observedModels.some(entry => entry?.[AUTO_REVIEW_ROOT_MARKER] !== undefined)) return undefined;
61
64
  const configuredValues = new Set(observedModels.flatMap(entry => {
62
65
  const value = entry?.auto_review_model_override;
@@ -90,7 +93,7 @@ function clearLegacyRootStamps(models: readonly RawEntry[], sourceModels: readon
90
93
  const legacyStamp = legacyRootStampValues([...models, ...sourceModels]);
91
94
  if (legacyStamp === undefined) return;
92
95
  for (const entry of models) {
93
- if (!entry || typeof entry !== "object") continue;
96
+ if (!entry || typeof entry !== "object" || isCodexControlPlaneModel(entry.slug)) continue;
94
97
  const current = entry.auto_review_model_override;
95
98
  if (entry[AUTO_REVIEW_ROOT_MARKER] === undefined
96
99
  && typeof current === "string" && legacyStamp.has(current)) clearAutoReviewOverrideValue(entry);
@@ -107,7 +110,7 @@ function clearAutoReviewModelOverride(
107
110
  ): void {
108
111
  const legacyStamp = legacyRootStampValues([...models, ...sourceModels]);
109
112
  for (const entry of models) {
110
- if (!entry || typeof entry !== "object") continue;
113
+ if (!entry || typeof entry !== "object" || isCodexControlPlaneModel(entry.slug)) continue;
111
114
  const current = entry.auto_review_model_override;
112
115
  if (isRoutedCatalogEntry(entry)
113
116
  || (entry[AUTO_REVIEW_ROOT_MARKER] === true || rootAutoReviewStamp(entry) !== undefined)
@@ -188,6 +191,7 @@ function preserveNativeAutoReviewModelOverrides(
188
191
 
189
192
  /** Stamp a root-derived override and mark native rows so later root removal is durable. */
190
193
  function stampRootAutoReviewOverride(entry: RawEntry, target: string): void {
194
+ if (isCodexControlPlaneModel(entry.slug)) return;
191
195
  if (!isRoutedCatalogEntry(entry)) {
192
196
  const previous = rootAutoReviewStamp(entry);
193
197
  const current = entry.auto_review_model_override;
@@ -399,7 +403,7 @@ function applyRootSelectorToRemaining(
399
403
  ): AutoReviewModelOverrideResult {
400
404
  const clearRemaining = (): void => {
401
405
  for (const entry of models) {
402
- if (!entry || providerStamped.has(entry)) continue;
406
+ if (!entry || providerStamped.has(entry) || isCodexControlPlaneModel(entry.slug)) continue;
403
407
  // Native rows written by releases before the root marker cannot be told apart from upstream
404
408
  // values once provider stamps diverge. clearLegacyRootStamps sweeps the ones the legacy
405
409
  // uniform signature still recognizes before provider plans land, because provider stamping
@@ -427,7 +431,7 @@ function applyRootSelectorToRemaining(
427
431
  return "unresolved";
428
432
  }
429
433
  for (const entry of models) {
430
- if (!entry || providerStamped.has(entry)) continue;
434
+ if (!entry || providerStamped.has(entry) || isCodexControlPlaneModel(entry.slug)) continue;
431
435
  stampRootAutoReviewOverride(entry, trimmed);
432
436
  }
433
437
  return "applied";
@@ -449,7 +453,7 @@ export function applyConfiguredAutoReviewModelOverride(
449
453
  const { plans, failure } = buildProviderReviewPlans(models, config);
450
454
  const providerStamped = new Set<RawEntry>();
451
455
  for (const entry of models) {
452
- if (!entry || typeof entry !== "object") continue;
456
+ if (!entry || typeof entry !== "object" || isCodexControlPlaneModel(entry.slug)) continue;
453
457
  const provider = catalogEntryProviderName(entry);
454
458
  if (!provider) continue;
455
459
  const plan = plans.get(provider);
@@ -482,7 +486,10 @@ export function finalizeAutoReviewModelOverride(
482
486
  sourceModels: readonly RawEntry[] = [],
483
487
  config?: Pick<OcxConfig, "providers">,
484
488
  ): AutoReviewModelOverrideResult {
485
- if (models && sourceModels.length > 0) preserveNativeAutoReviewModelOverrides(models, sourceModels);
489
+ if (models && sourceModels.length > 0) preserveNativeAutoReviewModelOverrides(
490
+ models.filter(entry => !isCodexControlPlaneModel(entry.slug)),
491
+ sourceModels.filter(entry => !isCodexControlPlaneModel(entry.slug)),
492
+ );
486
493
  if (config && configHasProviderAutoReview(config)) {
487
494
  return applyConfiguredAutoReviewModelOverride(models, readConfiguredAutoReviewModel(), config, sourceModels);
488
495
  }
@@ -1,3 +1,5 @@
1
+ import { isCodexControlPlaneModel } from "../control-plane-models";
2
+ import { withCodexControlPlaneRows } from "./control-plane";
1
3
  import { CODEX_REASONING_LEVELS, type CodexReasoningLevel } from "../../reasoning-effort";
2
4
  import { clearModelCache } from "../model-cache";
3
5
  import { routedSlug, slugEquivalenceKey } from "../../providers/slug-codec";
@@ -194,7 +196,7 @@ export function buildCatalogEntriesFromObservedState({
194
196
  const comboPublicSlugs = new Set(goModels
195
197
  .filter(model => model.provider === COMBO_NAMESPACE)
196
198
  .map(catalogModelSlug));
197
- for (const slug of gptSlugs) {
199
+ for (const slug of gptSlugs.filter(slug => !isCodexControlPlaneModel(slug))) {
198
200
  const native = deriveEntry(template, slug, "OpenAI native model (Codex OAuth passthrough).", 9, undefined, new Set(), openaiContextCap);
199
201
  if (rank.has(slug)) native.priority = rank.get(slug)!;
200
202
  nativeEntries.push(native);
@@ -224,7 +226,7 @@ export function buildCatalogEntriesFromObservedState({
224
226
  const selectorNativeSlugs = accountNativeSlugsBySelector?.get(selector)
225
227
  ?? accountNativeSlugs
226
228
  ?? gptSlugs;
227
- const accountNativeEntries = selectorNativeSlugs.filter(slug => slug !== NATIVE_RESERVE_MODEL).map(slug => (
229
+ const accountNativeEntries = selectorNativeSlugs.filter(slug => slug !== NATIVE_RESERVE_MODEL && !isCodexControlPlaneModel(slug)).map(slug => (
228
230
  nativeEntriesBySlug.get(slug)
229
231
  ?? deriveEntry(template, slug, "OpenAI native model (Codex OAuth passthrough).", 9, undefined, new Set(), openaiContextCap)
230
232
  ));
@@ -306,7 +308,7 @@ export function buildCatalogEntriesFromObservedState({
306
308
  delete entry.prefer_websockets;
307
309
  }
308
310
  }
309
- return applyMultiAgentMode(out, multiAgentMode, multiAgentV2Enabled, {
311
+ return applyMultiAgentMode(withCodexControlPlaneRows(out, template ? [template] : [], true, wsEnabled), multiAgentMode, multiAgentV2Enabled, {
310
312
  keepNativeChatGptOnV1,
311
313
  preserveDefaultMultiAgentVersion: isReserveCatalogProjection,
312
314
  });
@@ -409,12 +411,12 @@ export function mergeCatalogModelsWithNativeRecovery(
409
411
  ): RawEntry[] {
410
412
  const merged = [...primaryCatalogModels];
411
413
  const recoveredNativeSlugs = new Set(primaryCatalogModels.flatMap(entry => {
412
- const slug = recoverableNativeSlug(entry);
414
+ const slug = isCodexControlPlaneModel(entry.slug) ? String(entry.slug) : recoverableNativeSlug(entry);
413
415
  return slug === null ? [] : [slug];
414
416
  }));
415
417
  for (const source of nativeRecoverySources) {
416
418
  for (const entry of source) {
417
- const slug = recoverableNativeSlug(entry);
419
+ const slug = isCodexControlPlaneModel(entry.slug) ? String(entry.slug) : recoverableNativeSlug(entry);
418
420
  if (slug === null || recoveredNativeSlugs.has(slug)) continue;
419
421
  merged.push(structuredClone(entry) as RawEntry);
420
422
  recoveredNativeSlugs.add(slug);
@@ -460,6 +462,7 @@ export function applyFullModelPickerOrder(entries: RawEntry[], order: readonly s
460
462
  if (!pickerOrder.some(slug => !slug.includes("/"))) return;
461
463
  const rankOf = modelPickerRank(pickerOrder);
462
464
  for (const entry of entries) {
465
+ if (isCodexControlPlaneModel(entry.slug)) continue;
463
466
  const natural = entry[SPAWN_PRIORITY_FIELD] ?? entry.priority ?? 9;
464
467
  entry[SPAWN_PRIORITY_FIELD] = natural;
465
468
  entry.priority = rankOf(String(entry.slug)) ?? pickerOrder.length + Number(natural);
@@ -539,10 +542,12 @@ export function mergeCatalogEntriesFromObservedState({
539
542
  // Raw catalog rows contain nested arrays/objects that normalization mutates. Detach every row at
540
543
  // the observed-core boundary so callers can safely retain evidence objects or repeat the merge.
541
544
  const detachedCatalogModels = catalogModels
545
+ .filter(entry => !isCodexControlPlaneModel(entry.slug))
542
546
  .map(entry => restoreNativeDisplayName(structuredClone(entry) as RawEntry));
543
547
  const detachedBaselineCatalogModels = baselineCatalogModels
548
+ .filter(entry => !isCodexControlPlaneModel(entry.slug))
544
549
  .map(entry => restoreNativeDisplayName(structuredClone(entry) as RawEntry));
545
- const detachedRoutedEntries = routedEntries.map(entry => structuredClone(entry) as RawEntry);
550
+ const detachedRoutedEntries = routedEntries.filter(entry => !isCodexControlPlaneModel(entry.slug)).map(entry => structuredClone(entry) as RawEntry);
546
551
  // Track this invocation's generated custom rows, not ownership markers read from disk.
547
552
  // Their builder already finalized exact native ladders and ordinary routed mock tiers.
548
553
  const freshCustomEntries = new Set(detachedRoutedEntries.filter(entry =>
@@ -921,7 +926,10 @@ export function mergeCatalogEntriesFromObservedState({
921
926
  // clobber a hide flag back to list. Bare ids disable every account clone; qualified ids disable
922
927
  // only their generated account row.
923
928
  const versionedEntries = applyMultiAgentMode(
924
- applyNativeVisibility(mergedEntries, disabledModels, alignedAccountBoundEntries.length > 0, observedNativeSlugs),
929
+ withCodexControlPlaneRows(
930
+ applyNativeVisibility(mergedEntries, disabledModels, alignedAccountBoundEntries.length > 0, observedNativeSlugs),
931
+ [...catalogModels, ...baselineCatalogModels, ...routedEntries], includeNativeOpenAi, wsEnabled,
932
+ ),
925
933
  multiAgentMode,
926
934
  multiAgentV2Enabled,
927
935
  { keepNativeChatGptOnV1, preserveDefaultMultiAgentVersion: isReserveCatalogProjection, nativeDefaults: nativeMultiAgentDefaults },
@@ -0,0 +1,45 @@
1
+ import { CODEX_INTERNAL_OPENAI_MODELS, isCodexControlPlaneModel } from "../control-plane-models";
2
+ import { COMBO_NAMESPACE } from "../../combos";
3
+ import { CODEX_NATIVE_ALIAS_CATALOG_KIND } from "./kinds";
4
+ import { NATIVE_RESERVE_MODEL, SUPPORTED_NATIVE_OPENAI_SLUGS } from "./native-models";
5
+ import { isReserveCatalogProjection } from "./reserve";
6
+ import { pinnedNativeModelRows } from "./pinned-models";
7
+ import type { RawEntry } from "./parsing";
8
+
9
+ /** Hidden ordinary natives qualify; Reserve, account choices and routed aliases do not. */
10
+ export function hasOrdinaryNativeOpenAiRow(entries: readonly RawEntry[]): boolean {
11
+ return entries.some(entry => typeof entry.slug === "string"
12
+ && !entry.slug.includes("/")
13
+ && SUPPORTED_NATIVE_OPENAI_SLUGS.has(entry.slug)
14
+ && entry.slug !== NATIVE_RESERVE_MODEL
15
+ && !isReserveCatalogProjection(entry)
16
+ && entry.opencodex_catalog_kind !== CODEX_NATIVE_ALIAS_CATALOG_KIND
17
+ && entry.owned_by !== COMBO_NAMESPACE);
18
+ }
19
+
20
+ /** Keep internal reviewer metadata separate from native synthesis, picker ordering and account clones. */
21
+ export function withCodexControlPlaneRows(
22
+ entries: RawEntry[],
23
+ sourceRows: readonly RawEntry[],
24
+ includeNativeOpenAi: boolean,
25
+ wsEnabled: boolean,
26
+ ): RawEntry[] {
27
+ const models = entries.filter(entry => !isCodexControlPlaneModel(entry.slug));
28
+ if (!includeNativeOpenAi || !hasOrdinaryNativeOpenAiRow(models)) return models;
29
+ for (const slug of CODEX_INTERNAL_OPENAI_MODELS) {
30
+ const source = sourceRows.find(entry => entry.slug === slug)
31
+ ?? pinnedNativeModelRows().find(entry => entry.slug === slug);
32
+ // A pin without the row degrades to Codex's own task-model fallback rather than failing the
33
+ // whole catalog write.
34
+ if (!source) continue;
35
+ const row: RawEntry = structuredClone(source);
36
+ row.visibility = "hide";
37
+ if (wsEnabled) row.supports_websockets = true;
38
+ else {
39
+ delete row.supports_websockets;
40
+ delete row.prefer_websockets;
41
+ }
42
+ models.push(row);
43
+ }
44
+ return models;
45
+ }
@@ -1,3 +1,5 @@
1
+ import { hasOrdinaryNativeOpenAiRow } from "./control-plane";
2
+ import { isCodexControlPlaneModel } from "../control-plane-models";
1
3
  import { execFileSync } from "node:child_process";
2
4
  import { createHash } from "node:crypto";
3
5
  import { copyFileSync, existsSync, mkdirSync, readFileSync, realpathSync } from "node:fs";
@@ -385,7 +387,7 @@ export function clampEntryToCodexSupportedEfforts(
385
387
  entry: RawEntry,
386
388
  supported: ReadonlySet<string> | null,
387
389
  ): void {
388
- if (!supported) return;
390
+ if (!supported || isCodexControlPlaneModel(entry.slug)) return;
389
391
  const levels = Array.isArray(entry.supported_reasoning_levels)
390
392
  ? entry.supported_reasoning_levels as Array<{ effort?: string }>
391
393
  : null;
@@ -532,6 +534,14 @@ export function clampCatalogModelsToObservedCodexSupport(
532
534
  else index += 1;
533
535
  }
534
536
 
537
+ // Only Reserve rows are currently omitted above; ordinary natives keep a fallback ladder.
538
+ // Also repair a persisted orphan so a removed Reserve can never leave a reviewer behind.
539
+ if (!hasOrdinaryNativeOpenAiRow(models)) {
540
+ for (let index = models.length - 1; index >= 0; index -= 1) {
541
+ if (isCodexControlPlaneModel(models[index]!.slug)) models.splice(index, 1);
542
+ }
543
+ }
544
+
535
545
  return {
536
546
  removedEfforts: [...removed].sort(),
537
547
  affectedModels: affected,
@@ -32,6 +32,7 @@ import {
32
32
  type OAuthActiveTokenObservation,
33
33
  } from "../../oauth";
34
34
  import { getAccountSet } from "../../oauth/store";
35
+ import { mayResolveModelsOAuth } from "../../oauth/model-discovery-auth";
35
36
  import type { OcxConfig, OcxProviderConfig } from "../../types";
36
37
  import { modelInList } from "../../types";
37
38
  import { CODEX_REASONING_LEVELS, codexEffortRank, configuredReasoningEfforts, modelRecordValue, sanitizeCodexReasoningEfforts } from "../../reasoning-effort";
@@ -389,15 +390,15 @@ export function captureProviderGather(
389
390
  ? authResolver.resolve(name, provider)
390
391
  : undefined;
391
392
  // A refreshing capture carries the stored origin so accounts on different hosts keep separate
392
- // flights. The send is rebuilt from the auth the gather resolves, and the observed path never
393
+ // flights. Host-scoped auth rebuilds its request; B retains its configured captured target. Observed capture never
393
394
  // reads the live store.
394
395
  const oauthApiBaseUrl = observedAuth
395
396
  ? observedAuth.oauthApiBaseUrl
396
- : authResolver.kind === "refreshing" && provider.authMode === "oauth"
397
+ : authResolver.kind === "refreshing" && provider.authMode === "oauth" && mayResolveModelsOAuth(name, provider)
397
398
  ? getOAuthCredentialApiBaseUrl(name)
398
399
  : undefined;
399
400
  const request = captureModelsRequest(name, provider, oauthApiBaseUrl);
400
- const refreshingOAuthAccountId = !observedAuth && authResolver.kind === "refreshing" && provider.authMode === "oauth"
401
+ const refreshingOAuthAccountId = !observedAuth && authResolver.kind === "refreshing" && provider.authMode === "oauth" && mayResolveModelsOAuth(name, provider)
401
402
  ? getAccountSet(name)?.activeAccountId
402
403
  : undefined;
403
404
  const resolved = resolveProviderModelDiscovery(name, provider);
@@ -531,6 +532,7 @@ function providerCatalogFingerprint(name: string, prov: OcxProviderConfig): Reco
531
532
  live: prov.liveModels ?? null,
532
533
  base: prov.baseUrl ?? "",
533
534
  adapter: prov.adapter ?? "",
535
+ instance: prov.anthropicOAuthInstance ?? null,
534
536
  models: [...(prov.models ?? [])].sort(),
535
537
  retain: [...(prov.retainModels ?? [])].sort(),
536
538
  selected: [...(prov.selectedModels ?? [])].sort(),
@@ -1,3 +1,4 @@
1
+ import { isCodexControlPlaneModel } from "../control-plane-models";
1
2
  import { execFileSync } from "node:child_process";
2
3
  import { createHash } from "node:crypto";
3
4
  import { copyFileSync, existsSync, mkdirSync, readFileSync, realpathSync } from "node:fs";
@@ -700,7 +701,7 @@ export function catalogEntryIsNativeChatGpt(entry: RawEntry): boolean {
700
701
  && entry.use_responses_lite === true
701
702
  && hasNativeOpenAiCapabilityMetadata(routedNativeSlug)
702
703
  ) return true;
703
- if (UPSTREAM_NATIVE_ENTRIES.has(slug) || SUPPORTED_NATIVE_OPENAI_SLUGS.has(slug)) return true;
704
+ if (isCodexControlPlaneModel(slug) || UPSTREAM_NATIVE_ENTRIES.has(slug) || SUPPORTED_NATIVE_OPENAI_SLUGS.has(slug)) return true;
704
705
  return false;
705
706
  }
706
707
 
@@ -1,3 +1,4 @@
1
+ import { isAnthropicOAuthInstance } from "../../providers/anthropic-instance";
1
2
  import { effectiveProviderAlias, effectiveProviderAliasDecision } from "../../providers/default-aliases";
2
3
  import { initialModelSelectionPending } from "../../providers/initial-model-selection";
3
4
  import { execFileSync } from "node:child_process";
@@ -28,11 +29,12 @@ import {
28
29
  } from "../model-cache";
29
30
  import {
30
31
  buildModelsRequest,
31
- getValidAccessTokenSnapshot,
32
+ getModelsOAuthAccessSnapshot,
32
33
  observeActiveOAuthAccessToken,
33
34
  resolveModelsAuthToken,
34
35
  type OAuthActiveTokenObservation,
35
36
  } from "../../oauth";
37
+ import { mayResolveModelsOAuth } from "../../oauth/model-discovery-auth";
36
38
  import type { OcxConfig, OcxProviderConfig } from "../../types";
37
39
  import { modelInList } from "../../types";
38
40
  import { CODEX_REASONING_LEVELS, codexEffortRank, configuredReasoningEfforts, modelRecordValue, sanitizeCodexReasoningEfforts } from "../../reasoning-effort";
@@ -145,7 +147,8 @@ export function observedModelsAuthResolver(
145
147
  return { apiKey: resolveProviderApiKey(provider.apiKey), observed: true };
146
148
  }
147
149
 
148
- const observation = observeActiveOAuthAccessToken(name, authStoreBuffer);
150
+ const observation: OAuthActiveTokenObservation = mayResolveModelsOAuth(name, provider)
151
+ ? observeActiveOAuthAccessToken(name, authStoreBuffer) : { kind: "missing" };
149
152
  outcomes.push({ provider: name, state: observation.kind });
150
153
  if (observation.kind !== "available") return { apiKey: undefined, observed: true };
151
154
  return {
@@ -179,7 +182,7 @@ export async function fetchProviderModelsWithAuth(
179
182
  // generation, so a request started with the former account cannot later publish its result.
180
183
  const cacheGeneration = captureModelCacheGeneration(name);
181
184
  const isCurrentCacheGeneration = () => isModelCacheGenerationCurrent(name, cacheGeneration);
182
- const anthropicSelection = name === "anthropic" && prov.authMode === "oauth"
185
+ const anthropicSelection = isAnthropicOAuthInstance(name) && prov.authMode === "oauth" && mayResolveModelsOAuth(name, prov)
183
186
  ? captureOAuthAccountSelection(name) : null;
184
187
  if (prov.authMode === "forward") return observed([], "authoritative"); // ChatGPT backend has no /models
185
188
  const seedVertexDefault = prov.adapter === "google"
@@ -257,9 +260,11 @@ export async function fetchProviderModelsWithAuth(
257
260
  }
258
261
  return observed(configured, "authoritative");
259
262
  }
260
- const auth: ModelsAuthResolution = captured.observedAuth ?? (resolveAuth.kind === "refreshing"
263
+ const auth: ModelsAuthResolution = prov.authMode === "oauth" && !mayResolveModelsOAuth(name, prov)
264
+ ? { apiKey: undefined, observed: resolveAuth.kind === "observed" }
265
+ : captured.observedAuth ?? (resolveAuth.kind === "refreshing"
261
266
  ? prov.authMode === "oauth"
262
- ? await getValidAccessTokenSnapshot(name)
267
+ ? await getModelsOAuthAccessSnapshot(name, prov)
263
268
  .then(snapshot => ({
264
269
  apiKey: snapshot.accessToken,
265
270
  observed: false,
@@ -274,7 +279,8 @@ export async function fetchProviderModelsWithAuth(
274
279
  : resolveAuth.resolve(name, prov));
275
280
  const apiKey = auth.apiKey;
276
281
  const maySendAnthropicDiscovery = () => {
277
- if (name !== "anthropic" || prov.authMode !== "oauth") return true;
282
+ if (!isAnthropicOAuthInstance(name) || prov.authMode !== "oauth") return true;
283
+ if (!mayResolveModelsOAuth(name, prov)) return false;
278
284
  const selected = captureOAuthAccountSelection(name);
279
285
  const row = auth.oauthAccountId ? getAccountCredentialWithStatus(name, auth.oauthAccountId) : null;
280
286
  return !!anthropicSelection && !!selected && !!row && !row.paused && !row.needsReauth
@@ -633,7 +639,7 @@ export async function fetchProviderModelsWithAuth(
633
639
  }
634
640
  // The captured request predates any refresh, so a refreshing gather rebuilds it
635
641
  // from the auth it resolved: the token and its origin, together.
636
- const request = resolveAuth.kind === "refreshing"
642
+ const request = resolveAuth.kind === "refreshing" && name !== "anthropic2"
637
643
  ? captureModelsRequest(name, prov, auth.oauthApiBaseUrl)
638
644
  : captured.request;
639
645
  const url = request.url;
@@ -653,7 +659,7 @@ export async function fetchProviderModelsWithAuth(
653
659
  const failedDiscoveryFallback = (
654
660
  failure: ProviderModelDiscoveryFailure,
655
661
  ): { models: CatalogModel[]; fallback: "stale" | "configured"; shouldLog: boolean } => {
656
- if (!isCurrentCacheGeneration()) {
662
+ if (!isCurrentCacheGeneration() || !maySendAnthropicDiscovery()) {
657
663
  return {
658
664
  models: withConfiguredRetention(failedDiscoveryConfigured),
659
665
  fallback: "configured",
@@ -854,7 +860,7 @@ export async function fetchProviderModelsWithAuth(
854
860
  `[opencodex] Provider model discovery for "${name}" returned an authoritative empty catalog; ${droppedConfiguredIds.length > 0 ? `dropping configured model ids: ${droppedConfiguredIds.join(", ")}` : "no models will be exposed"}.`,
855
861
  );
856
862
  }
857
- if (!setCached(name, forCache, Date.now(), cacheGeneration)) {
863
+ if (!maySendAnthropicDiscovery() || !setCached(name, forCache, Date.now(), cacheGeneration)) {
858
864
  return observed(withConfiguredRetention(configured), "degraded");
859
865
  }
860
866
  markProviderDiscoveryOk(name, liveModelCount);
@@ -0,0 +1,6 @@
1
+ /** Exact Codex control-plane ids routed to canonical OpenAI, outside the public native roster. */
2
+ export const CODEX_INTERNAL_OPENAI_MODELS: ReadonlySet<string> = new Set(["codex-auto-review"]);
3
+
4
+ export function isCodexControlPlaneModel(slug: unknown): boolean {
5
+ return typeof slug === "string" && CODEX_INTERNAL_OPENAI_MODELS.has(slug);
6
+ }