@bitkyc08/opencodex 2.59.0 → 2.61.0-preview.20260922

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 (374) hide show
  1. package/AGENTS_INSTALL.md +64 -0
  2. package/README.md +28 -1
  3. package/bin/ocx.mjs +382 -209
  4. package/gui/dist/assets/App-E64Rzjap.js +50 -0
  5. package/gui/dist/assets/Tray-_nfzD8k4.js +1 -0
  6. package/gui/dist/assets/index-DpdfZWMK.js +86 -0
  7. package/gui/dist/assets/index-_bpvxJu0.css +1 -0
  8. package/gui/dist/assets/usage-companion-chart-DtoK7T6h.js +1 -0
  9. package/gui/dist/favicon.png +0 -0
  10. package/gui/dist/index.html +2 -2
  11. package/gui/dist/provider-icons/stepfun-color.svg +1 -0
  12. package/package.json +8 -1
  13. package/src/adapters/anthropic-image-codec.ts +16 -2
  14. package/src/adapters/anthropic-image-normalize.ts +49 -2
  15. package/src/adapters/anthropic.ts +20 -1
  16. package/src/adapters/base.ts +23 -0
  17. package/src/adapters/coding-agent/protocol.ts +36 -6
  18. package/src/adapters/coding-agent/turn.ts +32 -4
  19. package/src/adapters/command-code.ts +52 -4
  20. package/src/adapters/cursor/catalog.ts +51 -7
  21. package/src/adapters/cursor/checkpoint-store.ts +3 -0
  22. package/src/adapters/cursor/discovery.ts +11 -8
  23. package/src/adapters/cursor/live-transport.ts +26 -9
  24. package/src/adapters/cursor/protobuf-request.ts +6 -3
  25. package/src/adapters/cursor/request-builder.ts +20 -4
  26. package/src/adapters/cursor/transport.ts +19 -0
  27. package/src/adapters/cursor.ts +14 -3
  28. package/src/adapters/declaration-carrier.ts +45 -0
  29. package/src/adapters/devin/cloud-direct/chat.ts +3 -1
  30. package/src/adapters/devin/cloud-direct/stated-reset-retry.ts +42 -5
  31. package/src/adapters/devin.ts +125 -36
  32. package/src/adapters/google-antigravity-replay.ts +1 -1
  33. package/src/adapters/google-antigravity-wire.ts +34 -7
  34. package/src/adapters/google-errors.ts +7 -1
  35. package/src/adapters/google-http.ts +49 -10
  36. package/src/adapters/google-tool-schema.ts +595 -31
  37. package/src/adapters/google-wire-compiler.ts +93 -10
  38. package/src/adapters/google-wire-shape.ts +461 -0
  39. package/src/adapters/google.ts +66 -11
  40. package/src/adapters/image.ts +4 -1
  41. package/src/adapters/input-media-guard.ts +21 -9
  42. package/src/adapters/kiro/usage.ts +3 -2
  43. package/src/adapters/kiro-tool-fallback.ts +1 -1
  44. package/src/adapters/ollama-native.ts +6 -0
  45. package/src/adapters/openai-chat/developer-role.ts +61 -0
  46. package/src/adapters/openai-chat/messages.ts +46 -27
  47. package/src/adapters/openai-chat/parallel-tool-calls.ts +32 -0
  48. package/src/adapters/openai-chat/passthrough.ts +33 -9
  49. package/src/adapters/openai-chat/reasoning-wire.ts +89 -0
  50. package/src/adapters/openai-chat-images.ts +3 -1
  51. package/src/adapters/openai-chat.ts +23 -58
  52. package/src/adapters/openai-responses/image-gen.ts +8 -6
  53. package/src/adapters/openai-responses/passthrough.ts +17 -3
  54. package/src/adapters/openai-responses/reasoning.ts +7 -0
  55. package/src/adapters/opencode-go-additional-tools.ts +12 -2
  56. package/src/adapters/registry.ts +3 -2
  57. package/src/adapters/run-turn-queue.ts +178 -29
  58. package/src/adapters/xai-web-search.ts +16 -1
  59. package/src/bridge/errors.ts +8 -2
  60. package/src/bridge/response-json.ts +9 -1
  61. package/src/bridge/sse.ts +13 -151
  62. package/src/chat/inbound.ts +141 -5
  63. package/src/claude/desktop-3p.ts +7 -1
  64. package/src/claude/desktop-first-party.ts +183 -0
  65. package/src/claude/desktop-gateway-state.ts +41 -0
  66. package/src/claude/inbound-content-options.ts +6 -0
  67. package/src/claude/inbound.ts +32 -6
  68. package/src/claude/intercept/connect-proxy.ts +179 -0
  69. package/src/claude/intercept/listener.ts +122 -0
  70. package/src/claude/intercept/local-ca.ts +298 -0
  71. package/src/claude/intercept/runtime.ts +98 -0
  72. package/src/claude/intercept/settings.ts +189 -0
  73. package/src/cli/access.ts +87 -0
  74. package/src/cli/account-auth.ts +19 -0
  75. package/src/cli/account-extended.ts +4 -4
  76. package/src/cli/capabilities.ts +31 -0
  77. package/src/cli/claude-desktop.ts +206 -16
  78. package/src/cli/codex-shim-autorestore.ts +3 -0
  79. package/src/cli/companion.ts +56 -0
  80. package/src/cli/dispatch.ts +46 -7
  81. package/src/cli/doctor.ts +28 -9
  82. package/src/cli/ensure-desired-integrations.ts +43 -5
  83. package/src/cli/help.ts +7 -9
  84. package/src/cli/hub.ts +3 -2
  85. package/src/cli/index.ts +203 -59
  86. package/src/cli/init.ts +8 -0
  87. package/src/cli/integrations.ts +7 -1
  88. package/src/cli/opencode.ts +2 -2
  89. package/src/cli/provider.ts +13 -1
  90. package/src/cli/registry.ts +41 -2
  91. package/src/cli/resolve.ts +230 -0
  92. package/src/cli/root.ts +24 -1
  93. package/src/cli/start-ownership-publication.ts +56 -0
  94. package/src/cli/status-probes.ts +2 -18
  95. package/src/cli/status.ts +62 -0
  96. package/src/cli/stop-report.ts +143 -0
  97. package/src/cli/uninstall-plan.ts +9 -0
  98. package/src/client/machine-api.ts +2 -2
  99. package/src/client/machine-listener.ts +4 -7
  100. package/src/client/runtime.ts +26 -2
  101. package/src/clients/aside-profiles.ts +4 -0
  102. package/src/clients/config-export/zcode-store.ts +157 -0
  103. package/src/clients/config-export.ts +36 -0
  104. package/src/codex/account-store.ts +65 -0
  105. package/src/codex/app-server-processes.ts +72 -40
  106. package/src/codex/auth-api/account-list.ts +19 -11
  107. package/src/codex/auth-api/login-flow.ts +6 -1
  108. package/src/codex/auth-api/pool-quota-probe.ts +30 -7
  109. package/src/codex/autostart-health.ts +28 -0
  110. package/src/codex/catalog/build-entries.ts +2 -2
  111. package/src/codex/catalog/effort.ts +3 -3
  112. package/src/codex/catalog/gather-capture.ts +21 -2
  113. package/src/codex/catalog/model-hints.ts +29 -28
  114. package/src/codex/catalog/parsing.ts +7 -0
  115. package/src/codex/catalog/provider-models.ts +43 -17
  116. package/src/codex/catalog/retained-sync.ts +24 -28
  117. package/src/codex/catalog/routed-gather.ts +19 -0
  118. package/src/codex/context-compat.ts +5 -2
  119. package/src/codex/convergence.ts +2 -2
  120. package/src/codex/desired-state.ts +4 -1
  121. package/src/codex/history-job.ts +6 -6
  122. package/src/codex/history-provider.ts +31 -166
  123. package/src/codex/history-rollout-read.ts +174 -0
  124. package/src/codex/inject/config-toml.ts +41 -6
  125. package/src/codex/inject/paginated-openai-compat.ts +90 -0
  126. package/src/codex/inject.ts +18 -15
  127. package/src/codex/injected-marker.ts +18 -0
  128. package/src/codex/internal/catalog-writer.ts +33 -1
  129. package/src/codex/main-account.ts +6 -0
  130. package/src/codex/model-cache.ts +99 -6
  131. package/src/codex/model-entitlement-admission.ts +59 -0
  132. package/src/codex/model-entitlements.ts +116 -54
  133. package/src/codex/native-main-admission.ts +83 -0
  134. package/src/codex/observed-model-denials.ts +101 -8
  135. package/src/codex/prompt-text-probe.ts +9 -6
  136. package/src/codex/routing/health-store.ts +39 -0
  137. package/src/codex/routing/selection.ts +37 -1
  138. package/src/codex/routing.ts +12 -42
  139. package/src/codex/shim-templates.ts +29 -3
  140. package/src/codex/shim.ts +1 -1
  141. package/src/codex/subagent-model-fallback.ts +22 -4
  142. package/src/combos/failover.ts +3 -0
  143. package/src/companion/settings.ts +132 -0
  144. package/src/config/admitted-identity.ts +222 -0
  145. package/src/config/atomic-write.ts +117 -5
  146. package/src/config/diagnostics.ts +22 -1
  147. package/src/config/feature-flags.ts +5 -0
  148. package/src/config/load-degrade.ts +52 -7
  149. package/src/config/process-state.ts +1 -1
  150. package/src/config/proxy-env.ts +8 -2
  151. package/src/config/schema/compaction-triggers.ts +11 -0
  152. package/src/config/schema/config-schema.ts +31 -1
  153. package/src/config/schema/leaf-validators.ts +59 -0
  154. package/src/config.ts +3 -3
  155. package/src/generated/compatibility-version.json +604 -280
  156. package/src/grok/reset-coupons.ts +38 -19
  157. package/src/images/loop.ts +6 -1
  158. package/src/integrations/aside-profile-context.ts +37 -3
  159. package/src/integrations/aside-profile-journal.ts +68 -3
  160. package/src/integrations/aside-profiles.ts +128 -3
  161. package/src/integrations/config-io.ts +44 -10
  162. package/src/integrations/merge.ts +120 -13
  163. package/src/integrations/mutation-plan.ts +921 -0
  164. package/src/integrations/registry.ts +38 -0
  165. package/src/integrations/state.ts +78 -45
  166. package/src/integrations/target.ts +208 -0
  167. package/src/integrations/writer.ts +134 -110
  168. package/src/lab/conformance/fixture-provider.ts +5 -0
  169. package/src/lab/live/transport.ts +4 -0
  170. package/src/lab/live/types.ts +5 -0
  171. package/src/lab/subject/behavior-fingerprint.ts +1 -1
  172. package/src/lib/admin-secrets.ts +9 -1
  173. package/src/lib/browser-launch-notice.ts +59 -0
  174. package/src/lib/bun-runtime.ts +6 -2
  175. package/src/lib/debug-log-buffer.ts +6 -1
  176. package/src/lib/debug.ts +63 -0
  177. package/src/lib/errors.ts +79 -0
  178. package/src/lib/http-response-semantics.ts +57 -0
  179. package/src/lib/lab-live-pinned-sender.ts +26 -12
  180. package/src/lib/open-url.ts +51 -7
  181. package/src/lib/package-tree-integrity.ts +2 -1
  182. package/src/lib/package-version.ts +8 -0
  183. package/src/lib/pinned-http.ts +142 -2
  184. package/src/lib/plain-data.ts +103 -0
  185. package/src/lib/process-control.ts +13 -5
  186. package/src/lib/provider-egress.ts +310 -0
  187. package/src/lib/provider-outbound.ts +109 -16
  188. package/src/lib/proxy-env.ts +82 -7
  189. package/src/lib/request-execution-budget.ts +72 -0
  190. package/src/lib/request-failure-attribution.ts +183 -0
  191. package/src/lib/request-failure-model.ts +236 -0
  192. package/src/lib/request-resend-gate.ts +138 -0
  193. package/src/lib/socks5-fetch.ts +136 -26
  194. package/src/lib/spend-ledger-owner.ts +364 -0
  195. package/src/lib/spend-reservation-ledger.ts +218 -27
  196. package/src/lib/standalone.ts +16 -0
  197. package/src/lib/upstream-retry.ts +167 -16
  198. package/src/lib/windows-system-proxy.ts +16 -11
  199. package/src/lib/winsw.ts +2 -2
  200. package/src/oauth/callback-server.ts +4 -3
  201. package/src/oauth/generic-account-failover.ts +1 -0
  202. package/src/oauth/health.ts +12 -1
  203. package/src/oauth/index.ts +27 -108
  204. package/src/oauth/login-cli.ts +80 -29
  205. package/src/oauth/login-flow-state.ts +127 -0
  206. package/src/providers/api-key-resolve.ts +133 -0
  207. package/src/providers/api-key-selection.ts +5 -1
  208. package/src/providers/derive.ts +34 -17
  209. package/src/providers/devin-cli-authmode-migration.ts +14 -10
  210. package/src/providers/key-failover.ts +97 -19
  211. package/src/providers/key-store.ts +34 -110
  212. package/src/providers/model-rename-fields.ts +147 -0
  213. package/src/providers/model-rename-migration.ts +179 -38
  214. package/src/providers/model-rename-startup.ts +7 -5
  215. package/src/providers/openai-virtual-models.ts +42 -2
  216. package/src/providers/quota/antigravity.ts +22 -2
  217. package/src/providers/quota/vendor-probes-key.ts +38 -23
  218. package/src/providers/reasoning-metadata.ts +43 -18
  219. package/src/providers/registry/entries-core.ts +47 -20
  220. package/src/providers/registry/entries-extended.ts +63 -4
  221. package/src/providers/registry/model-ids.ts +168 -0
  222. package/src/providers/registry/model-seeds.ts +56 -10
  223. package/src/providers/registry/types.ts +2 -0
  224. package/src/providers/resolved-model-policy-merge.ts +167 -0
  225. package/src/providers/resolved-model-policy.ts +406 -0
  226. package/src/providers/stale-vision-classification-migration.ts +137 -0
  227. package/src/providers/xai-transport.ts +12 -1
  228. package/src/reasoning-effort.ts +8 -0
  229. package/src/responses/apply-patch-envelope.ts +0 -12
  230. package/src/responses/freeform-wrapper-scan.ts +279 -0
  231. package/src/responses/function-call-compat.ts +38 -1
  232. package/src/responses/inline-document.ts +65 -0
  233. package/src/responses/input-media.ts +42 -8
  234. package/src/responses/legacy-dotted-tool-name-repair.ts +134 -0
  235. package/src/responses/muse-tool-name-alias.ts +19 -0
  236. package/src/responses/parser-content.ts +8 -2
  237. package/src/responses/parser-tools.ts +3 -0
  238. package/src/responses/parser.ts +3 -1
  239. package/src/responses/progressive-freeform-input.ts +130 -0
  240. package/src/responses/reasoning-envelope.ts +30 -0
  241. package/src/responses/schema.ts +3 -0
  242. package/src/responses/state.ts +5 -12
  243. package/src/responses/tool-name-aliases.ts +15 -1
  244. package/src/router.ts +108 -117
  245. package/src/routing/compatibility/behavior.ts +9 -0
  246. package/src/routing/compatibility/subject.ts +16 -1
  247. package/src/server/adapter-resolve.ts +9 -0
  248. package/src/server/admission-model-scope.ts +219 -0
  249. package/src/server/audio-live.ts +9 -3
  250. package/src/server/audio-upstream.ts +18 -0
  251. package/src/server/auth-cors.ts +29 -0
  252. package/src/server/chat-completions.ts +60 -4
  253. package/src/server/chat-native.ts +19 -4
  254. package/src/server/claude-messages.ts +61 -20
  255. package/src/server/effort-row.ts +11 -3
  256. package/src/server/grok-responses-control-frame.ts +160 -1
  257. package/src/server/grok-responses-snapshot-repair.ts +113 -11
  258. package/src/server/gui-freshness.ts +103 -0
  259. package/src/server/gui-static.ts +7 -9
  260. package/src/server/images.ts +59 -6
  261. package/src/server/index/claude-intercept-lifecycle.ts +49 -0
  262. package/src/server/index/serve-options.ts +142 -39
  263. package/src/server/index/spend-ledger-lifecycle.ts +92 -0
  264. package/src/server/index/startup-warnings.ts +24 -0
  265. package/src/server/index/websocket-handler.ts +6 -1
  266. package/src/server/index.ts +34 -38
  267. package/src/server/lifecycle.ts +4 -4
  268. package/src/server/live-call-bindings.ts +6 -0
  269. package/src/server/live.ts +88 -3
  270. package/src/server/management/agent-settings-routes.ts +121 -36
  271. package/src/server/management/aside-profile-routes.ts +266 -7
  272. package/src/server/management/companion-routes.ts +77 -0
  273. package/src/server/management/config-routes.ts +18 -2
  274. package/src/server/management/context.ts +3 -0
  275. package/src/server/management/integration-routes.ts +287 -5
  276. package/src/server/management/logs-usage-routes.ts +19 -0
  277. package/src/server/management/metrics-routes.ts +20 -0
  278. package/src/server/management/model-rows.ts +224 -12
  279. package/src/server/management/native-integration-routes.ts +103 -6
  280. package/src/server/management/oauth-account-routes.ts +45 -7
  281. package/src/server/management/route-registry.ts +20 -0
  282. package/src/server/management/shared.ts +28 -4
  283. package/src/server/management/system-restart.ts +7 -2
  284. package/src/server/management/system-routes.ts +2 -0
  285. package/src/server/management/usage-aggregate-cache.ts +4 -0
  286. package/src/server/management/usage-timeline-routes.ts +44 -0
  287. package/src/server/management-api.ts +10 -9
  288. package/src/server/management-auth.ts +15 -1
  289. package/src/server/proxy-liveness.ts +75 -0
  290. package/src/server/readiness.ts +29 -10
  291. package/src/server/relay-eager.ts +24 -2
  292. package/src/server/relay.ts +138 -12
  293. package/src/server/request-log-failure-attribution.ts +99 -0
  294. package/src/server/request-log.ts +169 -2
  295. package/src/server/request-metrics.ts +298 -0
  296. package/src/server/responses/adapter-continuation.ts +3 -3
  297. package/src/server/responses/adapter-dispatch.ts +11 -6
  298. package/src/server/responses/codex-ws-wire.ts +34 -8
  299. package/src/server/responses/combo-stream-preflight.ts +168 -6
  300. package/src/server/responses/compact.ts +43 -10
  301. package/src/server/responses/compaction-routing.ts +111 -0
  302. package/src/server/responses/core-codex-account.ts +8 -3
  303. package/src/server/responses/core-combo.ts +7 -7
  304. package/src/server/responses/core-normalize.ts +6 -12
  305. package/src/server/responses/core-opaque-recovery.ts +91 -0
  306. package/src/server/responses/core-options.ts +4 -0
  307. package/src/server/responses/encrypted-payload.ts +20 -2
  308. package/src/server/responses/fetch-helpers.ts +124 -8
  309. package/src/server/responses/input-admission.ts +10 -0
  310. package/src/server/responses/passthrough-delivery.ts +45 -15
  311. package/src/server/responses/passthrough-dispatch.ts +215 -44
  312. package/src/server/responses/passthrough-error.ts +27 -8
  313. package/src/server/responses/policy-fallback.ts +5 -13
  314. package/src/server/responses/request-prepare.ts +109 -18
  315. package/src/server/responses/request-send-budget.ts +17 -1
  316. package/src/server/responses/request-sidecar-auth.ts +1 -1
  317. package/src/server/responses/request-transport.ts +26 -6
  318. package/src/server/responses/reset-replay.ts +108 -0
  319. package/src/server/responses/run-turn-execution.ts +25 -3
  320. package/src/server/responses/sidecar-execution.ts +17 -2
  321. package/src/server/responses/ws-upstream.ts +14 -27
  322. package/src/server/responses-custom-tool-repair.ts +27 -54
  323. package/src/server/responses-request-tool-scope.ts +214 -0
  324. package/src/server/responses-undeclared-tool-guard.ts +35 -2
  325. package/src/server/search.ts +25 -1
  326. package/src/server/sse-payload-rewrite.ts +1 -1
  327. package/src/server/usage-ledger-retention.ts +73 -0
  328. package/src/service/cli.ts +48 -2
  329. package/src/service/health.ts +3 -2
  330. package/src/service/install-state-contract.d.mts +27 -0
  331. package/src/service/install-state-contract.mjs +34 -0
  332. package/src/service/launchd.ts +1 -1
  333. package/src/service/orchestration.ts +2 -4
  334. package/src/service/ownership-compatibility.ts +164 -0
  335. package/src/service/ownership-mutation-lease.d.mts +32 -0
  336. package/src/service/ownership-mutation-lease.mjs +211 -0
  337. package/src/service/repair.ts +45 -1
  338. package/src/service/state-lock.ts +269 -0
  339. package/src/service/state-record.d.mts +36 -0
  340. package/src/service/state-record.mjs +138 -0
  341. package/src/service/state.ts +582 -68
  342. package/src/service/windows-taskxml.ts +11 -10
  343. package/src/service.ts +7 -3
  344. package/src/tray/windows-tray.ps1 +156 -4
  345. package/src/types/config.ts +48 -3
  346. package/src/types/provider.ts +91 -0
  347. package/src/types/request.ts +30 -2
  348. package/src/types/tools.ts +33 -0
  349. package/src/types.ts +4 -0
  350. package/src/update/index.ts +207 -63
  351. package/src/update/job.ts +9 -5
  352. package/src/update/ownership-transaction.ts +47 -0
  353. package/src/update/restart-ownership.ts +54 -0
  354. package/src/update/runtime-ownership.d.mts +40 -0
  355. package/src/update/runtime-ownership.mjs +122 -0
  356. package/src/usage/attempt-delivery.ts +198 -0
  357. package/src/usage/cache-diagnostic.ts +305 -0
  358. package/src/usage/failure-fingerprint.ts +118 -0
  359. package/src/usage/failure-projection-cache.ts +174 -0
  360. package/src/usage/failure-projection.ts +174 -0
  361. package/src/usage/ledger-retention.ts +165 -0
  362. package/src/usage/log.ts +126 -79
  363. package/src/usage/request-outcome.ts +150 -0
  364. package/src/usage/retention-contract.ts +28 -0
  365. package/src/usage/summary.ts +2 -2
  366. package/src/usage/telemetry-contract.ts +237 -0
  367. package/src/usage/timeline.ts +236 -0
  368. package/src/vision/eligibility.ts +88 -9
  369. package/src/vision/plan.ts +34 -10
  370. package/src/web-search/alpha-search.ts +21 -1
  371. package/src/web-search/executor.ts +41 -2
  372. package/src/web-search/loop.ts +6 -1
  373. package/gui/dist/assets/index-C5IebErG.js +0 -136
  374. package/gui/dist/assets/index-OESInAjC.css +0 -1
@@ -786,6 +786,58 @@ function catalogStatusFromProcesses(
786
786
  return { state: stale ? "stale" : "fresh", processes: withStarts, catalogMtimeMs };
787
787
  }
788
788
 
789
+ interface ComputedCodexAppServerCatalogStatus {
790
+ status: CodexAppServerCatalogStatus;
791
+ processes: CodexAppServerProcess[];
792
+ }
793
+
794
+ /**
795
+ * Compute one catalog-state observation while retaining the command lines from the
796
+ * same process enumeration. The public collector deliberately exposes only the
797
+ * identity and timestamp projection; post-write warning code also needs the matched
798
+ * process records, and re-enumerating there creates a race between classification and
799
+ * reporting (as well as a second expensive Windows CIM walk).
800
+ */
801
+ function computeCodexAppServerCatalogStatus(
802
+ io: CodexAppServerProcessIo,
803
+ ): ComputedCodexAppServerCatalogStatus {
804
+ const platform = io.platform ?? process.platform;
805
+ const getuid = io.getuid ?? (() => {
806
+ try {
807
+ return typeof process.getuid === "function" ? process.getuid() : undefined;
808
+ } catch {
809
+ return undefined;
810
+ }
811
+ });
812
+ let snapshots: ProcessSnapshot[];
813
+ let enumerationFailed = false;
814
+ const enumerate = io.listSnapshots ?? (() => defaultListSnapshots(platform, getuid));
815
+ try {
816
+ snapshots = enumerate();
817
+ } catch {
818
+ // A failed process read is unknown, never proof that nothing is running.
819
+ snapshots = [];
820
+ enumerationFailed = true;
821
+ }
822
+ const processes = codexAppServerProcessesFromSnapshots(snapshots);
823
+ if (processes.length === 0) {
824
+ return {
825
+ processes,
826
+ status: enumerationFailed
827
+ ? { state: "unknown", processes: [], catalogMtimeMs: null }
828
+ : { state: "not_running", processes: [], catalogMtimeMs: null },
829
+ };
830
+ }
831
+ const catalogMtimeMs = (io.catalogMtimeMs ?? defaultCatalogMtimeMs)();
832
+ const starts = io.readStartMs
833
+ ? new Map(processes.map(proc => [proc.pid, io.readStartMs!(proc.pid)] as const))
834
+ : readProcessStartMsBatch(processes.map(proc => proc.pid), platform);
835
+ return {
836
+ processes,
837
+ status: catalogStatusFromProcesses(processes, catalogMtimeMs, starts),
838
+ };
839
+ }
840
+
789
841
  // Short TTL: process listing + stat run once per window even under per-turn
790
842
  // guidance calls (#857).
791
843
  let catalogStateCache: { atMs: number; status: CodexAppServerCatalogStatus } | null = null;
@@ -889,41 +941,7 @@ export function collectCodexAppServerCatalogState(
889
941
  && now - catalogStateCache.atMs < catalogStateTtlMs(catalogStateCache.status.state)) {
890
942
  return catalogStateCache.status;
891
943
  }
892
- const compute = (): CodexAppServerCatalogStatus => {
893
- const platform = io.platform ?? process.platform;
894
- const getuid = io.getuid ?? (() => {
895
- try {
896
- return typeof process.getuid === "function" ? process.getuid() : undefined;
897
- } catch {
898
- return undefined;
899
- }
900
- });
901
- let snapshots: ProcessSnapshot[];
902
- let enumerationFailed = false;
903
- const enumerate = io.listSnapshots ?? (() => defaultListSnapshots(platform, getuid));
904
- try {
905
- snapshots = enumerate();
906
- } catch {
907
- // Enumeration failure must never read as "nothing running" — that would let
908
- // positive model guidance through on guesswork (#857). The injected seam gets
909
- // the same contract as the default path: whoever enumerates, a failure to read
910
- // the process list is unknown, not an empty machine.
911
- snapshots = [];
912
- enumerationFailed = true;
913
- }
914
- const processes = codexAppServerProcessesFromSnapshots(snapshots);
915
- if (processes.length === 0) {
916
- return enumerationFailed
917
- ? { state: "unknown", processes: [], catalogMtimeMs: null }
918
- : { state: "not_running", processes: [], catalogMtimeMs: null };
919
- }
920
- const catalogMtimeMs = (io.catalogMtimeMs ?? defaultCatalogMtimeMs)();
921
- const starts = io.readStartMs
922
- ? new Map(processes.map(proc => [proc.pid, io.readStartMs!(proc.pid)] as const))
923
- : readProcessStartMsBatch(processes.map(proc => proc.pid), platform);
924
- return catalogStatusFromProcesses(processes, catalogMtimeMs, starts);
925
- };
926
- const status = compute();
944
+ const status = computeCodexAppServerCatalogStatus(io).status;
927
945
  if (fullyDefault) {
928
946
  catalogStateCache = { atMs: now, status };
929
947
  }
@@ -1228,16 +1246,30 @@ export function afterCatalogWriteHandleAppServers(
1228
1246
  options: AfterCatalogWriteAppServerOptions,
1229
1247
  ): AfterCatalogWriteAppServerResult {
1230
1248
  const excluded = new Set(options.excludePids ?? []);
1249
+ const hint = STALE_CODEX_APP_SERVER_HINT;
1250
+ if (!options.restart) {
1251
+ // A running process is not necessarily stale. Classify the exact process
1252
+ // enumeration that supplies the warning, and stay quiet when freshness cannot be
1253
+ // established rather than presenting an unknown observation as a known mismatch.
1254
+ const observed = computeCodexAppServerCatalogStatus(options.io ?? {});
1255
+ const starts = new Map(observed.status.processes.map(process => [process.pid, process.startedAtMs]));
1256
+ const catalogMtimeMs = observed.status.catalogMtimeMs;
1257
+ const processes = observed.processes.filter(process => !excluded.has(process.pid));
1258
+ const staleProcesses = catalogMtimeMs === null
1259
+ ? []
1260
+ : processes.filter(process => {
1261
+ const startedAtMs = starts.get(process.pid);
1262
+ return startedAtMs !== null && startedAtMs !== undefined && startedAtMs <= catalogMtimeMs;
1263
+ });
1264
+ if (staleProcesses.length === 0) return { processes, warned: false, hint };
1265
+ options.log?.error(formatStaleCodexAppServerWarning(staleProcesses));
1266
+ return { processes: staleProcesses, warned: true, hint };
1267
+ }
1231
1268
  const processes = listCodexAppServerProcesses(options.io)
1232
1269
  .filter(process => !excluded.has(process.pid));
1233
- const hint = STALE_CODEX_APP_SERVER_HINT;
1234
1270
  if (processes.length === 0) {
1235
1271
  return { processes, warned: false, hint };
1236
1272
  }
1237
- if (!options.restart) {
1238
- options.log?.error(formatStaleCodexAppServerWarning(processes));
1239
- return { processes, warned: true, hint };
1240
- }
1241
1273
  options.log?.log(
1242
1274
  `Stopping Codex app-server process(es): ${processes.map(process => process.pid).join(", ")} `
1243
1275
  + "(active turns may be interrupted).",
@@ -82,9 +82,9 @@ export function mainQuotaWithCarriedResetCredits(
82
82
  /**
83
83
  * Why an account needs the operator. `missing_credential`, `refresh_failed`, and
84
84
  * `quota_unauthorized` are the three causes this surface tells apart on its own. `unauthorized`
85
- * and `forbidden` exist because the shared health projection may return them; today
86
- * `projectCodexAccountHealth` only ever produces `refresh_failed`, so accepting the full union
87
- * keeps this field correct if that projection widens rather than silently dropping a reason.
85
+ * and `forbidden` come from the shared health projection: `projectCodexAccountHealth` maps a
86
+ * stored verification failure's `http_status:401`/`http_status:403` to them, so the union has
87
+ * to accept every reason the projection can emit rather than silently dropping one.
88
88
  */
89
89
  export type CodexAccountReauthReason =
90
90
  | "missing_credential"
@@ -105,18 +105,26 @@ export function poolAccountDto(
105
105
  const plan = codexPlanValue(account.plan);
106
106
  const quota = quotaForPlan(quotaResult.quota, plan);
107
107
  const runtimeReauth = isAccountNeedsReauth(account.id);
108
+ const rawReauthReason: CodexAccountReauthReason | undefined = !hasCredential
109
+ ? "missing_credential"
110
+ : quotaResult.reauthReason;
108
111
  const needsReauth = !hasCredential || quotaResult.needsReauth || runtimeReauth;
109
- const health = projectCodexAccountHealth({ accountId: account.id, needsReauth });
112
+ // An in-memory reauth mark carries no cause of its own, so it must not name one: passing
113
+ // a caller reason here would outrank the stored verdict's http_status inside the
114
+ // projection and hide unauthorized/forbidden until the mark is gone. With no caller
115
+ // reason the projection falls back to the persisted cause, then to refresh_failed.
116
+ const healthReason = rawReauthReason === "quota_unauthorized" || rawReauthReason === "missing_credential"
117
+ ? "unauthorized"
118
+ : rawReauthReason;
119
+ const health = projectCodexAccountHealth({
120
+ accountId: account.id,
121
+ needsReauth,
122
+ reauthReason: needsReauth ? healthReason : undefined,
123
+ });
110
124
  // `needsReauth` is an OR of three independent causes plus a persisted verdict resolved inside the
111
125
  // health projection. Emitting only the boolean is what left #4212's reporter guessing which
112
126
  // account took their model away and why, so name the cause they actually have to act on.
113
- const reauthReason: CodexAccountReauthReason | undefined = !hasCredential
114
- ? "missing_credential"
115
- : runtimeReauth
116
- ? "refresh_failed"
117
- : quotaResult.needsReauth
118
- ? "quota_unauthorized"
119
- : health.status === "reauth_required" ? health.reason : undefined;
127
+ const reauthReason: CodexAccountReauthReason | undefined = rawReauthReason ?? (health.status === "reauth_required" ? health.reason : undefined);
120
128
  return {
121
129
  id: account.id,
122
130
  email: projectEmail(account.email, maskEmails) ?? account.email,
@@ -226,9 +226,12 @@ export async function handleCodexAuthLoginStart(req: Request, config: OcxConfig,
226
226
  // machine. Opening it on the hub host is useless at best, and on a
227
227
  // headless host it fails. `deviceCode` is the same signal the generic
228
228
  // OAuth login route uses to make this decision.
229
+ // Reported to the caller rather than discarded (#5261): a login whose browser never opened
230
+ // is indistinguishable from one that did, so it reads as success while nothing happens.
231
+ let browserLaunch: "started" | "failed" | "skipped" = "skipped";
229
232
  if (result.url && !result.deviceCode && shouldOpenBrowserForLogin(body.openBrowser, runtimeConfig)) {
230
233
  const { openUrl } = await import("../../lib/open-url");
231
- openUrl(result.url);
234
+ browserLaunch = (await openUrl(result.url)).status === "started" ? "started" : "failed";
232
235
  }
233
236
 
234
237
  (async () => {
@@ -488,6 +491,8 @@ export async function handleCodexAuthLoginStart(req: Request, config: OcxConfig,
488
491
  flowId,
489
492
  url: result.url,
490
493
  instructions: result.instructions,
494
+ // Never fatal: the URL is still a valid thing to open by hand, and the flow stays live.
495
+ browserLaunch,
491
496
  // Dropped before #3366: every device-code surface renders this field,
492
497
  // so withholding it left the GUI and CLI with no code to show.
493
498
  ...(result.deviceCode ? { deviceCode: result.deviceCode } : {}),
@@ -1,4 +1,4 @@
1
- import { capturePoolQuotaWriter, getValidCodexToken, isCodexAccountGenerationLive, forceRefreshCodexPoolToken, markCodexAccountValidated, markCodexAccountValidationFailed, readCodexAccountRecord, CodexCredentialGenerationConflictError, CodexCredentialRefreshLockTimeoutError, CodexCredentialRefreshBusyError, CodexCredentialRefreshStaleError, TokenRefreshError } from "../account-store";
1
+ import { capturePoolQuotaWriter, getValidCodexToken, isCodexAccountGenerationLive, forceRefreshCodexPoolToken, markCodexAccountValidated, markCodexAccountValidationFailed, readCodexAccountRecord, isTerminalCodexPoolRefreshFailure, CodexCredentialGenerationConflictError, CodexCredentialRefreshLockTimeoutError, CodexCredentialRefreshBusyError, CodexCredentialRefreshStaleError, TokenRefreshError } from "../account-store";
2
2
  import type { PoolQuotaWriter } from "../quota-types";
3
3
  import { isValidWhamHistoryObservation, getAccountQuota, isCompleteCodexQuotaRecoverySnapshot, parseUsageQuota, setAccountQuotaFromParsed } from "../quota";
4
4
  import type { StoredAccountQuota, WhamUsageResponse } from "../quota";
@@ -46,6 +46,8 @@ export interface PoolQuotaResult {
46
46
  resetRefreshLineage?: ManualResetRefreshLineage;
47
47
  quota: StoredAccountQuota | null;
48
48
  needsReauth: boolean;
49
+ /** Failure source observed while obtaining the quota, when reauthentication is required. */
50
+ reauthReason?: "refresh_failed" | "quota_unauthorized";
49
51
  /** Credential generation whose cache or network result this DTO state belongs to. */
50
52
  credentialGeneration?: number;
51
53
  /** Present only when this call freshly parsed a WHAM usage response. */
@@ -178,7 +180,7 @@ export async function recoverPoolQuotaFrom401(ctx: {
178
180
  // the credential it condemned, so a late terminal response arriving after the operator
179
181
  // re-authenticated would quarantine the replacement.
180
182
  markAccountNeedsReauth(accountId, captureConfigGeneration(), rejectedGeneration);
181
- return { quota: existing ?? null, needsReauth: true, credentialGeneration: rejectedGeneration };
183
+ return { quota: existing ?? null, needsReauth: true, reauthReason: "quota_unauthorized", credentialGeneration: rejectedGeneration };
182
184
  }
183
185
 
184
186
  const claim = claimQuotaRecovery(accountId, rejectedGeneration);
@@ -186,7 +188,7 @@ export async function recoverPoolQuotaFrom401(ctx: {
186
188
  // A lineage fenced by a TERMINAL refresh failure stays terminal. Without this, the
187
189
  // budget being used would make the next bare 401 report a dead credential as healthy.
188
190
  if (quotaRecoveryTerminalFor(accountId, rejectedGeneration)) {
189
- return { quota: existing ?? null, needsReauth: true, credentialGeneration: rejectedGeneration };
191
+ return { quota: existing ?? null, needsReauth: true, reauthReason: "refresh_failed", credentialGeneration: rejectedGeneration };
190
192
  }
191
193
  // Otherwise: this lineage spent its attempt, another caller is mid-refresh, or a
192
194
  // transient failure is backing off. Report transient and let the next poll try —
@@ -225,8 +227,15 @@ export async function recoverPoolQuotaFrom401(ctx: {
225
227
  // A refresh that failed terminally is the one case where the credential really is gone.
226
228
  // Everything else is unknown, and unknown is not proof.
227
229
  if (e instanceof TokenRefreshError && isTerminalRefreshError(e)) {
230
+ // Persist the same verdict the token guardian writes: the in-memory mark dies with
231
+ // this process, and only the stored terminal failure keeps a cached listing from
232
+ // calling the dead grant healthy after a restart.
233
+ markCodexAccountValidationFailed(accountId, `refresh_${e.reason}`, {
234
+ expectedGeneration: rejectedGeneration,
235
+ terminal: true,
236
+ });
228
237
  markAccountNeedsReauth(accountId, captureConfigGeneration(), rejectedGeneration);
229
- return { quota: existing ?? null, needsReauth: true, credentialGeneration: rejectedGeneration };
238
+ return { quota: existing ?? null, needsReauth: true, reauthReason: "refresh_failed", credentialGeneration: rejectedGeneration };
230
239
  }
231
240
  return { quota: existing ?? null, needsReauth: false, credentialGeneration: rejectedGeneration };
232
241
  }
@@ -258,7 +267,7 @@ export async function recoverPoolQuotaFrom401(ctx: {
258
267
  // let the next poll call a dead credential healthy. The evidence is about the
259
268
  // REFRESHED credential, which is what the replay used.
260
269
  markAccountNeedsReauth(accountId, writerGeneration, refreshed.generation);
261
- return { quota: existing ?? null, needsReauth: true, credentialGeneration: refreshed.generation };
270
+ return { quota: existing ?? null, needsReauth: true, reauthReason: "quota_unauthorized", credentialGeneration: refreshed.generation };
262
271
  }
263
272
  return { quota: existing ?? null, needsReauth: false, credentialGeneration: refreshed.generation };
264
273
  }
@@ -399,9 +408,23 @@ export async function fetchFreshPoolAccountQuota(
399
408
  quotaProbeSkipped: true,
400
409
  }, quotaProbeEvidence);
401
410
  }
402
- if (e instanceof TokenRefreshError) {
411
+ // Terminal means the grant itself is dead or missing; an `unknown` refresh failure (a
412
+ // token-endpoint 5xx, a transport blip) may clear, so it reports transient like the
413
+ // 401-recovery path instead of quarantining a healthy account (#2887). A revoked or
414
+ // expired grant is also written to the record as a terminal validation failure, the same
415
+ // verdict the token guardian persists: the in-memory mark dies with this process, and
416
+ // the stored verdict is what keeps a cached listing from calling the dead grant healthy
417
+ // after a restart.
418
+ if (isTerminalCodexPoolRefreshFailure(e)) {
419
+ if (e instanceof TokenRefreshError && isTerminalRefreshError(e)) {
420
+ markCodexAccountValidationFailed(accountId, `refresh_${e.reason}`, {
421
+ expectedGeneration: requestCredentialGeneration,
422
+ terminal: true,
423
+ });
424
+ }
425
+ markAccountNeedsReauth(accountId, captureConfigGeneration(), requestCredentialGeneration);
403
426
  return withQuotaProbeEvidence(
404
- { quota: existing ?? null, needsReauth: true, credentialGeneration: requestCredentialGeneration },
427
+ { quota: existing ?? null, needsReauth: true, reauthReason: "refresh_failed", credentialGeneration: requestCredentialGeneration },
405
428
  quotaProbeEvidence,
406
429
  );
407
430
  }
@@ -154,6 +154,34 @@ export function startupHealthSummary(health: StartupHealth): string {
154
154
  return action ? `${summary}; ${action}` : summary;
155
155
  }
156
156
 
157
+ /**
158
+ * What to say at the end of setup, once routing is on disk and the autostart choice is made.
159
+ *
160
+ * #5261: applying the Codex integration does not install a background service, and on Windows
161
+ * the scheduled task that a separate install would create is logon-triggered rather than
162
+ * boot-triggered. So "routing written, nothing listening" is an ordinary state after a restart
163
+ * rather than a corruption — and the user is never told, because setup ends on a success line.
164
+ *
165
+ * A boot trigger is not the missing piece and would be a false reassurance. The task runs as
166
+ * the interactive user, so before logon there is no session for it to run in; making it truly
167
+ * pre-logon means a different principal and a different service backend, not another trigger.
168
+ * What is actually missing is that nobody says the dependency exists, which is cheap to fix and
169
+ * true on every platform.
170
+ *
171
+ * Reuses the existing health model rather than re-deriving the condition, so this cannot drift
172
+ * from what `ocx status` and `ocx doctor` report about the same install.
173
+ */
174
+ export function injectedRoutingRestartWarningLines(health: StartupHealth): string[] {
175
+ if (health.status !== "at-risk") return [];
176
+ return [
177
+ // Deliberately not "nothing will restart the proxy": a healthy launcher shim does restart it,
178
+ // for CLI launches only, and is still at-risk. The summary line below says which case this is.
179
+ "⚠️ Codex routing is written to disk and survives a restart; keeping the proxy running is a separate matter.",
180
+ ` ${startupHealthSummary(health)}`,
181
+ " While the proxy is down Codex cannot sign in or reach a model. 'ocx restore' undoes the routing without needing it.",
182
+ ];
183
+ }
184
+
157
185
  function classifyStartupHealthSummary(health: StartupHealth): string {
158
186
  if (health.status === "native") return health.routingKind === "custom-remote"
159
187
  ? "custom remote Codex routing (no local restart dependency)"
@@ -1,4 +1,4 @@
1
- import { CODEX_REASONING_LEVELS } from "../../reasoning-effort";
1
+ import { CODEX_REASONING_LEVELS, type CodexReasoningLevel } from "../../reasoning-effort";
2
2
  import { clearModelCache } from "../model-cache";
3
3
  import { routedSlug, slugEquivalenceKey } from "../../providers/slug-codec";
4
4
  import { COMBO_NAMESPACE } from "../../combos";
@@ -872,7 +872,7 @@ export function mergeCatalogEntriesFromObservedState({
872
872
  // (luna: no ultra) stay intact.
873
873
  if (!freshCustomEntries.has(m) && !exactCombo && !reserveProjection && !String(e.slug ?? "").startsWith("opencode-go/")) {
874
874
  const levels = Array.isArray(e.supported_reasoning_levels)
875
- ? e.supported_reasoning_levels as Array<{ effort?: string }>
875
+ ? e.supported_reasoning_levels as Array<Partial<CodexReasoningLevel>>
876
876
  : [];
877
877
  if (levels.length > 0
878
878
  && !suppressedSyntheticMaxSlugs.has(String(e.slug ?? ""))
@@ -8,7 +8,7 @@ import { clearModelCache, DEFAULT_MODEL_CACHE_TTL_MS, getFreshCached, getStaleCa
8
8
  import { buildModelsRequest, resolveModelsAuthToken } from "../../oauth";
9
9
  import type { OcxConfig, OcxProviderConfig } from "../../types";
10
10
  import { modelInList } from "../../types";
11
- import { CODEX_REASONING_LEVELS, codexEffortRank, configuredReasoningEfforts, modelRecordValue, sanitizeCodexReasoningEfforts } from "../../reasoning-effort";
11
+ import { CODEX_REASONING_LEVELS, codexEffortRank, configuredReasoningEfforts, modelRecordValue, sanitizeCodexReasoningEfforts, type CodexReasoningLevel } from "../../reasoning-effort";
12
12
  import { getModelMetadata, getModelMetadataCaseInsensitive, listModelMetadata, resolveMetadataProvider } from "../../generated/model-metadata";
13
13
  import { enrichProviderFromRegistry, shouldCaseFoldMetadataModelId } from "../../providers/derive";
14
14
  import { getProviderRegistryEntry } from "../../providers/registry";
@@ -286,7 +286,7 @@ export function isGpt56NativeSlug(slug: string): boolean {
286
286
 
287
287
  export function ensureGpt56ReasoningLevels(entry: RawEntry): void {
288
288
  const levels = Array.isArray(entry.supported_reasoning_levels)
289
- ? entry.supported_reasoning_levels as Array<{ effort?: string }>
289
+ ? entry.supported_reasoning_levels as Array<Partial<CodexReasoningLevel>>
290
290
  : [];
291
291
  const out = [...levels];
292
292
  // max is a real native rung on the 5.6 family — always restored; ultra always advertised.
@@ -300,7 +300,7 @@ export function ensureGpt56ReasoningLevels(entry: RawEntry): void {
300
300
 
301
301
  export function ensureUltraReasoningLevel(entry: RawEntry): void {
302
302
  const levels = Array.isArray(entry.supported_reasoning_levels)
303
- ? entry.supported_reasoning_levels as Array<{ effort?: string }>
303
+ ? entry.supported_reasoning_levels as Array<Partial<CodexReasoningLevel>>
304
304
  : [];
305
305
  if (levels.length === 0) return;
306
306
  const wanted = ["max", "ultra"];
@@ -100,8 +100,9 @@ import type {
100
100
  CatalogSourceEvidence,
101
101
  CatalogTrustedOpenAiApiPolicySnapshot,
102
102
  } from "../convergence-types";
103
- import { applyRegistryCapabilitySeedFill, modelCapabilities, modelInputModalities } from "./model-hints";
103
+ import { modelCapabilities, modelInputModalities } from "./model-hints";
104
104
  import { configuredComboTargetModelsByProvider } from "./combo-member";
105
+ import { resolveModelPolicy } from "../../providers/resolved-model-policy";
105
106
 
106
107
  /** Concurrent gatherRoutedModels callers with the same catalog identity share one live discovery.
107
108
  * Keyed by gatherFlightKey so a different config cannot join or evict the wrong flight. */
@@ -344,8 +345,26 @@ export function captureProviderGather(
344
345
  ): CapturedProviderGather {
345
346
  const enriched = detachedClone(withCanonicalOpenAiForwardAuthDefault(name, configured));
346
347
  enrichProviderFromRegistry(name, enriched);
347
- applyRegistryCapabilitySeedFill(name, enriched);
348
348
  const registryTransportMatch = providerMatchesRegistryTransport(name, enriched);
349
+ const registryEntry = registryTransportMatch ? getProviderRegistryEntry(name) : undefined;
350
+ const staticProvider = resolveModelPolicy({
351
+ providerName: name,
352
+ modelId: enriched.defaultModel ?? "__catalog_capture__",
353
+ provider: enriched,
354
+ registryEntry,
355
+ transportMatchedRegistry: registryTransportMatch,
356
+ ...(enriched.authMode ? { effectiveAuth: { authMode: enriched.authMode } } : {}),
357
+ }).provider;
358
+ for (const key of [
359
+ "modelContextWindows", "modelInputModalities", "modelMaxInputTokens", "modelMaxOutputTokens",
360
+ "modelReasoningEfforts", "modelSupportsReasoningSummaries",
361
+ "modelSupportsVerbosity", "modelSupportsServiceTier",
362
+ ] as const) {
363
+ if (staticProvider[key] !== undefined) enriched[key] = detachedClone(staticProvider[key]) as never;
364
+ }
365
+ // A present modelDefaultReasoningEfforts object is a whole-map catalog authority, including
366
+ // explicit {}. enrichProviderFromRegistry already preserves/fills that contract; replacing it
367
+ // with the resolver's per-key registry fill would turn "no configured default" into a seed default.
349
368
  const provider = recursivelyFreeze(enriched);
350
369
  const fastPolicyAuthority = captureFastPolicyAuthority(
351
370
  name,
@@ -90,6 +90,7 @@ import type { CatalogModel } from "./parsing";
90
90
  import { disabledNativeSlugs, hasComboTargets, hasNativeOpenAiCapabilityMetadata, NATIVE_GPT56_MAX_INPUT_TOKENS, nativeContextLimits, nativeOpenAiCapabilityDisplayName, nativeDefaultReasoningEffort, nativeInputModalities, nativeOpenAiAutoCompactTokenLimit, nativeOpenAiContextWindow, nativeOpenAiMaxInputTokens, nativeOpenAiMaxOutputTokens, nativeOpenAiSlugs, nativeParallelToolCalls, nativeReasoningEfforts } from "./metadata";
91
91
  import { deriveComboCatalogModel, normalizedOpenAiApiSignature, openAiApiCollisionWarnings, replaceLastComboCatalogOmissions, warnUncataloguedComboOnce } from "./aggregation";
92
92
  import type { ComboCatalogOmission } from "./aggregation";
93
+ import { clampObservedModelLimits, resolveModelPolicy } from "../../providers/resolved-model-policy";
93
94
  import type { CatalogGatherProviderAuthEvidence } from "./filesystem-evidence";
94
95
  import type {
95
96
  CatalogAdmissionSnapshot,
@@ -246,22 +247,6 @@ export function configuredReasoningSummarySupport(prov: OcxProviderConfig | unde
246
247
  return modelRecordValue(prov.modelReasoningSummaryDelivery, id) !== undefined ? true : undefined;
247
248
  }
248
249
 
249
- function configuredVerbositySupport(name: string, prov: OcxProviderConfig | undefined, id: string): boolean | undefined {
250
- const explicit = prov ? modelRecordValue(prov.modelSupportsVerbosity, id) : undefined;
251
- if (explicit !== undefined) return explicit;
252
- if (!prov) return undefined;
253
- void name;
254
- // Provider-wide fallback for ids the per-model map does not enumerate — a live-discovered
255
- // model would otherwise re-advertise a control the upstream accepts and ignores.
256
- //
257
- // Read from the PROVIDER CONFIG, never from PROVIDER_REGISTRY. A gather flight captures its
258
- // registry authority up front and forbids any later registry read, so consulting the registry
259
- // here made a custom-destination flight fall back to "configured" instead of serving its own
260
- // discovery result (tests/codex-integration/codex-gather-authority.test.ts). `applyVerbosityDefaults` in
261
- // providers/derive.ts materializes the registry default into the config at seed/enrich time.
262
- return prov.supportsVerbosity;
263
- }
264
-
265
250
  export function applyProviderConfigHints(
266
251
  name: string,
267
252
  prov: OcxProviderConfig,
@@ -270,6 +255,15 @@ export function applyProviderConfigHints(
270
255
  metadataModelIdCaseFold?: boolean,
271
256
  effectiveAlias?: string | null,
272
257
  ): CatalogModel {
258
+ const staticPolicy = resolveModelPolicy({
259
+ providerName: name,
260
+ modelId: model.id,
261
+ provider: prov,
262
+ transportMatchedRegistry: false,
263
+ modelCapabilities: prov.modelCapabilities?.[model.id],
264
+ ...(prov.authMode ? { effectiveAuth: { authMode: prov.authMode } } : {}),
265
+ ...(effectiveAlias !== undefined ? { effectiveAlias } : {}),
266
+ });
273
267
  const displayName = configuredModelDisplayName(prov, model.id);
274
268
  // The alias decision is resolved once at flight admission (captureProviderGather) and threaded
275
269
  // through as `effectiveAlias`. Re-deriving it here would read PROVIDER_REGISTRY after admission,
@@ -279,11 +273,15 @@ export function applyProviderConfigHints(
279
273
  const providerAlias = typeof effectiveAlias === "string" || effectiveAlias === null
280
274
  ? effectiveAlias
281
275
  : model.providerAlias;
282
- const configuredCap = configuredContextWindow(prov, model.id);
283
- const configuredMaxInput = configuredMaxInputTokens(prov, model.id);
276
+ const configuredCap = staticPolicy.model.contextWindow ?? configuredContextWindow(prov, model.id);
277
+ const configuredMaxInput = staticPolicy.model.maxInputTokens;
284
278
  const maxOutputTokens = routedMaxOutputTokens(name, prov, model, model.id, metadataModelIdCaseFold);
285
279
  const configuredAutoCompact = configuredAutoCompactTokenLimit(prov, model.id);
286
- let inputModalities = configuredInputModalities(prov, model.id);
280
+ // The resolver owns exact capability precedence and legacy exact/colon-family/case-fold fallback.
281
+ // Removing an exact capability row therefore returns this projection to legacy-map inference.
282
+ let inputModalities = staticPolicy.model.inputModalities
283
+ ? [...staticPolicy.model.inputModalities]
284
+ : undefined;
287
285
  // The shared vision-sidecar consumer predicate keeps catalog advertisement and request-time
288
286
  // planning aligned. The catalog must still advertise image input — the Codex app
289
287
  // gates attachments client-side on input_modalities, and a text-only entry would block images
@@ -297,10 +295,12 @@ export function applyProviderConfigHints(
297
295
  }
298
296
  const reasoningEfforts = configuredReasoningEfforts(prov, model.id);
299
297
  const suppressSyntheticMax = modelRecordValue(prov.modelSuppressSyntheticMax, model.id) === true;
300
- const defaultReasoningEffort = modelRecordValue(prov.modelDefaultReasoningEfforts, model.id) ?? model.defaultReasoningEffort;
301
- const supportsReasoningSummaries = configuredReasoningSummarySupport(prov, model.id);
302
- const supportsVerbosity = configuredVerbositySupport(name, prov, model.id);
298
+ const defaultReasoningEffort = staticPolicy.model.defaultReasoningEffort ?? model.defaultReasoningEffort;
299
+ const supportsReasoningSummaries = staticPolicy.model.supportsReasoningSummaries;
300
+ const supportsVerbosity = staticPolicy.model.supportsVerbosity;
303
301
  const fastPolicy = fastPolicyForModel(prov, model.id, name);
302
+ // Frozen gather providers retain the captured Fast authority. That late/captured eligibility
303
+ // owns catalog publication, including an explicit provider-level false.
304
304
  const supportsServiceTier = serviceTierSupportFromPolicy(fastPolicy);
305
305
  const {
306
306
  supportsServiceTier: _staleServiceTier,
@@ -313,9 +313,12 @@ export function applyProviderConfigHints(
313
313
  const discoveredWindow = typeof model.contextWindow === "number" && model.contextWindow > 0
314
314
  ? model.contextWindow
315
315
  : undefined;
316
- const hintedWindow = discoveredWindow !== undefined
317
- ? (configuredCap !== undefined ? Math.min(discoveredWindow, configuredCap) : discoveredWindow)
318
- : (configuredCap ?? (providerCap !== undefined ? resolveUnknownRoutedContextWindow(providerCap) : undefined));
316
+ const projectedLimits = clampObservedModelLimits(staticPolicy.model, {
317
+ ...(discoveredWindow !== undefined ? { contextWindow: discoveredWindow } : {}),
318
+ ...(typeof model.maxInputTokens === "number" && model.maxInputTokens > 0 ? { maxInputTokens: model.maxInputTokens } : {}),
319
+ });
320
+ const hintedWindow = projectedLimits.contextWindow
321
+ ?? (providerCap !== undefined ? resolveUnknownRoutedContextWindow(providerCap) : undefined);
319
322
  const hinted = {
320
323
  ...modelWithoutServiceTier,
321
324
  ...(displayName !== undefined ? { displayName } : {}),
@@ -326,9 +329,7 @@ export function applyProviderConfigHints(
326
329
  ...(suppressSyntheticMax ? { suppressSyntheticMax: true } : {}),
327
330
  ...(configuredMaxInput !== undefined
328
331
  ? {
329
- maxInputTokens: typeof model.maxInputTokens === "number" && model.maxInputTokens > 0
330
- ? Math.min(model.maxInputTokens, configuredMaxInput)
331
- : configuredMaxInput,
332
+ maxInputTokens: projectedLimits.maxInputTokens ?? configuredMaxInput,
332
333
  }
333
334
  : {}),
334
335
  ...(maxOutputTokens !== undefined ? { maxOutputTokens } : {}),
@@ -803,6 +803,13 @@ export function normalizeRoutedCatalogEntry(
803
803
  delete entry.multi_agent_reasoning_effort;
804
804
  delete entry.use_responses_lite;
805
805
  delete entry.supports_websockets;
806
+ // Routed rows cloned from native templates must not inherit OpenAI-only experimental context
807
+ // delivery. Codex reads the flag as "this model accepts experimental context history" and drives
808
+ // its context-management cadence from it, so a third-party provider that never negotiated it gets
809
+ // a compact-after-every-step loop instead (observed on a routed DeepSeek row: 1,600+ compactions
810
+ // in a single thread). Nothing re-applies the field from provider metadata during this step, so
811
+ // the row leaves this normalization without it.
812
+ delete entry.supports_experimental_context;
806
813
  /*
807
814
  * Tier metadata is stripped from routed rows because a row cloned from a native template
808
815
  * would otherwise hand a third-party provider OpenAI's tiers.