@bitkyc08/opencodex 2.80.0 → 2.82.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 (376) hide show
  1. package/README.md +3 -3
  2. package/bin/ocx.mjs +166 -73
  3. package/gui/dist/assets/App-Ds_qL7o8.js +54 -0
  4. package/gui/dist/assets/Tray-BqHF5Trd.js +1 -0
  5. package/gui/dist/assets/{index-Brk6F3fn.css → index-BAbcj51J.css} +1 -1
  6. package/gui/dist/assets/index-D3FBCNiP.js +94 -0
  7. package/gui/dist/assets/quota-summary-DTBC8n7-.js +1 -0
  8. package/gui/dist/index.html +2 -2
  9. package/gui/dist/provider-icons/claude-green.svg +1 -0
  10. package/package.json +3 -2
  11. package/src/adapters/anthropic-model-contract.ts +8 -2
  12. package/src/adapters/anthropic.ts +8 -3
  13. package/src/adapters/codebuddy/mcp-server.ts +2 -25
  14. package/src/adapters/codebuddy/tool-bridge.ts +1 -7
  15. package/src/adapters/coding-agent/mcp-server.ts +33 -0
  16. package/src/adapters/coding-agent/protocol.ts +122 -17
  17. package/src/adapters/coding-agent/tool-catalog.ts +75 -0
  18. package/src/adapters/coding-agent/turn.ts +101 -44
  19. package/src/adapters/cursor/catalog.ts +8 -0
  20. package/src/adapters/cursor/effort-map.ts +2 -0
  21. package/src/adapters/devin/live-models.ts +2 -0
  22. package/src/adapters/kiro-retry.ts +9 -4
  23. package/src/adapters/openai-chat/messages.ts +7 -6
  24. package/src/adapters/openai-responses/compaction-search-history.ts +109 -0
  25. package/src/adapters/openai-responses/passthrough.ts +9 -4
  26. package/src/adapters/openai-responses/tool-output-recovery.ts +20 -0
  27. package/src/adapters/physical-send.ts +8 -5
  28. package/src/adapters/qoder/adapter.ts +41 -6
  29. package/src/adapters/qoder/scaffold-guard.ts +2 -3
  30. package/src/adapters/zed.ts +16 -1
  31. package/src/claude/agents-inject.ts +14 -3
  32. package/src/claude/context-windows.ts +19 -17
  33. package/src/claude/desktop-3p.ts +88 -12
  34. package/src/claude/desktop-profile.ts +2 -1
  35. package/src/claude/gateway-cache.ts +18 -1
  36. package/src/claude/inbound-cache-stabilize.ts +4 -9
  37. package/src/claude/inbound.ts +78 -3
  38. package/src/claude/intercept/cli-catalog.ts +3 -0
  39. package/src/claude/intercept/model-bindings.ts +1 -0
  40. package/src/claude/intercept/picker-ca-persistence.ts +7 -6
  41. package/src/claude/intercept/picker-listener.ts +71 -4
  42. package/src/claude/intercept/picker-models.ts +14 -6
  43. package/src/claude/intercept/settings.ts +3 -0
  44. package/src/claude/long-context.ts +66 -0
  45. package/src/claude/model-info.ts +19 -16
  46. package/src/claude/outbound.ts +94 -9
  47. package/src/claude/subagent-model.ts +40 -11
  48. package/src/cli/account-anthropic-threshold.ts +14 -5
  49. package/src/cli/account-api.ts +3 -1
  50. package/src/cli/account-extended.ts +13 -9
  51. package/src/cli/account-policy-dto.ts +1 -1
  52. package/src/cli/account-policy.ts +13 -4
  53. package/src/cli/account.ts +3 -3
  54. package/src/cli/agent.ts +9 -5
  55. package/src/cli/aside-profile-recovery.ts +33 -0
  56. package/src/cli/capabilities-accounts.ts +2 -2
  57. package/src/cli/capabilities-agents-routing.ts +24 -0
  58. package/src/cli/capabilities-base.ts +5 -5
  59. package/src/cli/capabilities-integrations.ts +18 -2
  60. package/src/cli/cli-path-diagnostics.ts +132 -0
  61. package/src/cli/codex-shim-autorestore.ts +3 -0
  62. package/src/cli/desktop-runtime-guidance.ts +65 -0
  63. package/src/cli/dispatch.ts +15 -1
  64. package/src/cli/doctor.ts +16 -3
  65. package/src/cli/help.ts +4 -1
  66. package/src/cli/index.ts +25 -14
  67. package/src/cli/integration-aside-sync.ts +2 -1
  68. package/src/cli/integration-plan-dto.ts +1 -0
  69. package/src/cli/integration-preview.ts +8 -1
  70. package/src/cli/integrations.ts +48 -6
  71. package/src/cli/message-args.ts +34 -0
  72. package/src/cli/message-command.ts +43 -0
  73. package/src/cli/message-runtime.ts +6 -0
  74. package/src/cli/observe.ts +3 -1
  75. package/src/cli/provider.ts +29 -6
  76. package/src/cli/registry.ts +24 -2
  77. package/src/cli/resolve.ts +25 -2
  78. package/src/cli/root.ts +4 -2
  79. package/src/cli/status.ts +64 -12
  80. package/src/cli/update-restart-child.ts +4 -2
  81. package/src/cli/update-restart-home.ts +28 -10
  82. package/src/cli/update-restart-service-record.ts +116 -0
  83. package/src/cli/update-restart-supervision.ts +96 -0
  84. package/src/cli/update-restart.ts +59 -13
  85. package/src/cli/v2.ts +27 -20
  86. package/src/cli/version-skew-notice.ts +77 -0
  87. package/src/clients/config-export/commandcode.ts +131 -0
  88. package/src/clients/config-export/contracts.ts +12 -0
  89. package/src/clients/config-export.ts +64 -1
  90. package/src/clients/lazycodex.ts +1 -1
  91. package/src/clients/omo-role-models.ts +32 -7
  92. package/src/codex/auth-api/account-list.ts +11 -3
  93. package/src/codex/auth-api/account-selection.ts +107 -0
  94. package/src/codex/auth-api/main-account-probe.ts +25 -8
  95. package/src/codex/auth-api/pool-mode-gate.ts +33 -9
  96. package/src/codex/auth-api/pool-quota-probe.ts +15 -1
  97. package/src/codex/auth-api/routes.ts +11 -49
  98. package/src/codex/auth-context.ts +20 -1
  99. package/src/codex/autostart-health.ts +12 -3
  100. package/src/codex/catalog/auto-review.ts +13 -6
  101. package/src/codex/catalog/build-entries.ts +33 -7
  102. package/src/codex/catalog/bundled.ts +155 -26
  103. package/src/codex/catalog/combo-member.ts +3 -2
  104. package/src/codex/catalog/control-plane.ts +45 -0
  105. package/src/codex/catalog/effort.ts +37 -6
  106. package/src/codex/catalog/gather-capture.ts +10 -3
  107. package/src/codex/catalog/model-hints.ts +15 -8
  108. package/src/codex/catalog/model-visibility.ts +3 -1
  109. package/src/codex/catalog/operator-tiers.ts +38 -0
  110. package/src/codex/catalog/parsing.ts +2 -1
  111. package/src/codex/catalog/provider-models.ts +58 -44
  112. package/src/codex/catalog/retained-sync.ts +7 -5
  113. package/src/codex/catalog/routed-gather.ts +15 -1
  114. package/src/codex/catalog-auto-refresh-sources.ts +3 -4
  115. package/src/codex/catalog-auto-refresh.ts +19 -0
  116. package/src/codex/catalog.ts +1 -1
  117. package/src/codex/codex-home-owner.ts +3 -0
  118. package/src/codex/config-write-lock.ts +333 -0
  119. package/src/codex/control-plane-models.ts +6 -0
  120. package/src/codex/convergence.ts +2 -1
  121. package/src/codex/features.ts +257 -200
  122. package/src/codex/inject/config-write-section.ts +34 -0
  123. package/src/codex/inject/multi-agent-v2.ts +14 -6
  124. package/src/codex/inject/remove.ts +36 -11
  125. package/src/codex/inject/restore.ts +85 -20
  126. package/src/codex/inject-coordination.ts +15 -6
  127. package/src/codex/inject.ts +62 -20
  128. package/src/codex/journal.ts +62 -17
  129. package/src/codex/main-account-cache.ts +43 -0
  130. package/src/codex/paths.ts +2 -0
  131. package/src/codex/prepared-home.ts +18 -0
  132. package/src/codex/prompt-journal.ts +25 -12
  133. package/src/codex/prompt-layers.ts +49 -26
  134. package/src/codex/prompt-lock-claim.ts +134 -0
  135. package/src/codex/prompt-lock-evidence.ts +26 -0
  136. package/src/codex/prompt-lock-io.ts +16 -0
  137. package/src/codex/prompt-lock-owner.ts +130 -0
  138. package/src/codex/prompt-lock.ts +84 -53
  139. package/src/codex/routing/failure-window.ts +146 -0
  140. package/src/codex/routing/selection.ts +4 -1
  141. package/src/codex/routing.ts +13 -17
  142. package/src/codex/runtime.ts +106 -18
  143. package/src/codex/shim.ts +45 -19
  144. package/src/combos/jev-decision-contract.ts +25 -2
  145. package/src/combos/jev-service-exchange.ts +281 -0
  146. package/src/combos/jev.ts +34 -219
  147. package/src/combos/types.ts +1 -1
  148. package/src/config/atomic-write.ts +1 -1
  149. package/src/config/codex-ws-reuse-setting.ts +10 -0
  150. package/src/config/diagnostics.ts +27 -16
  151. package/src/config/live-reconcile.ts +17 -0
  152. package/src/config/load-degrade.ts +42 -0
  153. package/src/config/rebase-provenance.ts +11 -0
  154. package/src/config/schema/anthropic-account-pool.ts +143 -0
  155. package/src/config/schema/config-schema.ts +11 -1
  156. package/src/config/schema/leaf-validators.ts +4 -0
  157. package/src/config.ts +2 -0
  158. package/src/generated/compatibility-version.json +566 -298
  159. package/src/generated/model-metadata.ts +7 -6
  160. package/src/grok/inject.ts +50 -4
  161. package/src/grok/sync.ts +2 -1
  162. package/src/images/loop.ts +30 -11
  163. package/src/integrations/aside-profiles.ts +10 -3
  164. package/src/integrations/catalog-refresh.ts +19 -3
  165. package/src/integrations/mutation-plan.ts +7 -0
  166. package/src/integrations/registry.ts +8 -0
  167. package/src/integrations/state.ts +9 -2
  168. package/src/integrations/writer.ts +17 -5
  169. package/src/lib/bun-path-runtime.d.mts +32 -0
  170. package/src/lib/bun-path-runtime.mjs +95 -0
  171. package/src/lib/bun-runtime-preflight.ts +140 -0
  172. package/src/lib/desktop-cli-handoff.d.mts +20 -0
  173. package/src/lib/desktop-cli-handoff.mjs +92 -0
  174. package/src/lib/desktop-cli-record.d.mts +29 -0
  175. package/src/lib/desktop-cli-record.mjs +102 -0
  176. package/src/lib/lab-live-route-production.ts +9 -1
  177. package/src/lib/request-execution-budget.ts +244 -58
  178. package/src/lib/spend-pool-alias-validation.ts +79 -0
  179. package/src/lib/spend-pool-continuity.ts +34 -0
  180. package/src/lib/spend-reservation-ledger.ts +412 -50
  181. package/src/lib/state-store-registrations.ts +20 -3
  182. package/src/lib/synced-state-location.ts +46 -6
  183. package/src/lib/test-home-guard.ts +87 -1
  184. package/src/lib/translator-budget.ts +17 -4
  185. package/src/lib/upstream-retry.ts +17 -5
  186. package/src/lib/windows-atomic-replace.ts +38 -16
  187. package/src/lib/windows-elevation.ts +6 -0
  188. package/src/lib/windows-secret-acl.ts +92 -25
  189. package/src/lib/winsw.ts +2 -3
  190. package/src/lib/workflow-budget.ts +53 -11
  191. package/src/messaging/budget.ts +45 -0
  192. package/src/messaging/discovery.ts +57 -0
  193. package/src/messaging/envelope.ts +49 -0
  194. package/src/messaging/input.ts +37 -0
  195. package/src/messaging/rpc.ts +196 -0
  196. package/src/messaging/send.ts +58 -0
  197. package/src/messaging/socket.ts +72 -0
  198. package/src/messaging/types.ts +29 -0
  199. package/src/oauth/anthropic-account-refusal.ts +90 -18
  200. package/src/oauth/anthropic-account-threshold.ts +8 -2
  201. package/src/oauth/anthropic-continuity.ts +15 -1
  202. package/src/oauth/anthropic-model-quota.ts +120 -48
  203. package/src/oauth/anthropic-model-routes.ts +11 -3
  204. package/src/oauth/anthropic-oauth-definitions.ts +98 -0
  205. package/src/oauth/anthropic-pool-config.ts +30 -0
  206. package/src/oauth/anthropic-rate-limit-policy.ts +64 -9
  207. package/src/oauth/anthropic-routing.ts +1202 -866
  208. package/src/oauth/anthropic-send-ownership.ts +41 -0
  209. package/src/oauth/anthropic.ts +4 -1
  210. package/src/oauth/chatgpt.ts +113 -41
  211. package/src/oauth/generic-account-failover.ts +3 -1
  212. package/src/oauth/health.ts +8 -6
  213. package/src/oauth/index.ts +96 -175
  214. package/src/oauth/kiro.ts +22 -9
  215. package/src/oauth/login-cli.ts +2 -0
  216. package/src/oauth/model-discovery-auth.ts +43 -0
  217. package/src/oauth/pool-kernel.ts +11 -5
  218. package/src/oauth/pool-settings-capability.ts +5 -3
  219. package/src/oauth/store-anthropic-instance.ts +87 -0
  220. package/src/oauth/store.ts +68 -10
  221. package/src/oauth/token-guardian.ts +2 -0
  222. package/src/oauth/types.ts +2 -0
  223. package/src/protocols/plan-snapshot.ts +5 -0
  224. package/src/protocols/settings.ts +20 -5
  225. package/src/providers/anthropic-instance-id.ts +47 -0
  226. package/src/providers/anthropic-instance.ts +48 -0
  227. package/src/providers/anthropic-reset-grant-ledger.ts +11 -1
  228. package/src/providers/azure-model-metadata.ts +115 -0
  229. package/src/providers/derive.ts +23 -0
  230. package/src/providers/kiro-models.ts +3 -0
  231. package/src/providers/label.ts +1 -1
  232. package/src/providers/model-rename-fields.ts +2 -0
  233. package/src/providers/quota/account-cache.ts +37 -11
  234. package/src/providers/quota/anthropic-account-quota.ts +93 -0
  235. package/src/providers/quota/anthropic-cooldown-recovery.ts +222 -111
  236. package/src/providers/quota/report-cache.ts +24 -1
  237. package/src/providers/quota/vendor-probes-key.ts +127 -17
  238. package/src/providers/quota/vendor-probes-oauth.ts +46 -14
  239. package/src/providers/quota.ts +23 -33
  240. package/src/providers/registry/entries-core.ts +35 -30
  241. package/src/providers/registry/model-ids.ts +1 -0
  242. package/src/providers/registry/model-seeds.ts +7 -4
  243. package/src/providers/registry/types.ts +3 -0
  244. package/src/providers/registry.ts +3 -3
  245. package/src/responses/reasoning-envelope.ts +29 -1
  246. package/src/responses/reasoning-replay-cache.ts +17 -1
  247. package/src/responses/spill-store.ts +20 -3
  248. package/src/responses/state/metrics.ts +95 -0
  249. package/src/responses/state/snapshot-codec.ts +6 -0
  250. package/src/responses/state/spill-failure.ts +12 -0
  251. package/src/responses/state/spill-queue.ts +13 -5
  252. package/src/responses/state/unforced-store-false.ts +55 -0
  253. package/src/responses/state.ts +68 -76
  254. package/src/responses/task-input.ts +17 -0
  255. package/src/router.ts +28 -4
  256. package/src/routing/compatibility/assemble.ts +1 -1
  257. package/src/routing/quota.ts +7 -3
  258. package/src/server/auth-cors.ts +5 -0
  259. package/src/server/background-lifecycle.ts +27 -0
  260. package/src/server/chat-completions.ts +7 -1
  261. package/src/server/chat-native.ts +64 -28
  262. package/src/server/claude-messages.ts +59 -8
  263. package/src/server/gui-static.ts +38 -4
  264. package/src/server/inbound-body-admission.ts +1 -0
  265. package/src/server/index/claude-intercept-lifecycle.ts +3 -1
  266. package/src/server/index/serve-options.ts +10 -2
  267. package/src/server/index/spend-ledger-lifecycle.ts +12 -5
  268. package/src/server/index/startup-warnings.ts +19 -0
  269. package/src/server/index.ts +5 -2
  270. package/src/server/inference/context.ts +1 -1
  271. package/src/server/lifecycle.ts +1 -1
  272. package/src/server/management/agent-settings-routes.ts +123 -38
  273. package/src/server/management/anthropic-account-threshold.ts +12 -9
  274. package/src/server/management/anthropic-pool-settings.ts +72 -0
  275. package/src/server/management/anthropic-reset-grant-routes.ts +98 -39
  276. package/src/server/management/codex-agent-role-routes.ts +4 -2
  277. package/src/server/management/codex-prompt-routes.ts +1 -0
  278. package/src/server/management/codex-role-auto-assign.ts +13 -2
  279. package/src/server/management/config-routes.ts +85 -14
  280. package/src/server/management/context.ts +2 -2
  281. package/src/server/management/decision-routes.ts +2 -1
  282. package/src/server/management/integration-routes.ts +25 -2
  283. package/src/server/management/oauth-account-routes.ts +79 -98
  284. package/src/server/management/provider-instance-ownership.ts +44 -0
  285. package/src/server/management/provider-routes.ts +21 -28
  286. package/src/server/management/route-registry.ts +3 -1
  287. package/src/server/management/routing-profile-routes.ts +4 -4
  288. package/src/server/management/shared.ts +3 -3
  289. package/src/server/management/system-restart.ts +44 -14
  290. package/src/server/management/system-routes.ts +24 -2
  291. package/src/server/management/vision-sidecar-options.ts +9 -6
  292. package/src/server/management/web-search-sidecar-options.ts +51 -2
  293. package/src/server/management-auth.ts +6 -0
  294. package/src/server/messages-native-eligibility.ts +3 -2
  295. package/src/server/messages-native-oauth.ts +91 -45
  296. package/src/server/messages-native-selector.ts +39 -0
  297. package/src/server/messages-native.ts +133 -38
  298. package/src/server/messages-response-headers.ts +14 -0
  299. package/src/server/request-log-conversation.ts +48 -5
  300. package/src/server/request-log.ts +36 -2
  301. package/src/server/responses/adapter-continuation.ts +31 -19
  302. package/src/server/responses/adapter-dispatch.ts +75 -24
  303. package/src/server/responses/antigravity-validation-refusal.ts +20 -6
  304. package/src/server/responses/codex-ws-exchange.ts +17 -2
  305. package/src/server/responses/codex-ws-pool.ts +100 -13
  306. package/src/server/responses/codex-ws-wire.ts +9 -0
  307. package/src/server/responses/combo-requested-effort.ts +3 -1
  308. package/src/server/responses/compact.ts +25 -8
  309. package/src/server/responses/compaction-recovery.ts +1 -1
  310. package/src/server/responses/core-codex-account.ts +22 -1
  311. package/src/server/responses/core-combo-native.ts +24 -4
  312. package/src/server/responses/core-combo.ts +38 -25
  313. package/src/server/responses/core-lifetime.ts +3 -36
  314. package/src/server/responses/core-normalize.ts +1 -0
  315. package/src/server/responses/core-options.ts +8 -1
  316. package/src/server/responses/core-replay.ts +78 -0
  317. package/src/server/responses/core.ts +3 -3
  318. package/src/server/responses/fetch-helpers.ts +26 -3
  319. package/src/server/responses/passthrough-delivery.ts +30 -4
  320. package/src/server/responses/passthrough-dispatch.ts +39 -9
  321. package/src/server/responses/request-prepare.ts +41 -5
  322. package/src/server/responses/request-send-budget.ts +150 -50
  323. package/src/server/responses/request-spend.ts +325 -33
  324. package/src/server/responses/request-transport.ts +132 -52
  325. package/src/server/responses/response-effects.ts +1 -0
  326. package/src/server/responses/run-turn-execution.ts +13 -3
  327. package/src/server/responses/sidecar-execution.ts +113 -22
  328. package/src/server/responses/sidecar-send-budget.ts +58 -0
  329. package/src/server/responses/ws-upstream.ts +19 -6
  330. package/src/server/responses-undeclared-tool-guard.ts +5 -1
  331. package/src/server/startup-health-cache.ts +2 -1
  332. package/src/server/workflow-refusal.ts +33 -0
  333. package/src/service/cli.ts +20 -4
  334. package/src/service/desktop-command-guard.ts +32 -0
  335. package/src/service/desktop-startup.ts +66 -40
  336. package/src/service/desktop-supervision.d.mts +26 -0
  337. package/src/service/desktop-supervision.mjs +140 -0
  338. package/src/service/managing-cli.ts +1 -0
  339. package/src/service/orchestration.ts +46 -20
  340. package/src/service/ownership-mutation-lease.d.mts +1 -1
  341. package/src/service/ownership-mutation-lease.mjs +5 -7
  342. package/src/service/repair.ts +25 -9
  343. package/src/service/state.ts +4 -3
  344. package/src/service/systemd.ts +17 -8
  345. package/src/service/windows-ops.ts +61 -22
  346. package/src/sidecar/anthropic-binding.ts +79 -0
  347. package/src/sidecar/auth.ts +76 -2
  348. package/src/storage/policy-job.ts +5 -1
  349. package/src/storage/policy-worker.ts +3 -1
  350. package/src/storage/policy.ts +9 -0
  351. package/src/types/anthropic-account-pool.ts +33 -0
  352. package/src/types/config.ts +33 -28
  353. package/src/types/provider.ts +5 -0
  354. package/src/types/request.ts +5 -0
  355. package/src/types/tools.ts +7 -0
  356. package/src/types/wire.ts +1 -0
  357. package/src/types.ts +4 -0
  358. package/src/update/index.ts +57 -9
  359. package/src/update/restart-ownership.ts +10 -3
  360. package/src/update/runtime-ownership.d.mts +6 -0
  361. package/src/update/runtime-ownership.mjs +23 -7
  362. package/src/usage/cost.ts +3 -2
  363. package/src/usage/expected-prices.ts +66 -14
  364. package/src/vision/anthropic-describe.ts +16 -3
  365. package/src/vision/index.ts +3 -0
  366. package/src/vision/plan.ts +19 -6
  367. package/src/web-search/alpha-search.ts +7 -1
  368. package/src/web-search/anthropic-executor.ts +16 -4
  369. package/src/web-search/index.ts +7 -3
  370. package/src/web-search/loop.ts +32 -7
  371. package/src/web-search/passthrough-bridge.ts +4 -1
  372. package/src/web-search/sidecar-providers.ts +4 -3
  373. package/gui/dist/assets/App-MbDVfUeC.js +0 -54
  374. package/gui/dist/assets/Tray-CzkxK82A.js +0 -1
  375. package/gui/dist/assets/index-UJt6npXk.js +0 -94
  376. package/gui/dist/assets/quota-summary-DU3sD6Z-.js +0 -1
@@ -39,6 +39,10 @@ import {
39
39
  parsePersistedCodexRuntime,
40
40
  peekCodexRuntimeProcessCache,
41
41
  resolveAndPersistCodexRuntime,
42
+ resolveAndPersistCodexRuntimeAsync,
43
+ codexRuntimeSelectionIdentity,
44
+ codexRuntimeStateEpoch,
45
+ execCodexFileAsync,
42
46
  } from "../runtime";
43
47
  import type {
44
48
  DeepReadonly,
@@ -63,7 +67,9 @@ export type ReadonlyRawCatalog = DeepReadonly<RawCatalog>;
63
67
  interface BundledCatalogMemo {
64
68
  /** Selected runtime identity; must change when doctor/sync picks a different binary. */
65
69
  readonly key: string;
70
+ readonly inputs: string;
66
71
  readonly expiresAt: number;
72
+ readonly refreshAt: number;
67
73
  readonly epoch: number;
68
74
  readonly valueIdentity: string;
69
75
  readonly value: ReadonlyRawCatalog | null;
@@ -76,6 +82,9 @@ export interface BundledCatalogCacheState {
76
82
 
77
83
  let bundledCatalogEpoch = 0;
78
84
  let bundledCatalogCache: BundledCatalogMemo | null = null;
85
+ let bundledCatalogRetry: { inputs: string; epoch: number; after: number } | null = null;
86
+ let bundledCatalogFlight: { inputs: string; epoch: number; controller: AbortController; promise: Promise<ReadonlyRawCatalog | null> } | null = null;
87
+ const catalogAlwaysCurrent = () => true;
79
88
 
80
89
  function cloneAndDeepFreeze<T>(value: T): DeepReadonly<T> {
81
90
  const clone = (current: unknown): unknown => {
@@ -110,14 +119,18 @@ function publishBundledCatalogCache(
110
119
  const epoch = ++bundledCatalogEpoch;
111
120
  bundledCatalogCache = {
112
121
  key,
122
+ inputs: codexRuntimeSelectionIdentity({ discoverAlternatives: false }),
113
123
  expiresAt,
124
+ refreshAt: Math.min(expiresAt, Date.now() + 15_000),
114
125
  epoch,
115
126
  valueIdentity: `bundled:${epoch}`,
116
127
  value: value === null ? null : cloneAndDeepFreeze(value),
117
128
  };
129
+ bundledCatalogRetry = null;
118
130
  }
119
131
 
120
132
  function clearBundledCatalogCache(): void {
133
+ bundledCatalogFlight?.controller.abort();
121
134
  bundledCatalogEpoch += 1;
122
135
  bundledCatalogCache = null;
123
136
  }
@@ -162,12 +175,17 @@ export type ExecFile = (
162
175
  windowsHide: boolean;
163
176
  shell?: boolean;
164
177
  windowsVerbatimArguments?: boolean;
178
+ signal?: AbortSignal;
179
+ maxBuffer?: number;
165
180
  },
166
181
  ) => string;
167
182
 
168
183
  export interface BundledCatalogDeps {
184
+ /** Already-observed source for server clamping; null never triggers a probe. */
185
+ observedCatalog?: ReadonlyRawCatalog | null;
169
186
  commandCandidates?: () => string[];
170
187
  execFileSync?: ExecFile;
188
+ execFile?: (...args: Parameters<ExecFile>) => Promise<string>;
171
189
  onEffortClamp?: (diagnostic: EffortClampDiagnostic) => void;
172
190
  configDir?: string;
173
191
  env?: NodeJS.ProcessEnv;
@@ -224,11 +242,11 @@ export function codexShimCommandCandidates(): string[] {
224
242
  }
225
243
  }
226
244
 
227
- export function runCodexDebugModels(
245
+ export function runCodexDebugModels<T>(
228
246
  command: string,
229
- execFile: ExecFile,
247
+ execFile: (...args: Parameters<ExecFile>) => T,
230
248
  deps: Pick<BundledCatalogDeps, "env" | "platform" | "existsSync"> = {},
231
- ): string {
249
+ ): T {
232
250
  const args = ["debug", "models", "--bundled"];
233
251
  const invocation = codexExecInvocation(command, args, deps.platform ?? process.platform, {
234
252
  env: deps.env,
@@ -238,23 +256,19 @@ export function runCodexDebugModels(
238
256
  encoding: "utf8" as const,
239
257
  stdio: ["ignore", "pipe", "ignore"] as ["ignore", "pipe", "ignore"],
240
258
  timeout: 10_000,
259
+ maxBuffer: 64 * 1024 * 1024,
241
260
  windowsHide: true,
242
261
  ...invocation.options,
243
262
  });
244
263
  }
245
264
 
246
- export function loadBundledCodexCatalog(deps: BundledCatalogDeps = {}): ReadonlyRawCatalog | null {
247
- const useCache = !deps.commandCandidates && !deps.execFileSync && !deps.configDir && !deps.env;
248
- const execFile = deps.execFileSync ?? (execFileSync as unknown as ExecFile);
249
- // Prefer the single resolved runtime so sync/clamp never probe a different binary
250
- // than OpenCodex will launch. Tests may inject commandCandidates to stub probing.
251
- let cacheKey: string | null = null;
252
- const candidates = deps.commandCandidates?.() ?? (() => {
253
- const resolved = resolveAndPersistCodexRuntime({
265
+ function bundledRuntimeDeps(deps: BundledCatalogDeps) {
266
+ return {
254
267
  // Forward an INJECTED execFileSync only. Passing the real one unconditionally made
255
268
  // resolveCacheKey() bail out (it refuses to memoize injected-dep resolves), so every
256
269
  // catalog read re-ran the ~1s `codex --version` probe even on a warm cache hit.
257
270
  ...(deps.execFileSync ? { execFileSync: deps.execFileSync } : {}),
271
+ ...(deps.execFile ? { execFile: deps.execFile } : {}),
258
272
  configDir: deps.configDir,
259
273
  env: deps.env,
260
274
  platform: deps.platform,
@@ -266,28 +280,41 @@ export function loadBundledCodexCatalog(deps: BundledCatalogDeps = {}): Readonly
266
280
  // which alone can exceed the 3s budget `ocx claude` allows /api/claude-code. Priority
267
281
  // selection is identical either way; callers wanting discovery diagnostics opt in.
268
282
  discoverAlternatives: deps.discoverAlternatives ?? false,
269
- });
270
- if (useCache) {
271
- cacheKey = bundledRuntimeKey(resolved.runtime);
272
- }
273
- return [resolved.runtime.command];
274
- })();
283
+ };
284
+ }
285
+
286
+ function* bundledCatalogSteps(
287
+ candidates: string[],
288
+ cacheKey: string | null,
289
+ current: () => boolean = () => true,
290
+ publishFailure = true,
291
+ ): Generator<string, ReadonlyRawCatalog | null, string | null> {
292
+ if (cacheKey && bundledCatalogCache?.key === cacheKey && current()) {
293
+ // The driver validated this same command/version under the new pin/env inputs.
294
+ // Re-stamp the memo even if its TTL is due, so failed refresh keeps confirmed rows.
295
+ bundledCatalogCache = { ...bundledCatalogCache,
296
+ inputs: codexRuntimeSelectionIdentity({ discoverAlternatives: false }) };
297
+ }
275
298
  if (
276
- useCache
277
- && cacheKey
299
+ cacheKey
278
300
  && bundledCatalogCache
279
301
  && bundledCatalogCache.key === cacheKey
302
+ && bundledCatalogCache.inputs === codexRuntimeSelectionIdentity({ discoverAlternatives: false })
280
303
  && bundledCatalogCache.expiresAt > Date.now()
281
304
  ) {
305
+ if (!current()) return null;
306
+ bundledCatalogCache = { ...bundledCatalogCache, refreshAt: Date.now() + 15_000 };
282
307
  return bundledCatalogCache.value === null
283
308
  ? null
284
309
  : cloneAndDeepFreeze(bundledCatalogCache.value);
285
310
  }
286
311
  for (const command of unique(candidates)) {
287
- try {
288
- const catalog = parseCatalogJson(runCodexDebugModels(command, execFile, deps));
312
+ if (!current()) return null;
313
+ const output = yield command;
314
+ if (!current()) return null;
315
+ const catalog = output === null ? null : parseCatalogJson(output);
289
316
  if (catalog && findNativeTemplate(catalog)) {
290
- if (useCache && cacheKey) {
317
+ if (cacheKey) {
291
318
  publishBundledCatalogCache(
292
319
  cacheKey,
293
320
  Date.now() + BUNDLED_CATALOG_CACHE_MS,
@@ -297,9 +324,8 @@ export function loadBundledCodexCatalog(deps: BundledCatalogDeps = {}): Readonly
297
324
  }
298
325
  return cloneAndDeepFreeze(catalog);
299
326
  }
300
- } catch { /* try next candidate */ }
301
327
  }
302
- if (useCache && cacheKey) {
328
+ if (cacheKey && (publishFailure || bundledCatalogCache?.key !== cacheKey || !bundledCatalogCache?.value) && current()) {
303
329
  publishBundledCatalogCache(
304
330
  cacheKey,
305
331
  Date.now() + BUNDLED_CATALOG_CACHE_MS,
@@ -309,6 +335,109 @@ export function loadBundledCodexCatalog(deps: BundledCatalogDeps = {}): Readonly
309
335
  return null;
310
336
  }
311
337
 
338
+ /** Explicit synchronous CLI driver; request readers use the snapshot below. */
339
+ export function loadBundledCodexCatalog(deps: BundledCatalogDeps = {}): ReadonlyRawCatalog | null {
340
+ const useCache = !deps.commandCandidates && !deps.execFileSync && !deps.execFile && !deps.configDir && !deps.env;
341
+ const resolved = deps.commandCandidates ? null : resolveAndPersistCodexRuntime(bundledRuntimeDeps(deps));
342
+ const inputs = codexRuntimeSelectionIdentity(bundledRuntimeDeps(deps));
343
+ const steps = bundledCatalogSteps(deps.commandCandidates?.() ?? [resolved!.runtime.command],
344
+ useCache ? bundledRuntimeKey(resolved!.runtime) : null,
345
+ () => inputs === codexRuntimeSelectionIdentity(bundledRuntimeDeps(deps)));
346
+ let step = steps.next();
347
+ while (!step.done) {
348
+ let output: string | null = null;
349
+ try { output = runCodexDebugModels(step.value, deps.execFileSync ?? (execFileSync as unknown as ExecFile), deps); }
350
+ catch { /* try next candidate */ }
351
+ step = steps.next(output);
352
+ }
353
+ return step.value;
354
+ }
355
+
356
+ /** Same candidate/parser authority, with version and bundled-model execs off the event loop. */
357
+ export function loadBundledCodexCatalogAsync(
358
+ deps: BundledCatalogDeps = {},
359
+ isCurrent: () => boolean = catalogAlwaysCurrent,
360
+ signal?: AbortSignal,
361
+ ): Promise<ReadonlyRawCatalog | null> {
362
+ if (signal?.aborted || !isCurrent()) return Promise.resolve(null);
363
+ const view = (promise: Promise<ReadonlyRawCatalog | null>) => {
364
+ if (!signal) return promise.then(value => isCurrent() ? value : null);
365
+ let abort: (() => void) | undefined;
366
+ return Promise.race([promise, new Promise<null>(resolve => {
367
+ abort = () => resolve(null);
368
+ signal.addEventListener("abort", abort, { once: true });
369
+ if (signal.aborted) abort();
370
+ })]).then(value => isCurrent() && !signal.aborted ? value : null)
371
+ .finally(() => { if (abort) signal.removeEventListener("abort", abort); });
372
+ };
373
+ const runtimeDeps = bundledRuntimeDeps(deps);
374
+ const inputs = codexRuntimeSelectionIdentity(runtimeDeps);
375
+ const epoch = bundledCatalogEpoch;
376
+ const useCache = !deps.commandCandidates && !deps.execFileSync && !deps.execFile && !deps.configDir && !deps.env;
377
+ if (useCache && bundledCatalogFlight?.inputs === inputs && bundledCatalogFlight.epoch === epoch) {
378
+ return view(bundledCatalogFlight.promise);
379
+ }
380
+ const controller = new AbortController();
381
+ const flight = { inputs, epoch, controller, promise: Promise.resolve<ReadonlyRawCatalog | null>(null) };
382
+ if (useCache && !signal && isCurrent === catalogAlwaysCurrent) {
383
+ bundledCatalogFlight?.controller.abort();
384
+ bundledCatalogFlight = flight;
385
+ }
386
+ const abort = () => controller.abort();
387
+ signal?.addEventListener("abort", abort, { once: true });
388
+ if (signal?.aborted) abort();
389
+ let timer: ReturnType<typeof setTimeout> | undefined;
390
+ let deadlineReached = false;
391
+ const work = (async () => {
392
+ const active = () => !controller.signal.aborted && isCurrent() && bundledCatalogEpoch === epoch;
393
+ const resolved = deps.commandCandidates ? null : await resolveAndPersistCodexRuntimeAsync({ ...runtimeDeps, signal: controller.signal },
394
+ () => active() && codexRuntimeSelectionIdentity(runtimeDeps) === inputs);
395
+ if (!active() || (!deps.commandCandidates && !resolved)) return null;
396
+ flight.inputs = codexRuntimeSelectionIdentity(runtimeDeps);
397
+ const runtimeEpoch = codexRuntimeStateEpoch();
398
+ const current = () => active() && runtimeEpoch === codexRuntimeStateEpoch()
399
+ && flight.inputs === codexRuntimeSelectionIdentity(runtimeDeps);
400
+ const steps = bundledCatalogSteps(deps.commandCandidates?.() ?? [resolved!.runtime.command],
401
+ useCache ? bundledRuntimeKey(resolved!.runtime) : null, current, false);
402
+ let step = steps.next();
403
+ while (!step.done) {
404
+ let output: string | null = null;
405
+ try { output = await runCodexDebugModels(step.value,
406
+ (file, args, options) => (deps.execFile ?? execCodexFileAsync)(file, args, { ...options, signal: controller.signal }), deps); }
407
+ catch { /* preserve last confirmed evidence on failure */ }
408
+ step = steps.next(output);
409
+ }
410
+ return step.value;
411
+ })().catch(() => null);
412
+ flight.promise = Promise.race([work, new Promise<null>(resolve => {
413
+ timer = setTimeout(() => { deadlineReached = true; controller.abort(); resolve(null); }, 45_000);
414
+ timer.unref?.();
415
+ })]).then(value => {
416
+ if (useCache && value === null && (!controller.signal.aborted || deadlineReached)
417
+ && isCurrent() && bundledCatalogEpoch === epoch
418
+ && flight.inputs === codexRuntimeSelectionIdentity(runtimeDeps)) {
419
+ bundledCatalogRetry = { inputs: flight.inputs, epoch, after: Date.now() + BUNDLED_CATALOG_CACHE_MS };
420
+ }
421
+ return value;
422
+ }).finally(() => {
423
+ clearTimeout(timer);
424
+ signal?.removeEventListener("abort", abort);
425
+ controller.abort();
426
+ if (bundledCatalogFlight === flight) bundledCatalogFlight = null;
427
+ });
428
+ return view(flight.promise);
429
+ }
430
+
431
+ /** Serve only the same selection's last confirmed catalog; refresh never holds a request. */
432
+ export function bundledCodexCatalogSnapshot(): ReadonlyRawCatalog | null {
433
+ const inputs = codexRuntimeSelectionIdentity({ discoverAlternatives: false });
434
+ const memo = bundledCatalogCache?.inputs === inputs ? bundledCatalogCache : null;
435
+ const cooling = bundledCatalogRetry?.inputs === inputs && bundledCatalogRetry.epoch === bundledCatalogEpoch
436
+ && bundledCatalogRetry.after > Date.now();
437
+ if ((!memo || memo.refreshAt <= Date.now()) && !cooling) void loadBundledCodexCatalogAsync().catch(() => { /* bounded retry */ });
438
+ return memo?.value ? cloneAndDeepFreeze(memo.value) : null;
439
+ }
440
+
312
441
  export type CatalogGatherProcessLocalObservation =
313
442
  | Readonly<{ state: "unused" }>
314
443
  | Readonly<{ state: "used"; epoch: number; valueIdentity: string }>;
@@ -524,7 +653,7 @@ export function loadCatalogForSync(path: string): RawCatalog | null {
524
653
 
525
654
  export function readCurrentCatalogOrCache(): RawCatalog | null {
526
655
  const path = readCodexCatalogPath();
527
- const bundled = isDefaultCatalogPath(path) ? loadBundledCodexCatalog() : null;
656
+ const bundled = isDefaultCatalogPath(path) ? bundledCodexCatalogSnapshot() : null;
528
657
  if (bundled) return JSON.parse(JSON.stringify(bundled)) as RawCatalog;
529
658
  return readCatalog(path) ?? readCatalog(activeCodexModelsCachePath());
530
659
  }
@@ -547,7 +676,7 @@ export function readCurrentCodexModelsCache(): RawCatalog | null {
547
676
 
548
677
  export function loadCatalogTemplate(): RawEntry | null {
549
678
  const catalogPath = readCodexCatalogPath();
550
- const bundled = loadBundledCodexCatalog();
679
+ const bundled = bundledCodexCatalogSnapshot();
551
680
  // Template inheritance only. The validity gates in this file keep `findNativeTemplate`
552
681
  // so a catalog carrying only a newly launched native row stays valid (#2813).
553
682
  const native = findSupportedNativeTemplate(readCatalog(catalogPath))
@@ -218,6 +218,7 @@ export function resolveComboCatalogMember(
218
218
  contextCap?: number,
219
219
  callerFallback?: ComboCatalogMemberFallback,
220
220
  metadataModelIdCaseFold?: boolean,
221
+ metadataConfigDir?: string,
221
222
  ): CatalogModel | undefined {
222
223
  const existing = memberByKey.get(targetKey(target));
223
224
  const prov = providers.get(target.provider);
@@ -275,7 +276,7 @@ export function resolveComboCatalogMember(
275
276
  // supplies a vision sidecar. Apply the same provider hints used for thin
276
277
  // rows before deriving a combo from this complete row.
277
278
  const hinted = prov && isModelVisionSidecarConsumer(prov, existing.id)
278
- ? applyProviderConfigHints(target.provider, prov, existing, contextCap, metadataModelIdCaseFold)
279
+ ? applyProviderConfigHints(target.provider, prov, existing, contextCap, metadataModelIdCaseFold, undefined, metadataConfigDir)
279
280
  : existing;
280
281
  const capped = applyProviderContextCap(hinted.contextWindow, contextCap);
281
282
  if (capped === undefined || capped === existing.contextWindow) {
@@ -298,7 +299,7 @@ export function resolveComboCatalogMember(
298
299
  provider: target.provider,
299
300
  };
300
301
  const hinted = prov
301
- ? applyProviderConfigHints(target.provider, prov, base, contextCap, metadataModelIdCaseFold)
302
+ ? applyProviderConfigHints(target.provider, prov, base, contextCap, metadataModelIdCaseFold, undefined, metadataConfigDir)
302
303
  : base;
303
304
  const hintedContext = typeof hinted.contextWindow === "number" && hinted.contextWindow > 0
304
305
  ? hinted.contextWindow
@@ -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";
@@ -45,6 +47,7 @@ import {
45
47
  displayCodexRuntimePath,
46
48
  persistEffortClamp,
47
49
  resolveAndPersistCodexRuntime,
50
+ getCodexRuntimeSnapshot,
48
51
  UNCLAMPABLE_REASONING_EFFORTS,
49
52
  type EffortClampDiagnostic,
50
53
  } from "../runtime";
@@ -84,8 +87,14 @@ export function shouldApplyNativeEffortClamp(
84
87
  && isCanonicalOpenAiForwardProvider(provider);
85
88
  }
86
89
 
87
- export function catalogModelEfforts(slugs: readonly string[]): Map<string, string[]> {
88
- const out = new Map<string, string[]>();
90
+ export interface CatalogModelLadder {
91
+ readonly efforts: string[];
92
+ readonly defaultEffort?: string;
93
+ }
94
+
95
+ /** Each slug's reasoning ladder and default as the written Codex catalog offers them. */
96
+ export function catalogModelLadders(slugs: readonly string[]): Map<string, CatalogModelLadder> {
97
+ const out = new Map<string, CatalogModelLadder>();
89
98
  if (slugs.length === 0) return out;
90
99
  const catalog = readCatalog(readCodexCatalogPath());
91
100
  if (!catalog) return out;
@@ -99,11 +108,19 @@ export function catalogModelEfforts(slugs: readonly string[]): Map<string, strin
99
108
  ? entry.supported_reasoning_levels as Array<{ effort?: string }>
100
109
  : [];
101
110
  const efforts = levels.flatMap(l => typeof l.effort === "string" ? [l.effort] : []);
102
- if (efforts.length > 0) out.set(callerSlug, efforts);
111
+ if (efforts.length === 0) continue;
112
+ const defaultEffort = typeof entry.default_reasoning_level === "string" && efforts.includes(entry.default_reasoning_level)
113
+ ? entry.default_reasoning_level
114
+ : undefined;
115
+ out.set(callerSlug, defaultEffort === undefined ? { efforts } : { efforts, defaultEffort });
103
116
  }
104
117
  return out;
105
118
  }
106
119
 
120
+ export function catalogModelEfforts(slugs: readonly string[]): Map<string, string[]> {
121
+ return new Map([...catalogModelLadders(slugs)].map(([slug, ladder]) => [slug, ladder.efforts]));
122
+ }
123
+
107
124
  export function catalogEntryEfforts(entry: RawEntry): string[] {
108
125
  const levels = Array.isArray(entry.supported_reasoning_levels)
109
126
  ? entry.supported_reasoning_levels as Array<{ effort?: string }>
@@ -364,7 +381,9 @@ export function supportedCodexReasoningEffortsFromObservedCatalog(
364
381
  }
365
382
 
366
383
  export function codexSupportedReasoningEfforts(deps: BundledCatalogDeps = {}): ReadonlySet<string> | null {
367
- return supportedCodexReasoningEffortsFromObservedCatalog(loadBundledCodexCatalog(deps));
384
+ return supportedCodexReasoningEffortsFromObservedCatalog(
385
+ deps.observedCatalog !== undefined ? deps.observedCatalog : loadBundledCodexCatalog(deps),
386
+ );
368
387
  }
369
388
 
370
389
  export function clampedDefaultEffort(original: string, surviving: readonly string[]): string {
@@ -385,7 +404,7 @@ export function clampEntryToCodexSupportedEfforts(
385
404
  entry: RawEntry,
386
405
  supported: ReadonlySet<string> | null,
387
406
  ): void {
388
- if (!supported) return;
407
+ if (!supported || isCodexControlPlaneModel(entry.slug)) return;
389
408
  const levels = Array.isArray(entry.supported_reasoning_levels)
390
409
  ? entry.supported_reasoning_levels as Array<{ effort?: string }>
391
410
  : null;
@@ -532,6 +551,14 @@ export function clampCatalogModelsToObservedCodexSupport(
532
551
  else index += 1;
533
552
  }
534
553
 
554
+ // Only Reserve rows are currently omitted above; ordinary natives keep a fallback ladder.
555
+ // Also repair a persisted orphan so a removed Reserve can never leave a reviewer behind.
556
+ if (!hasOrdinaryNativeOpenAiRow(models)) {
557
+ for (let index = models.length - 1; index >= 0; index -= 1) {
558
+ if (isCodexControlPlaneModel(models[index]!.slug)) models.splice(index, 1);
559
+ }
560
+ }
561
+
535
562
  return {
536
563
  removedEfforts: [...removed].sort(),
537
564
  affectedModels: affected,
@@ -548,7 +575,11 @@ export function clampCatalogModelsToCodexSupport(models: RawEntry[], deps: Bundl
548
575
 
549
576
  let runtimePath = "codex";
550
577
  let runtimeVersion: string | null = null;
551
- if (!deps.commandCandidates) {
578
+ if (deps.observedCatalog !== undefined && !deps.commandCandidates) {
579
+ const snapshot = getCodexRuntimeSnapshot();
580
+ runtimePath = snapshot.runtime.command;
581
+ runtimeVersion = snapshot.runtime.version;
582
+ } else if (!deps.commandCandidates) {
552
583
  try {
553
584
  const resolved = resolveAndPersistCodexRuntime({
554
585
  execFileSync: deps.execFileSync,
@@ -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";
@@ -147,6 +148,8 @@ export interface CapturedModelsRequest {
147
148
  }
148
149
 
149
150
  export interface CapturedProviderGather {
151
+ /** Public metadata reads and refreshes stay bound to this admission root. */
152
+ readonly metadataConfigDir: string;
150
153
  readonly name: string;
151
154
  readonly provider: OcxProviderConfig;
152
155
  readonly discovery: ResolvedProviderModelDiscovery;
@@ -353,6 +356,7 @@ export function captureProviderGather(
353
356
  retainConfiguredModelIds?: ReadonlySet<string>,
354
357
  config?: Pick<OcxConfig, "providers">,
355
358
  ): CapturedProviderGather {
359
+ const metadataConfigDir = getConfigDir();
356
360
  const enriched = detachedClone(withCanonicalOpenAiForwardAuthDefault(name, configured));
357
361
  enrichProviderFromRegistry(name, enriched);
358
362
  const registryTransportMatch = providerMatchesRegistryTransport(name, enriched);
@@ -389,15 +393,15 @@ export function captureProviderGather(
389
393
  ? authResolver.resolve(name, provider)
390
394
  : undefined;
391
395
  // 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
396
+ // flights. Host-scoped auth rebuilds its request; B retains its configured captured target. Observed capture never
393
397
  // reads the live store.
394
398
  const oauthApiBaseUrl = observedAuth
395
399
  ? observedAuth.oauthApiBaseUrl
396
- : authResolver.kind === "refreshing" && provider.authMode === "oauth"
400
+ : authResolver.kind === "refreshing" && provider.authMode === "oauth" && mayResolveModelsOAuth(name, provider)
397
401
  ? getOAuthCredentialApiBaseUrl(name)
398
402
  : undefined;
399
403
  const request = captureModelsRequest(name, provider, oauthApiBaseUrl);
400
- const refreshingOAuthAccountId = !observedAuth && authResolver.kind === "refreshing" && provider.authMode === "oauth"
404
+ const refreshingOAuthAccountId = !observedAuth && authResolver.kind === "refreshing" && provider.authMode === "oauth" && mayResolveModelsOAuth(name, provider)
401
405
  ? getAccountSet(name)?.activeAccountId
402
406
  : undefined;
403
407
  const resolved = resolveProviderModelDiscovery(name, provider);
@@ -427,6 +431,7 @@ export function captureProviderGather(
427
431
  const effectiveAlias = effectiveProviderAliasDecision(name, configured, config);
428
432
  return Object.freeze({
429
433
  name,
434
+ metadataConfigDir,
430
435
  provider,
431
436
  discovery,
432
437
  policy,
@@ -481,6 +486,7 @@ export function captureGatherFlight(
481
486
  providerGraphIdentity: keyedGatherIdentity("catalog-gather-provider-graph-v1",
482
487
  providers.map(provider => ({
483
488
  name: provider.name,
489
+ metadataConfigDir: provider.metadataConfigDir,
484
490
  // `fetch` is a caller-owned transport executor, not admitted state: the
485
491
  // outbound transport honors it so a caller can supply its own HTTP path.
486
492
  // It is the one member of a provider row that is legitimately a function,
@@ -531,6 +537,7 @@ function providerCatalogFingerprint(name: string, prov: OcxProviderConfig): Reco
531
537
  live: prov.liveModels ?? null,
532
538
  base: prov.baseUrl ?? "",
533
539
  adapter: prov.adapter ?? "",
540
+ instance: prov.anthropicOAuthInstance ?? null,
534
541
  models: [...(prov.models ?? [])].sort(),
535
542
  retain: [...(prov.retainModels ?? [])].sort(),
536
543
  selected: [...(prov.selectedModels ?? [])].sort(),
@@ -35,7 +35,7 @@ import { modelInList } from "../../types";
35
35
  import { CODEX_REASONING_LEVELS, codexEffortRank, configuredReasoningEfforts, modelRecordValue, sanitizeCodexReasoningEfforts } from "../../reasoning-effort";
36
36
  import { isModelVisionSidecarConsumer } from "../../vision/eligibility";
37
37
  import { getModelMetadata, getModelMetadataCaseInsensitive, listModelMetadata, resolveMetadataProvider, type ModelMetadata } from "../../generated/model-metadata";
38
- import { enrichProviderFromRegistry, shouldCaseFoldMetadataModelId } from "../../providers/derive";
38
+ import { azureVendorModelMetadata, enrichProviderFromRegistry, shouldCaseFoldMetadataModelId } from "../../providers/derive";
39
39
  import {
40
40
  captureFastPolicyAuthority,
41
41
  fastPolicyForModel,
@@ -217,9 +217,11 @@ export function routedMaxOutputTokens(
217
217
  model: CatalogModel,
218
218
  metadataId = model.id,
219
219
  metadataModelIdCaseFold?: boolean,
220
+ metadataConfigDir?: string,
220
221
  ): number | undefined {
221
222
  const discovered = positiveSafeInteger(model.maxOutputTokens);
222
- const generated = generatedMaxOutputTokens(providerName, model.id, metadataId, metadataModelIdCaseFold);
223
+ const generated = positiveSafeInteger(azureVendorModelMetadata(provider.baseUrl, metadataId, metadataConfigDir)?.maxTokens)
224
+ ?? generatedMaxOutputTokens(providerName, model.id, metadataId, metadataModelIdCaseFold);
223
225
  const configured = positiveSafeInteger(
224
226
  modelRecordValue(provider.modelMaxOutputTokens, model.id),
225
227
  );
@@ -255,6 +257,7 @@ export function applyProviderConfigHints(
255
257
  providerCap?: number,
256
258
  metadataModelIdCaseFold?: boolean,
257
259
  effectiveAlias?: string | null,
260
+ metadataConfigDir?: string,
258
261
  ): CatalogModel {
259
262
  const staticPolicy = resolveModelPolicy({
260
263
  providerName: name,
@@ -276,13 +279,14 @@ export function applyProviderConfigHints(
276
279
  : model.providerAlias;
277
280
  const configuredCap = staticPolicy.model.contextWindow ?? configuredContextWindow(prov, model.id);
278
281
  const configuredMaxInput = staticPolicy.model.maxInputTokens;
279
- const maxOutputTokens = routedMaxOutputTokens(name, prov, model, model.id, metadataModelIdCaseFold);
282
+ const azureMetadata = azureVendorModelMetadata(prov.baseUrl, model.id, metadataConfigDir);
283
+ const maxOutputTokens = routedMaxOutputTokens(name, prov, model, model.id, metadataModelIdCaseFold, metadataConfigDir);
280
284
  const configuredAutoCompact = configuredAutoCompactTokenLimit(prov, model.id);
281
285
  // The resolver owns exact capability precedence and legacy exact/colon-family/case-fold fallback.
282
286
  // Removing an exact capability row therefore returns this projection to legacy-map inference.
283
- let inputModalities = staticPolicy.model.inputModalities
284
- ? [...staticPolicy.model.inputModalities]
285
- : undefined;
287
+ const resolvedModalities = staticPolicy.model.inputModalities ?? model.inputModalities
288
+ ?? (azureMetadata?.input ? modelInputModalities({ id: model.id, input_modalities: azureMetadata.input }, undefined) : undefined);
289
+ let inputModalities = resolvedModalities ? [...resolvedModalities] : undefined;
286
290
  // The shared vision-sidecar consumer predicate keeps catalog advertisement and request-time
287
291
  // planning aligned. The catalog must still advertise image input — the Codex app
288
292
  // gates attachments client-side on input_modalities, and a text-only entry would block images
@@ -319,6 +323,7 @@ export function applyProviderConfigHints(
319
323
  ...(typeof model.maxInputTokens === "number" && model.maxInputTokens > 0 ? { maxInputTokens: model.maxInputTokens } : {}),
320
324
  });
321
325
  const hintedWindow = projectedLimits.contextWindow
326
+ ?? positiveSafeInteger(azureMetadata?.contextWindow)
322
327
  ?? (providerCap !== undefined ? resolveUnknownRoutedContextWindow(providerCap) : undefined);
323
328
  const hinted = {
324
329
  ...modelWithoutServiceTier,
@@ -383,8 +388,9 @@ export function catalogHintsFromProviderConfig(
383
388
  contextCap?: number,
384
389
  metadataModelIdCaseFold?: boolean,
385
390
  effectiveAlias?: string | null,
391
+ metadataConfigDir?: string,
386
392
  ): Partial<CatalogModel> {
387
- const hinted = applyProviderConfigHints(name, prov, { id, provider: name }, contextCap, metadataModelIdCaseFold, effectiveAlias);
393
+ const hinted = applyProviderConfigHints(name, prov, { id, provider: name }, contextCap, metadataModelIdCaseFold, effectiveAlias, metadataConfigDir);
388
394
  const { provider: _provider, id: _id, ...hints } = hinted;
389
395
  return hints;
390
396
  }
@@ -396,8 +402,9 @@ export function applyConfigHintsToCachedModels(
396
402
  contextCap?: number,
397
403
  metadataModelIdCaseFold?: boolean,
398
404
  effectiveAlias?: string | null,
405
+ metadataConfigDir?: string,
399
406
  ): CatalogModel[] {
400
- return models.map(model => applyProviderConfigHints(name, prov, model, contextCap, metadataModelIdCaseFold, effectiveAlias));
407
+ return models.map(model => applyProviderConfigHints(name, prov, model, contextCap, metadataModelIdCaseFold, effectiveAlias, metadataConfigDir));
401
408
  }
402
409
 
403
410
  /** Catalog slugs whose configured model must not gain a missing synthetic max rung. */
@@ -230,6 +230,7 @@ export function mergeConfiguredModelsIntoLiveCatalog(opts: {
230
230
  seedVertexDefault?: boolean;
231
231
  retainComboTargets?: boolean;
232
232
  metadataModelIdCaseFold?: boolean;
233
+ metadataConfigDir?: string;
233
234
  }): { models: CatalogModel[]; droppedConfiguredIds: string[] } {
234
235
  const {
235
236
  name,
@@ -240,6 +241,7 @@ export function mergeConfiguredModelsIntoLiveCatalog(opts: {
240
241
  seedVertexDefault,
241
242
  retainComboTargets = true,
242
243
  metadataModelIdCaseFold,
244
+ metadataConfigDir,
243
245
  } = opts;
244
246
  const out = [...opts.models];
245
247
  const present = new Set(out.map(model => model.id));
@@ -248,7 +250,7 @@ export function mergeConfiguredModelsIntoLiveCatalog(opts: {
248
250
  if (present.has(candidate.id)) continue;
249
251
  const dated = out.find(live => isDatedVariantId(live.id, candidate.id));
250
252
  if (dated) {
251
- out.push(applyProviderConfigHints(name, prov, { ...dated, id: candidate.id }, contextCap, metadataModelIdCaseFold));
253
+ out.push(applyProviderConfigHints(name, prov, { ...dated, id: candidate.id }, contextCap, metadataModelIdCaseFold, undefined, metadataConfigDir));
252
254
  present.add(candidate.id);
253
255
  continue;
254
256
  }