@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
@@ -2,6 +2,7 @@ import * as readline from "node:readline";
2
2
  import { modelSelectionGuidance } from "../cli/model-selection-guidance";
3
3
  import { initializeProviderModelSelection } from "../providers/initial-model-selection";
4
4
  import { openUrl } from "../lib/open-url";
5
+ import { createBrowserLaunchReport } from "../lib/browser-launch-notice";
5
6
  import { loadConfig, saveConfig } from "../config";
6
7
  import { findLiveProxy } from "../server/proxy-liveness";
7
8
  import {
@@ -14,6 +15,41 @@ import type { OcxConfig, OcxProviderConfig } from "../types";
14
15
  import { configuredAdminToken } from "../lib/admin-secrets";
15
16
  import { codexAccountNamespaceProviderCollisionError } from "../codex/account-namespace-match";
16
17
 
18
+ /**
19
+ * Seams a test drives in place of a browser, a terminal and a real provider. Production passes
20
+ * none of them.
21
+ *
22
+ * They exist because the thing worth proving here is an ORDER — that a browser which did not
23
+ * open is on screen before the question that assumes it did — and an order is only observable
24
+ * from something that records both events. Spawning a launcher and attaching to stdin to find
25
+ * that out would test the operating system instead.
26
+ */
27
+ export interface LoginCliDeps {
28
+ runLogin?: typeof runLogin;
29
+ openUrl?: typeof openUrl;
30
+ warn?: (message: string) => void;
31
+ /** Ask one question, read one line. Defaults to a readline prompt that owns its own lifetime. */
32
+ ask?: (question: string) => Promise<string>;
33
+ }
34
+
35
+ /**
36
+ * Run `body` with a line reader, creating and closing a real one only when the caller did not
37
+ * supply its own. A readline interface attaches to stdin, so building one that nothing will ask
38
+ * a question keeps the process alive for no reason.
39
+ */
40
+ async function withPrompt<T>(
41
+ supplied: ((question: string) => Promise<string>) | undefined,
42
+ body: (ask: (question: string) => Promise<string>) => Promise<T>,
43
+ ): Promise<T> {
44
+ if (supplied) return await body(supplied);
45
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
46
+ try {
47
+ return await body(question => new Promise<string>(resolve => rl.question(question, resolve)));
48
+ } finally {
49
+ rl.close();
50
+ }
51
+ }
52
+
17
53
  const LIVE_RELOAD_PROVIDERS = new Set<string>([
18
54
  ...listOAuthProviders(),
19
55
  ...Object.keys(KEY_LOGIN_PROVIDERS),
@@ -83,7 +119,7 @@ export function loginUsageMessage(): string {
83
119
  + ` API-key login: ${Object.keys(KEY_LOGIN_PROVIDERS).join(", ")}`;
84
120
  }
85
121
 
86
- export async function handleLogin(provider?: string): Promise<void> {
122
+ export async function handleLogin(provider?: string, deps: LoginCliDeps = {}): Promise<void> {
87
123
  const name = (provider ?? "").trim().toLowerCase();
88
124
  // A removed provider id reached through its alias still logs in — the merged
89
125
  // successor owns the flow. Warn rather than silently reroute so scripts and
@@ -91,30 +127,40 @@ export async function handleLogin(provider?: string): Promise<void> {
91
127
  const alias = DEPRECATED_OAUTH_PROVIDER_ALIASES[name];
92
128
  if (alias) {
93
129
  console.error(`${name} is deprecated; logging in as ${alias}`);
94
- return handleOAuthLogin(alias);
130
+ return handleOAuthLogin(alias, deps);
95
131
  }
96
- if (isPublicOAuthProvider(name)) return handleOAuthLogin(name);
97
- if (isKeyLoginProvider(name)) return handleKeyLogin(name);
132
+ if (isPublicOAuthProvider(name)) return handleOAuthLogin(name, deps);
133
+ if (isKeyLoginProvider(name)) return handleKeyLogin(name, deps);
98
134
  console.error(loginUsageMessage());
99
135
  process.exit(1);
100
136
  }
101
137
 
102
- async function handleOAuthLogin(name: string): Promise<void> {
103
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
104
- try {
105
- await runLogin(name, {
138
+ export async function handleOAuthLogin(name: string, deps: LoginCliDeps = {}): Promise<void> {
139
+ const login = deps.runLogin ?? runLogin;
140
+ const launch = deps.openUrl ?? openUrl;
141
+ const browser = createBrowserLaunchReport(deps.warn);
142
+ await withPrompt(deps.ask, async (ask) => {
143
+ await login(name, {
106
144
  onAuth: ({ url, instructions }) => {
107
145
  console.log(`\n🔐 Opening browser for ${name} login...\n${url}\n`);
108
146
  if (instructions) console.log(instructions);
109
- openUrl(url);
147
+ // The controller does not await onAuth, so the launcher's answer cannot be reported from
148
+ // here — this returns long before it arrives. It reports itself instead, and the one
149
+ // thing that could collide with it waits below (#5261).
150
+ browser.track(launch(url));
110
151
  },
111
152
  onProgress: (m) => console.log(` ${m}`),
112
- onManualCodeInput: () =>
113
- new Promise((res) => rl.question("Paste redirect URL or code (or wait for browser): ", res)),
153
+ onManualCodeInput: async () => {
154
+ // "or wait for browser" is a lie if nothing opened, and a warning printed after readline
155
+ // has drawn the prompt lands on the line the user is typing on.
156
+ await browser.settled();
157
+ return await ask("Paste redirect URL or code (or wait for browser): ");
158
+ },
114
159
  });
115
- } finally {
116
- rl.close();
117
- }
160
+ });
161
+ // A device or polling provider never prompts, so nothing above waited on the launcher. It is
162
+ // still owed an answer before this claims the login worked.
163
+ await browser.settled();
118
164
  const reload = await notifyRunningProxyAfterOAuthLogin(name);
119
165
  console.log(`\n✅ Logged in to ${name}. Try: ocx sync`);
120
166
  for (const line of modelSelectionGuidance(name)) console.log(line);
@@ -192,8 +238,9 @@ export async function commitKeyLoginProvider(
192
238
  return mergedProvider;
193
239
  }
194
240
 
195
- async function handleKeyLogin(name: string): Promise<void> {
241
+ export async function handleKeyLogin(name: string, deps: LoginCliDeps = {}): Promise<void> {
196
242
  const def = KEY_LOGIN_PROVIDERS[name];
243
+ const launch = deps.openUrl ?? openUrl;
197
244
  const preflightConfig = loadConfig();
198
245
  const namespaceCollision = codexAccountNamespaceProviderCollisionError(preflightConfig.codexAccountNamespaces, name);
199
246
  if (namespaceCollision) {
@@ -201,21 +248,25 @@ async function handleKeyLogin(name: string): Promise<void> {
201
248
  process.exit(1);
202
249
  }
203
250
  console.log(`\n🔑 ${def.label} — opening ${def.dashboardUrl} so you can create/copy an API key...`);
204
- openUrl(def.dashboardUrl);
205
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
206
- const key = (await new Promise<string>((res) => rl.question(`Paste your ${def.label} API key: `, res))).trim();
207
- // Template URL with placeholders needs resolution before saving.
208
- let baseUrl = def.baseUrl;
209
- if (/\{[^}]*\}/.test(baseUrl)) {
210
- const resolved = (await new Promise<string>((res) => rl.question(`Your endpoint URL (${baseUrl}): `, res))).trim();
211
- if (!resolved) {
212
- rl.close();
213
- console.error("A resolved URL is required — replace the {placeholder} with your actual value.");
214
- process.exit(1);
251
+ const browser = createBrowserLaunchReport(deps.warn);
252
+ browser.track(launch(def.dashboardUrl));
253
+ // The next question asks for a key the user gets FROM that page, so a page that never opened
254
+ // has to be on screen before the question rather than underneath it (#5261).
255
+ await browser.settled();
256
+ const { key, baseUrl } = await withPrompt(deps.ask, async (ask) => {
257
+ const entered = (await ask(`Paste your ${def.label} API key: `)).trim();
258
+ // Template URL with placeholders needs resolution before saving.
259
+ let resolvedBaseUrl = def.baseUrl;
260
+ if (/\{[^}]*\}/.test(resolvedBaseUrl)) {
261
+ const resolved = (await ask(`Your endpoint URL (${resolvedBaseUrl}): `)).trim();
262
+ if (!resolved) {
263
+ console.error("A resolved URL is required — replace the {placeholder} with your actual value.");
264
+ process.exit(1);
265
+ }
266
+ resolvedBaseUrl = resolved;
215
267
  }
216
- baseUrl = resolved;
217
- }
218
- rl.close();
268
+ return { key: entered, baseUrl: resolvedBaseUrl };
269
+ });
219
270
  if (!key) {
220
271
  console.error("No key entered.");
221
272
  process.exit(1);
@@ -0,0 +1,127 @@
1
+ import { parseCallbackInput } from "./callback-server";
2
+ import { retainedUtf8Bytes } from "../lib/admission";
3
+ import type { GenerationContext } from "../lib/state-store-sweeper";
4
+
5
+ /**
6
+ * In-flight login flow state and the manual paste slot that feeds it.
7
+ *
8
+ * Split out of `index.ts` when that file crossed the repository file-size ratchet at 2009
9
+ * lines. The cut follows a seam rather than a line count: everything here is bookkeeping for
10
+ * a login that has started and not yet settled, and none of it reads or writes a stored
11
+ * credential. `index.ts` re-exports the two public names, so existing importers are unaffected.
12
+ */
13
+ export const loginState = new Map<string, { error?: string; done: boolean }>();
14
+ export const loginAbort = new Map<string, AbortController>();
15
+ export const kiroLoginSettling = new Set<string>();
16
+
17
+ /** Pending paste for a login in progress: either a waiter or a stashed early submission. */
18
+ export interface ManualCodeSlot {
19
+ pendingInput?: string;
20
+ resolve?: (value: string) => void;
21
+ /** Registered by the callback flow so submits can validate state synchronously. */
22
+ expectedState?: string;
23
+ }
24
+ const loginManual = new Map<string, ManualCodeSlot>();
25
+ const OAUTH_PENDING_CODE_MAX_BYTES = 4 * 1024;
26
+ let lastOAuthFlowReconciledGeneration = 0;
27
+
28
+ export function reconcileOAuthFlowState(context: GenerationContext): number {
29
+ if (context.generation <= lastOAuthFlowReconciledGeneration) return 0;
30
+ let removed = 0;
31
+ for (const [provider, state] of loginState) {
32
+ if (context.providerNames.has(provider) || !state.done || loginAbort.has(provider)) continue;
33
+ if (loginState.delete(provider)) removed += 1;
34
+ if (loginManual.delete(provider)) removed += 1;
35
+ if (loginAbort.delete(provider)) removed += 1;
36
+ }
37
+ lastOAuthFlowReconciledGeneration = context.generation;
38
+ return removed;
39
+ }
40
+
41
+ export function clearManualCodeSlot(provider: string): void {
42
+ loginManual.delete(provider);
43
+ }
44
+
45
+ export function ensureManualCodeSlot(provider: string): ManualCodeSlot {
46
+ let slot = loginManual.get(provider);
47
+ if (!slot) {
48
+ slot = {};
49
+ loginManual.set(provider, slot);
50
+ }
51
+ return slot;
52
+ }
53
+
54
+ /** Wait for a GUI/CLI paste of the OAuth redirect URL or code (or return a stashed early submit). */
55
+ export function waitForManualLoginCode(provider: string, signal: AbortSignal, expectedState?: string): Promise<string> {
56
+ if (signal.aborted) {
57
+ return Promise.reject(new Error(`OAuth callback cancelled: ${signal.reason}`));
58
+ }
59
+ const slot = ensureManualCodeSlot(provider);
60
+ if (expectedState !== undefined) slot.expectedState = expectedState;
61
+ if (slot.pendingInput !== undefined) {
62
+ const value = slot.pendingInput;
63
+ slot.pendingInput = undefined;
64
+ return Promise.resolve(value);
65
+ }
66
+ return new Promise<string>((resolve, reject) => {
67
+ const onAbort = () => {
68
+ if (slot.resolve === resolve) slot.resolve = undefined;
69
+ reject(new Error(`OAuth callback cancelled: ${signal.reason}`));
70
+ };
71
+ signal.addEventListener("abort", onAbort, { once: true });
72
+ slot.resolve = (value: string) => {
73
+ signal.removeEventListener("abort", onAbort);
74
+ if (slot.resolve === resolve) slot.resolve = undefined;
75
+ resolve(value);
76
+ };
77
+ });
78
+ }
79
+
80
+ /**
81
+ * Feed a pasted redirect URL or authorization code into an in-progress GUI login.
82
+ * Returns ok:false when no login is waiting (or input is empty). Invalid pastes are accepted
83
+ * here and re-prompted by the OAuth callback loop if they cannot be parsed / fail state checks.
84
+ */
85
+ export function submitManualLoginCode(provider: string, input: string): { ok: true } | { ok: false; error: string } {
86
+ const trimmed = input.trim();
87
+ if (!trimmed) return { ok: false, error: "empty code" };
88
+ if (retainedUtf8Bytes(trimmed) > OAUTH_PENDING_CODE_MAX_BYTES) return { ok: false, error: "code too large" };
89
+ const st = loginState.get(provider);
90
+ if (!st || st.done) return { ok: false, error: "no login in progress" };
91
+ const slot = ensureManualCodeSlot(provider);
92
+ // Synchronous validation (validated request/ack): reject un-parseable input and
93
+ // authorization responses (url/query kind) whose state is missing or mismatched
94
+ // once the flow has registered its expected state. Raw codes stay in-session-PKCE
95
+ // protected — but a raw paste carrying an explicit #state suffix is state-bearing
96
+ // and checked too. Early posts (flow not yet waiting, no expectedState) are
97
+ // stashed and re-validated by the callback loop.
98
+ const parsed = parseCallbackInput(trimmed);
99
+ // Command Code's manual fallback accepts a pasted JSON callback payload
100
+ // (`{ apiKey, state, ... }`) which has no `code` param. Let it through the
101
+ // shared gate so the provider-specific parser can validate it.
102
+ const isCommandCodeJson = provider === "command-code" && trimmed.startsWith("{") && !parsed.code;
103
+ if (!parsed.code && !isCommandCodeJson) return { ok: false, error: "no authorization code found in input" };
104
+ // A raw paste carrying an explicit code#state suffix is state-bearing too: it
105
+ // must match the expected state rather than bypass validation.
106
+ const stateBearing = parsed.kind !== "raw" || parsed.state !== undefined;
107
+ if (stateBearing && slot.expectedState !== undefined) {
108
+ if (parsed.state === undefined) return { ok: false, error: "redirect URL is missing the state parameter" };
109
+ if (parsed.state !== slot.expectedState) {
110
+ return {
111
+ ok: false,
112
+ error: parsed.kind === "raw"
113
+ ? "state mismatch — paste the bare code, or the correct code#state from THIS login attempt"
114
+ : "state mismatch — paste the redirect URL from THIS login attempt",
115
+ };
116
+ }
117
+ }
118
+ if (slot.resolve) {
119
+ const resolve = slot.resolve;
120
+ slot.resolve = undefined;
121
+ resolve(trimmed);
122
+ } else {
123
+ // Race: GUI may POST before the flow reaches onManualCodeInput — stash for the waiter.
124
+ slot.pendingInput = trimmed;
125
+ }
126
+ return { ok: true };
127
+ }
@@ -0,0 +1,133 @@
1
+ /**
2
+ * Read-path for provider key material: env references, keychain references, or the literal
3
+ * value.
4
+ *
5
+ * Leaf module on purpose: reasoning-metadata.ts imports resolveProviderApiKey() here so a
6
+ * learned-refusal identity can hash the same credential the wire sends. key-store.ts needs
7
+ * the ../config barrel for the write path (saveConfigPreservingClaudeCode), and importing it
8
+ * from reasoning-metadata would close the cycle the reasoning-metadata header warns about.
9
+ * resolveEnvValue comes from ../config/proxy-env for the same reason.
10
+ *
11
+ * `config.json` keeps only a reference (`keychain:<provider>` for the active key,
12
+ * `keychain:<provider>/<pool id>` for pool entries); the secret lives in the OS credential store
13
+ * under one service name. Reads are synchronous on purpose: `routedProviderConfig` and the
14
+ * quota/compaction/catalog callers are all sync, and `@napi-rs/keyring` ships a sync `Entry`.
15
+ *
16
+ * Policy: a reference that cannot be resolved fails closed (no key) and is warned once per
17
+ * account; nothing ever rewrites plaintext into config or its backups.
18
+ */
19
+ import { createRequire } from "node:module";
20
+ import { resolveEnvValue } from "../config/proxy-env";
21
+ import type { OcxProviderConfig } from "../types";
22
+
23
+ export const KEYCHAIN_REFERENCE_PREFIX = "keychain:";
24
+ export const PROVIDER_KEYCHAIN_SERVICE = "opencodex.provider-api-key.v1";
25
+
26
+ export interface ProviderKeychainEntry {
27
+ getPassword(): string | null;
28
+ setPassword(password: string): void;
29
+ deletePassword(): boolean;
30
+ }
31
+
32
+ export type ProviderKeychainEntryFactory = (service: string, account: string) => ProviderKeychainEntry;
33
+
34
+ const nodeRequire = createRequire(import.meta.url);
35
+
36
+ function defaultEntryFactory(service: string, account: string): ProviderKeychainEntry {
37
+ const { Entry } = nodeRequire("@napi-rs/keyring") as { Entry: new (s: string, a: string) => ProviderKeychainEntry };
38
+ return new Entry(service, account);
39
+ }
40
+
41
+ let entryFactory: ProviderKeychainEntryFactory = defaultEntryFactory;
42
+ const resolvedCache = new Map<string, string>();
43
+ const warnedAccounts = new Set<string>();
44
+
45
+ /** Test seam: swap the OS entry for an in-memory one and drop caches. */
46
+ export function setProviderKeychainEntryFactoryForTests(factory: ProviderKeychainEntryFactory | null): void {
47
+ entryFactory = factory ?? defaultEntryFactory;
48
+ resolvedCache.clear();
49
+ warnedAccounts.clear();
50
+ }
51
+
52
+ /** Write-path seam: a store/restore mutated secrets, so cached reads and warnings are stale. */
53
+ export function invalidateResolvedProviderKeyCache(): void {
54
+ resolvedCache.clear();
55
+ warnedAccounts.clear();
56
+ }
57
+
58
+ export function isKeychainReference(value: string | undefined): value is string {
59
+ return typeof value === "string" && value.startsWith(KEYCHAIN_REFERENCE_PREFIX) && value.length > KEYCHAIN_REFERENCE_PREFIX.length;
60
+ }
61
+
62
+ export function keychainAccount(reference: string): string {
63
+ return reference.slice(KEYCHAIN_REFERENCE_PREFIX.length);
64
+ }
65
+
66
+ /**
67
+ * A reference belongs to `name` only when its account is that provider's own active account
68
+ * or one of its pool accounts. `storeProviderKeyInKeychain` writes exactly those two shapes,
69
+ * so anything else in a provider's config names another provider's secret.
70
+ */
71
+ export function keychainReferenceBelongsToProvider(reference: string, name: string): boolean {
72
+ const account = keychainAccount(reference);
73
+ return account === name || account.startsWith(`${name}/`);
74
+ }
75
+
76
+ /** Entry for `account` under the provider-key service; read and write paths share the factory. */
77
+ export function providerKeychainEntry(account: string): ProviderKeychainEntry {
78
+ return entryFactory(PROVIDER_KEYCHAIN_SERVICE, account);
79
+ }
80
+
81
+ function readKeychain(account: string): string | undefined {
82
+ const cached = resolvedCache.get(account);
83
+ if (cached !== undefined) return cached;
84
+ try {
85
+ const value = providerKeychainEntry(account).getPassword();
86
+ if (typeof value === "string" && value.trim()) {
87
+ resolvedCache.set(account, value);
88
+ return value;
89
+ }
90
+ } catch {
91
+ // fall through to the single warning below
92
+ }
93
+ if (!warnedAccounts.has(account)) {
94
+ warnedAccounts.add(account);
95
+ console.warn(`[opencodex] provider key reference keychain:${account} could not be read from the OS keychain; requests for this provider have no credential until the keychain is available (no plaintext fallback)`);
96
+ }
97
+ return undefined;
98
+ }
99
+
100
+ /**
101
+ * Single resolver for provider key material: env references, keychain references, or the
102
+ * literal value. Every request-time read of `apiKey` goes through here.
103
+ */
104
+ export function resolveProviderApiKey(value: string | undefined): string | undefined {
105
+ if (!value) return undefined;
106
+ if (isKeychainReference(value)) return readKeychain(keychainAccount(value));
107
+ return resolveEnvValue(value);
108
+ }
109
+
110
+ export type ProviderKeyStoreKind = "keychain" | "env" | "file" | "none";
111
+
112
+ export function providerKeyStoreKind(provider: Pick<OcxProviderConfig, "apiKey"> | undefined): ProviderKeyStoreKind {
113
+ const key = provider?.apiKey;
114
+ if (!key) return "none";
115
+ if (isKeychainReference(key)) return "keychain";
116
+ if (/^\$\{?\w+\}?$/.test(key)) return "env";
117
+ return "file";
118
+ }
119
+
120
+ /** Probe the OS keychain with a throwaway account: write, read back, delete. */
121
+ export function probeProviderKeychain(): { available: true } | { available: false; reason: string } {
122
+ const account = `probe-${process.pid}-${Date.now()}`;
123
+ try {
124
+ const entry = providerKeychainEntry(account);
125
+ entry.setPassword("ok");
126
+ const back = entry.getPassword();
127
+ try { entry.deletePassword(); } catch { /* best effort */ }
128
+ if (back !== "ok") return { available: false, reason: "keychain read-back did not match" };
129
+ return { available: true };
130
+ } catch (error) {
131
+ return { available: false, reason: error instanceof Error ? error.message : "keychain unavailable" };
132
+ }
133
+ }
@@ -1,6 +1,7 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { mutatePersistedConfig } from "../config";
3
3
  import { publishAccountSelection } from "../lib/account-selection-events";
4
+ import { clearModelCache } from "../codex/model-cache";
4
5
  import type { OcxConfig, OcxProviderConfig } from "../types";
5
6
  import type { ProviderApiKeySelection } from "../types/provider";
6
7
  import { routedProviderConfig } from "../router";
@@ -100,6 +101,9 @@ export function commitProviderApiKeySelection<T>(
100
101
  if (outcome.status === "unavailable") return { status: "unavailable" };
101
102
  const committed = outcome.value;
102
103
  if (committed.status !== "unavailable") config.providers[name] = structuredClone(committed.provider);
103
- if (committed.status === "committed" && committed.notify) publishAccountSelection(name, "api-key");
104
+ if (committed.status === "committed" && committed.notify) {
105
+ clearModelCache(name);
106
+ publishAccountSelection(name, "api-key");
107
+ }
104
108
  return committed;
105
109
  }
@@ -1,5 +1,6 @@
1
1
  import type { CodexAccountMode, OcxProviderConfig } from "../types";
2
2
  import { cloneFastWire } from "./fastwire";
3
+ import { resolveModelPolicy } from "./resolved-model-policy";
3
4
  import {
4
5
  PROVIDER_REGISTRY,
5
6
  registryEntryForProviderDestination,
@@ -220,6 +221,14 @@ export function applyDirectReasoningEffortContracts(
220
221
  */
221
222
  export function providerConfigSeed(entry: ProviderRegistryEntry): OcxProviderConfig {
222
223
  const liveModels = registryEntrySupportsLiveModelDiscovery(entry) ? entry.liveModels : false;
224
+ const staticPolicy = resolveModelPolicy({
225
+ providerName: entry.id,
226
+ modelId: entry.defaultModel ?? entry.models?.[0] ?? "__provider_seed__",
227
+ provider: { adapter: entry.adapter, baseUrl: entry.baseUrl, authMode: entry.authKind },
228
+ registryEntry: entry,
229
+ transportMatchedRegistry: true,
230
+ effectiveAuth: { authMode: entry.authKind },
231
+ }).provider;
223
232
  return {
224
233
  adapter: entry.adapter,
225
234
  baseUrl: entry.baseUrl,
@@ -239,17 +248,17 @@ export function providerConfigSeed(entry: ProviderRegistryEntry): OcxProviderCon
239
248
  ...(entry.models ? { models: [...entry.models] } : {}),
240
249
  ...(liveModels !== undefined ? { liveModels } : {}),
241
250
  ...(entry.contextWindow !== undefined ? { contextWindow: entry.contextWindow } : {}),
242
- ...(entry.modelContextWindows ? { modelContextWindows: { ...entry.modelContextWindows } } : {}),
243
- ...(entry.modelDisplayNames ? { modelDisplayNames: { ...entry.modelDisplayNames } } : {}),
244
- ...(entry.modelInputModalities ? { modelInputModalities: cloneRecordOfArrays(entry.modelInputModalities) } : {}),
245
- ...(entry.modelMaxInputTokens ? { modelMaxInputTokens: { ...entry.modelMaxInputTokens } } : {}),
251
+ ...(staticPolicy.modelContextWindows ? { modelContextWindows: { ...staticPolicy.modelContextWindows } } : {}),
252
+ ...(staticPolicy.modelDisplayNames ? { modelDisplayNames: { ...staticPolicy.modelDisplayNames } } : {}),
253
+ ...(staticPolicy.modelInputModalities ? { modelInputModalities: cloneRecordOfArrays(staticPolicy.modelInputModalities) } : {}),
254
+ ...(staticPolicy.modelMaxInputTokens ? { modelMaxInputTokens: { ...staticPolicy.modelMaxInputTokens } } : {}),
246
255
  ...(entry.defaultMaxOutputTokens !== undefined ? { defaultMaxOutputTokens: entry.defaultMaxOutputTokens } : {}),
247
- ...(entry.modelMaxOutputTokens ? { modelMaxOutputTokens: { ...entry.modelMaxOutputTokens } } : {}),
256
+ ...(staticPolicy.modelMaxOutputTokens ? { modelMaxOutputTokens: { ...staticPolicy.modelMaxOutputTokens } } : {}),
248
257
  ...(entry.reasoningEfforts ? { reasoningEfforts: [...entry.reasoningEfforts] } : {}),
249
- ...(entry.modelReasoningEfforts ? { modelReasoningEfforts: cloneRecordOfArrays(entry.modelReasoningEfforts) } : {}),
250
- ...(entry.modelDefaultReasoningEfforts ? { modelDefaultReasoningEfforts: { ...entry.modelDefaultReasoningEfforts } } : {}),
251
- ...(entry.reasoningEffortMap ? { reasoningEffortMap: { ...entry.reasoningEffortMap } } : {}),
252
- ...(entry.modelReasoningEffortMap ? { modelReasoningEffortMap: cloneNestedRecord(entry.modelReasoningEffortMap) } : {}),
258
+ ...(staticPolicy.modelReasoningEfforts ? { modelReasoningEfforts: cloneRecordOfArrays(staticPolicy.modelReasoningEfforts) } : {}),
259
+ ...(staticPolicy.modelDefaultReasoningEfforts ? { modelDefaultReasoningEfforts: { ...staticPolicy.modelDefaultReasoningEfforts } } : {}),
260
+ ...(staticPolicy.reasoningEffortMap ? { reasoningEffortMap: { ...staticPolicy.reasoningEffortMap } } : {}),
261
+ ...(staticPolicy.modelReasoningEffortMap ? { modelReasoningEffortMap: cloneNestedRecord(staticPolicy.modelReasoningEffortMap) } : {}),
253
262
  ...(entry.reasoningWireFormat ? { reasoningWireFormat: entry.reasoningWireFormat } : {}),
254
263
  ...(entry.noVisionModels ? { noVisionModels: [...entry.noVisionModels] } : {}),
255
264
  ...(entry.noReasoningModels ? { noReasoningModels: [...entry.noReasoningModels] } : {}),
@@ -447,13 +456,16 @@ function applyVerbosityDefaults(prov: OcxProviderConfig, entry: ProviderRegistry
447
456
  * was skipped and the reasoning ladder was advertised without summary support — exactly the
448
457
  * inconsistency that makes Codex drop the inbound reasoning object.
449
458
  *
450
- * Deliberately narrow: reasoning-summary and effort metadata only, via
459
+ * Deliberately narrow: reasoning-summary, effort, and replay-compatibility metadata only, via
451
460
  * `registryEntryForProviderDestination`, which matches fixed key destinations and refuses
452
461
  * templated or overridable base URLs. A custom row keeps its own identity for everything else.
453
462
  */
454
463
  function enrichReasoningMetadataByDestination(prov: OcxProviderConfig): void {
455
464
  const destination = registryEntryForProviderDestination(prov);
456
465
  applyReasoningSummaryDefaults(prov, destination?.modelSupportsReasoningSummaries);
466
+ if (prov.dropResponsesReasoningItems === undefined && destination?.dropResponsesReasoningItems !== undefined) {
467
+ prov.dropResponsesReasoningItems = destination.dropResponsesReasoningItems;
468
+ }
457
469
  if (destination?.modelReasoningEfforts) {
458
470
  prov.modelReasoningEfforts = fillRecordOfArrays(destination.modelReasoningEfforts, prov.modelReasoningEfforts);
459
471
  }
@@ -491,6 +503,14 @@ export function enrichProviderFromRegistry(name: string, prov: OcxProviderConfig
491
503
  modelReasoningEffortMap: prov.modelReasoningEffortMap,
492
504
  };
493
505
  const seed = providerConfigSeed(entry);
506
+ const resolvedStatic = resolveModelPolicy({
507
+ providerName: name,
508
+ modelId: prov.defaultModel ?? entry.defaultModel ?? "__provider_enrich__",
509
+ provider: prov,
510
+ registryEntry: entry,
511
+ transportMatchedRegistry: true,
512
+ ...(prov.authMode ? { effectiveAuth: { authMode: prov.authMode } } : {}),
513
+ }).provider;
494
514
  repairStaticModelCatalogProvider(name, prov);
495
515
  if (prov.apiKeyTransport === undefined && seed.apiKeyTransport !== undefined) prov.apiKeyTransport = seed.apiKeyTransport;
496
516
  if (!prov.defaultModel && seed.defaultModel) prov.defaultModel = seed.defaultModel;
@@ -504,10 +524,8 @@ export function enrichProviderFromRegistry(name: string, prov: OcxProviderConfig
504
524
  if (!prov.modelContextWindows && seed.modelContextWindows) prov.modelContextWindows = { ...seed.modelContextWindows };
505
525
  // Per-model fill, not all-or-nothing: an operator who renamed ONE model must still receive
506
526
  // labels for the rest, and an existing install must pick up newly seeded rows on enrich.
507
- if (seed.modelDisplayNames) {
508
- prov.modelDisplayNames = { ...seed.modelDisplayNames, ...(prov.modelDisplayNames ?? {}) };
509
- }
510
- if (seed.modelInputModalities) prov.modelInputModalities = fillRecordOfArrays(seed.modelInputModalities, prov.modelInputModalities);
527
+ if (resolvedStatic.modelDisplayNames) prov.modelDisplayNames = { ...resolvedStatic.modelDisplayNames };
528
+ if (resolvedStatic.modelInputModalities) prov.modelInputModalities = cloneRecordOfArrays(resolvedStatic.modelInputModalities);
511
529
  if (prov.defaultMaxOutputTokens === undefined && seed.defaultMaxOutputTokens !== undefined) prov.defaultMaxOutputTokens = seed.defaultMaxOutputTokens;
512
530
  if (!prov.modelMaxOutputTokens && seed.modelMaxOutputTokens) prov.modelMaxOutputTokens = { ...seed.modelMaxOutputTokens };
513
531
  if ((!prov.reasoningEfforts || hasLegacyClinePassReasoningEfforts(name, prov)) && seed.reasoningEfforts) {
@@ -518,9 +536,7 @@ export function enrichProviderFromRegistry(name: string, prov: OcxProviderConfig
518
536
  // provider. That split the two planes apart — routing merges these maps per key
519
537
  // (mergeRecordFill in src/router.ts), so the wire honored the effort while /v1/models and
520
538
  // every client export showed no effort control at all.
521
- if (seed.modelReasoningEfforts) {
522
- prov.modelReasoningEfforts = fillRecordOfArrays(seed.modelReasoningEfforts, prov.modelReasoningEfforts);
523
- }
539
+ if (resolvedStatic.modelReasoningEfforts) prov.modelReasoningEfforts = cloneRecordOfArrays(resolvedStatic.modelReasoningEfforts);
524
540
  if (!prov.modelDefaultReasoningEfforts && seed.modelDefaultReasoningEfforts) prov.modelDefaultReasoningEfforts = { ...seed.modelDefaultReasoningEfforts };
525
541
  if (!prov.reasoningEffortMap && seed.reasoningEffortMap) prov.reasoningEffortMap = { ...seed.reasoningEffortMap };
526
542
  if (!prov.modelReasoningEffortMap && seed.modelReasoningEffortMap) prov.modelReasoningEffortMap = cloneNestedRecord(seed.modelReasoningEffortMap);
@@ -563,6 +579,7 @@ export function enrichProviderFromRegistry(name: string, prov: OcxProviderConfig
563
579
  prov.supportsResponsesCustomTools = entry.supportsResponsesCustomTools;
564
580
  }
565
581
  if (prov.preserveResponsesReasoningContent === undefined && entry.preserveResponsesReasoningContent !== undefined) prov.preserveResponsesReasoningContent = entry.preserveResponsesReasoningContent;
582
+ if (prov.dropResponsesReasoningItems === undefined && entry.dropResponsesReasoningItems !== undefined) prov.dropResponsesReasoningItems = entry.dropResponsesReasoningItems;
566
583
  applyReasoningSummaryDefaults(prov, entry.modelSupportsReasoningSummaries);
567
584
  applyServiceTierModelDefaults(prov, serviceTierModelDefaultsFor(entry, prov));
568
585
  applyVerbosityDefaults(prov, entry);
@@ -12,14 +12,12 @@
12
12
  * key: the `devin-cli` registry entry is gone, so the PROVIDER_REGISTRY lookup
13
13
  * that gated the rewrite here could never fire again.
14
14
  *
15
- * The adapter repair must keep working for CUSTOM-NAMED rows. The ACP adapter
16
- * was removed, so `"devin-cli"` is not a constructible adapter id and
17
- * `createRegisteredAdapter` would throw `Unknown adapter: devin-cli`. The
18
- * canonical `devin` row gets its adapter pinned from the registry by
19
- * `routedProviderConfig`, but a row named e.g. `"devin-acp"` has nothing
20
- * pinning it and would fail every request. So every row naming the retired
21
- * adapter is rewritten to `devin`, whatever the row is called, and each
22
- * rewrite is reported.
15
+ * Custom-named rows fail closed. The retired ACP transport used the installed
16
+ * CLI's identity and ignored both configured and caller credentials, so
17
+ * silently changing one of those rows into a cloud provider could send a
18
+ * credential with different provenance to Cognition. The warning tells the
19
+ * operator to reconfigure it explicitly; retaining the unknown adapter id
20
+ * prevents requests from reaching any upstream in the meantime.
23
21
  *
24
22
  * A row carrying the retired ACP identity URL is repointed at the api-server
25
23
  * in the same pass. That URL was never a destination — it existed only so
@@ -55,10 +53,16 @@ export function projectDevinCliAuthMode(config: OcxConfig): DevinCliAuthModeProj
55
53
  const warnings: string[] = [];
56
54
  let changed = false;
57
55
 
58
- // Every row, not only the registry id: a custom-named row had no registry pin,
59
- // so after the ACP removal it is the one that cannot construct an adapter.
60
56
  for (const [name, row] of Object.entries(config.providers ?? {})) {
61
57
  if (!row || row.adapter !== RETIRED_ACP_ADAPTER) continue;
58
+ if (name !== RETIRED_ACP_ADAPTER) {
59
+ warnings.push(
60
+ `left custom provider "${name}" unchanged because adapter ${RETIRED_ACP_ADAPTER} was removed: `
61
+ + "the row was not migrated; switch its adapter to devin and configure Devin authentication "
62
+ + "explicitly before using this provider.",
63
+ );
64
+ continue;
65
+ }
62
66
  row.adapter = "devin";
63
67
  changed = true;
64
68
  let detail = "";