@iislee/opencodex 2.11.0 → 2.31.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (675) hide show
  1. package/AGENTS_INSTALL.md +109 -0
  2. package/README.md +114 -19
  3. package/bin/ocx.mjs +164 -36
  4. package/bin/package-main.mjs +1 -1
  5. package/gui/dist/assets/index-BF38heuV.js +104 -0
  6. package/gui/dist/assets/index-DMiI18Kv.css +1 -0
  7. package/gui/dist/index.html +2 -2
  8. package/gui/dist/provider-icons/alibaba-color.svg +1 -1
  9. package/gui/dist/provider-icons/antigravity-color.svg +1 -1
  10. package/gui/dist/provider-icons/claude-color.svg +1 -1
  11. package/gui/dist/provider-icons/cline-color.svg +16 -0
  12. package/gui/dist/provider-icons/cloudflare-ai-gateway-color.svg +1 -1
  13. package/gui/dist/provider-icons/commandcode-color.svg +1 -0
  14. package/gui/dist/provider-icons/copilot-color.svg +1 -1
  15. package/gui/dist/provider-icons/cursor-color.svg +1 -1
  16. package/gui/dist/provider-icons/deepseek-color.svg +1 -1
  17. package/gui/dist/provider-icons/firepass-color.svg +1 -1
  18. package/gui/dist/provider-icons/fireworks-color.svg +1 -1
  19. package/gui/dist/provider-icons/gemini-color.svg +1 -1
  20. package/gui/dist/provider-icons/github-copilot-color.svg +1 -1
  21. package/gui/dist/provider-icons/gitlab-duo-color.svg +1 -1
  22. package/gui/dist/provider-icons/grok.svg +1 -1
  23. package/gui/dist/provider-icons/groq-color.svg +1 -1
  24. package/gui/dist/provider-icons/huggingface-color.svg +1 -1
  25. package/gui/dist/provider-icons/kimi-color.svg +1 -1
  26. package/gui/dist/provider-icons/kiro-color.svg +2 -2
  27. package/gui/dist/provider-icons/lm-studio-color.svg +1 -1
  28. package/gui/dist/provider-icons/mistral-color.svg +1 -1
  29. package/gui/dist/provider-icons/moonshot-color.svg +1 -1
  30. package/gui/dist/provider-icons/nvidia-color.svg +1 -1
  31. package/gui/dist/provider-icons/ollama-color.svg +1 -1
  32. package/gui/dist/provider-icons/openai.svg +1 -1
  33. package/gui/dist/provider-icons/opencode.svg +2 -1
  34. package/gui/dist/provider-icons/openrouter-color.svg +1 -1
  35. package/gui/dist/provider-icons/pi.svg +2 -2
  36. package/gui/dist/provider-icons/qianfan-color.svg +1 -1
  37. package/gui/dist/provider-icons/qwen-portal-color.svg +1 -1
  38. package/gui/dist/provider-icons/vercel-ai-gateway-color.svg +1 -1
  39. package/gui/dist/provider-icons/vllm-color.svg +1 -1
  40. package/gui/dist/provider-icons/xiaomi-color.svg +1 -1
  41. package/package.json +19 -10
  42. package/src/adapters/anthropic-output-schema.ts +137 -0
  43. package/src/adapters/anthropic.ts +376 -52
  44. package/src/adapters/base.ts +54 -7
  45. package/src/adapters/client-fingerprint.ts +18 -12
  46. package/src/adapters/cline-pass-deepseek-v4-tool-replay.ts +69 -0
  47. package/src/adapters/command-code.ts +601 -0
  48. package/src/adapters/cursor/checkpoint-store.ts +303 -0
  49. package/src/adapters/cursor/cursor-errors.ts +129 -6
  50. package/src/adapters/cursor/discovery.ts +61 -4
  51. package/src/adapters/cursor/effort-map.ts +27 -3
  52. package/src/adapters/cursor/framing.ts +39 -0
  53. package/src/adapters/cursor/h2-pool.ts +123 -0
  54. package/src/adapters/cursor/http1-bidi.ts +361 -0
  55. package/src/adapters/cursor/images.ts +704 -0
  56. package/src/adapters/cursor/live-models.ts +174 -58
  57. package/src/adapters/cursor/live-transport.ts +609 -170
  58. package/src/adapters/cursor/native-exec-common.ts +23 -2
  59. package/src/adapters/cursor/native-exec-fs.ts +10 -7
  60. package/src/adapters/cursor/native-exec-network.ts +1 -1
  61. package/src/adapters/cursor/native-exec-shell.ts +0 -1
  62. package/src/adapters/cursor/native-exec.ts +101 -14
  63. package/src/adapters/cursor/protobuf-events.ts +829 -11
  64. package/src/adapters/cursor/protobuf-request.ts +383 -65
  65. package/src/adapters/cursor/request-builder.ts +237 -31
  66. package/src/adapters/cursor/tool-definitions.ts +242 -12
  67. package/src/adapters/cursor/tool-result-normalize.ts +92 -0
  68. package/src/adapters/cursor/transport.ts +22 -0
  69. package/src/adapters/cursor/types.ts +28 -1
  70. package/src/adapters/cursor.ts +133 -7
  71. package/src/adapters/google-antigravity-replay.ts +558 -34
  72. package/src/adapters/google-antigravity-wire.ts +43 -10
  73. package/src/adapters/google-http.ts +38 -10
  74. package/src/adapters/google-tool-schema.ts +80 -15
  75. package/src/adapters/google-truncation.ts +11 -0
  76. package/src/adapters/google.ts +618 -74
  77. package/src/adapters/identity.ts +39 -6
  78. package/src/adapters/kiro-errors.ts +11 -0
  79. package/src/adapters/kiro-events.ts +19 -1
  80. package/src/adapters/kiro-thinking.ts +18 -2
  81. package/src/adapters/kiro-tools.ts +10 -1
  82. package/src/adapters/kiro.ts +92 -54
  83. package/src/adapters/mimo-free.ts +17 -0
  84. package/src/adapters/openai-chat-url.ts +11 -0
  85. package/src/adapters/openai-chat.ts +1311 -336
  86. package/src/adapters/openai-responses-url.ts +16 -0
  87. package/src/adapters/openai-responses.ts +830 -56
  88. package/src/adapters/registry.ts +175 -0
  89. package/src/adapters/responses-tool-schema.ts +67 -0
  90. package/src/adapters/tool-call-id.ts +119 -0
  91. package/src/adapters/tool-catalog-nudge.ts +104 -21
  92. package/src/adapters/xai-web-search.ts +185 -0
  93. package/src/bridge.ts +428 -61
  94. package/src/chat/inbound.ts +43 -19
  95. package/src/chat/outbound.ts +82 -26
  96. package/src/claude/agents-inject.ts +32 -9
  97. package/src/claude/context-windows.ts +21 -5
  98. package/src/claude/desktop-3p.ts +243 -9
  99. package/src/claude/gateway-cache.ts +41 -4
  100. package/src/claude/inbound.ts +72 -3
  101. package/src/claude/model-info.ts +38 -15
  102. package/src/claude/outbound.ts +70 -16
  103. package/src/cli/account-api.ts +35 -1
  104. package/src/cli/account-auth.ts +33 -6
  105. package/src/cli/account-catalog-refresh.ts +14 -0
  106. package/src/cli/account-extended.ts +389 -2
  107. package/src/cli/account-main.ts +317 -0
  108. package/src/cli/account.ts +30 -6
  109. package/src/cli/agent.ts +45 -1
  110. package/src/cli/claude-agent-startup-sync.ts +73 -0
  111. package/src/cli/claude-desktop.ts +29 -4
  112. package/src/cli/claude.ts +103 -34
  113. package/src/cli/codex-log-guard-doctor.ts +103 -0
  114. package/src/cli/codex-shim-autorestore.ts +2 -0
  115. package/src/cli/codex-shim-readiness.ts +76 -0
  116. package/src/cli/combo.ts +8 -0
  117. package/src/cli/config-command.ts +74 -10
  118. package/src/cli/dispatch.ts +593 -0
  119. package/src/cli/doctor.ts +315 -43
  120. package/src/cli/ensure-desired-integrations.ts +152 -0
  121. package/src/cli/export-command.ts +46 -20
  122. package/src/cli/help.ts +20 -271
  123. package/src/cli/index.ts +378 -519
  124. package/src/cli/init.ts +4 -17
  125. package/src/cli/integrations.ts +120 -2
  126. package/src/cli/lab.ts +607 -0
  127. package/src/cli/launcher-context.ts +77 -0
  128. package/src/cli/minimax.ts +497 -0
  129. package/src/cli/models-runtime.ts +35 -2
  130. package/src/cli/models.ts +100 -14
  131. package/src/cli/observe.ts +92 -3
  132. package/src/cli/opencode.ts +4 -2
  133. package/src/cli/provider-runtime.ts +18 -1
  134. package/src/cli/provider.ts +24 -3
  135. package/src/cli/ready.ts +301 -0
  136. package/src/cli/registry.ts +437 -0
  137. package/src/cli/root.ts +86 -0
  138. package/src/cli/route-policy.ts +92 -0
  139. package/src/cli/runtime-api.ts +6 -3
  140. package/src/cli/star-prompt.ts +71 -15
  141. package/src/cli/status.ts +10 -3
  142. package/src/cli/system-restart-client.ts +146 -0
  143. package/src/cli/tray-proxy.ts +153 -6
  144. package/src/cli/v2.ts +105 -10
  145. package/src/cli.ts +1 -1
  146. package/src/clients/config-export.ts +1358 -21
  147. package/src/codex/account-label.ts +14 -1
  148. package/src/codex/account-lifecycle.ts +130 -13
  149. package/src/codex/account-namespaces.ts +49 -3
  150. package/src/codex/account-priority.ts +83 -0
  151. package/src/codex/account-store.ts +29 -2
  152. package/src/codex/account-usability.ts +25 -2
  153. package/src/codex/admission.ts +256 -0
  154. package/src/codex/affinity-debug.ts +162 -0
  155. package/src/codex/app-server-processes.ts +493 -106
  156. package/src/codex/app-server-restart-service.ts +232 -0
  157. package/src/codex/auth-api.ts +849 -242
  158. package/src/codex/auth-collision.ts +5 -3
  159. package/src/codex/auth-context.ts +345 -32
  160. package/src/codex/autostart-health.ts +8 -1
  161. package/src/codex/catalog/account-models.ts +67 -0
  162. package/src/codex/catalog/aggregation.ts +68 -10
  163. package/src/codex/catalog/bundled.ts +331 -33
  164. package/src/codex/catalog/effort.ts +121 -30
  165. package/src/codex/catalog/filesystem-evidence.ts +302 -0
  166. package/src/codex/catalog/kinds.ts +2 -0
  167. package/src/codex/catalog/metadata.ts +529 -45
  168. package/src/codex/catalog/native-models.ts +72 -0
  169. package/src/codex/catalog/parsing.ts +224 -30
  170. package/src/codex/catalog/provider-fetch.ts +1460 -134
  171. package/src/codex/catalog/sync.ts +1449 -186
  172. package/src/codex/catalog-admission.ts +199 -0
  173. package/src/codex/catalog-refresh-status.ts +105 -0
  174. package/src/codex/catalog-write-serialization.ts +242 -0
  175. package/src/codex/catalog.ts +6 -3
  176. package/src/codex/codex-write-lock.ts +384 -0
  177. package/src/codex/convergence-types.ts +614 -0
  178. package/src/codex/convergence.ts +651 -0
  179. package/src/codex/coordinator-doctor.ts +332 -0
  180. package/src/codex/custom-model-catalog-migration.ts +176 -0
  181. package/src/codex/desired-state.ts +230 -0
  182. package/src/codex/features.ts +636 -39
  183. package/src/codex/generation.ts +202 -0
  184. package/src/codex/history-job.ts +407 -0
  185. package/src/codex/history-lock.ts +242 -0
  186. package/src/codex/history-migration-guardian.ts +26 -20
  187. package/src/codex/history-provider.ts +231 -28
  188. package/src/codex/history-transition.ts +105 -0
  189. package/src/codex/history-worker.ts +220 -0
  190. package/src/codex/inject-coordination.ts +290 -0
  191. package/src/codex/inject.ts +1073 -152
  192. package/src/codex/injected-marker.ts +37 -3
  193. package/src/codex/integration-record.ts +266 -0
  194. package/src/codex/internal/catalog-writer.ts +203 -0
  195. package/src/codex/internal/history-writer.ts +80 -0
  196. package/src/codex/journal.ts +66 -4
  197. package/src/codex/log-guard/inspect.ts +506 -0
  198. package/src/codex/log-guard/lock.ts +150 -0
  199. package/src/codex/log-guard/maintenance.ts +403 -0
  200. package/src/codex/log-guard/path-safety.ts +88 -0
  201. package/src/codex/log-guard/policy.ts +44 -0
  202. package/src/codex/log-guard/processes.ts +205 -0
  203. package/src/codex/log-guard/protection.ts +489 -0
  204. package/src/codex/log-guard/sqlite-errors.ts +9 -0
  205. package/src/codex/main-account-cache.ts +24 -0
  206. package/src/codex/main-account.ts +29 -1
  207. package/src/codex/management-convergence.ts +167 -0
  208. package/src/codex/model-cache.ts +56 -10
  209. package/src/codex/model-entitlements.ts +353 -0
  210. package/src/codex/native-main-admission.ts +47 -0
  211. package/src/codex/native-main-auth-temp.ts +187 -0
  212. package/src/codex/native-main-claim.ts +178 -0
  213. package/src/codex/native-main-lock-file.ts +162 -0
  214. package/src/codex/native-main-owner.ts +329 -0
  215. package/src/codex/native-profile-api.ts +247 -0
  216. package/src/codex/native-profile-manager.ts +1531 -0
  217. package/src/codex/native-profile-processes.ts +121 -0
  218. package/src/codex/native-profile-recovery.ts +99 -0
  219. package/src/codex/native-profile-stage-store.ts +387 -0
  220. package/src/codex/native-profile-startup.ts +492 -0
  221. package/src/codex/native-profile-store.ts +855 -0
  222. package/src/codex/native-profile-types.ts +120 -0
  223. package/src/codex/native-residue.ts +682 -0
  224. package/src/codex/paths.ts +80 -1
  225. package/src/codex/plan-from-token.ts +140 -0
  226. package/src/codex/plan.ts +40 -0
  227. package/src/codex/plugins-doctor.ts +1 -1
  228. package/src/codex/pool-rotation.ts +74 -4
  229. package/src/codex/project-config-warnings.ts +20 -6
  230. package/src/codex/prompt-journal.ts +352 -0
  231. package/src/codex/prompt-layers.ts +967 -0
  232. package/src/codex/prompt-lock.ts +143 -0
  233. package/src/codex/quota-rejection.ts +298 -0
  234. package/src/codex/quota.ts +175 -13
  235. package/src/codex/refresh.ts +11 -2
  236. package/src/codex/reset-credit-recovery.ts +1044 -0
  237. package/src/codex/routing.ts +505 -94
  238. package/src/codex/runtime.ts +159 -38
  239. package/src/codex/shim.ts +1009 -28
  240. package/src/codex/subagent-model-fallback.ts +350 -35
  241. package/src/codex/sync.ts +191 -2
  242. package/src/codex/transition-state.ts +612 -0
  243. package/src/codex/upstream-host-health.ts +368 -0
  244. package/src/codex/user-identity.ts +557 -0
  245. package/src/codex/warmup.ts +187 -81
  246. package/src/codex/write-coordination.ts +114 -0
  247. package/src/combos/failover.ts +20 -0
  248. package/src/combos/index.ts +4 -0
  249. package/src/combos/request.ts +32 -0
  250. package/src/combos/types.ts +81 -9
  251. package/src/config/provider-name.ts +24 -0
  252. package/src/config.ts +1762 -140
  253. package/src/generated/compatibility-version.json +3164 -0
  254. package/src/generated/{jawcode-model-metadata.ts → model-metadata.ts} +19 -17
  255. package/src/grok/inject.ts +16 -5
  256. package/src/grok/inspect.ts +45 -0
  257. package/src/grok/sync.ts +2 -2
  258. package/src/images/loop.ts +152 -29
  259. package/src/images/plan.ts +23 -13
  260. package/src/integrations/config-io.ts +269 -0
  261. package/src/integrations/journal.ts +315 -0
  262. package/src/integrations/merge.ts +135 -0
  263. package/src/integrations/mutation-flight.ts +71 -0
  264. package/src/integrations/native/ownership-preflight.ts +202 -0
  265. package/src/integrations/omp-yaml-source.ts +358 -0
  266. package/src/integrations/owned-refresh.ts +74 -0
  267. package/src/integrations/ownership.ts +111 -0
  268. package/src/integrations/registry.ts +159 -0
  269. package/src/integrations/serialize.ts +314 -0
  270. package/src/integrations/state.ts +361 -0
  271. package/src/integrations/store.ts +103 -0
  272. package/src/integrations/writer-lock.ts +98 -0
  273. package/src/integrations/writer.ts +691 -0
  274. package/src/lab/artifacts/sanitize.ts +586 -0
  275. package/src/lab/artifacts/secure-fs.ts +475 -0
  276. package/src/lab/artifacts/store.ts +310 -0
  277. package/src/lab/automation/budgets.ts +78 -0
  278. package/src/lab/automation/config-persistence.ts +256 -0
  279. package/src/lab/automation/constants.ts +39 -0
  280. package/src/lab/automation/cooldown.ts +103 -0
  281. package/src/lab/automation/dispatch.ts +211 -0
  282. package/src/lab/automation/index.ts +13 -0
  283. package/src/lab/automation/orchestrator.ts +499 -0
  284. package/src/lab/automation/persistence.ts +512 -0
  285. package/src/lab/automation/planner.ts +371 -0
  286. package/src/lab/automation/policy.ts +136 -0
  287. package/src/lab/automation/queue.ts +191 -0
  288. package/src/lab/automation/recovery.ts +24 -0
  289. package/src/lab/automation/route-context.ts +21 -0
  290. package/src/lab/automation/run-key.ts +44 -0
  291. package/src/lab/automation/runs-query.ts +34 -0
  292. package/src/lab/automation/types.ts +160 -0
  293. package/src/lab/conformance/assertion.ts +325 -0
  294. package/src/lab/conformance/digest.ts +22 -0
  295. package/src/lab/conformance/executor.ts +741 -0
  296. package/src/lab/conformance/fixture-provider.ts +27 -0
  297. package/src/lab/conformance/fixtures/live-v1-cases.json +175 -0
  298. package/src/lab/conformance/fixtures/protocol-v1-cases.json +461 -0
  299. package/src/lab/conformance/harness-budget.ts +47 -0
  300. package/src/lab/conformance/index.ts +5 -0
  301. package/src/lab/conformance/jcs.ts +64 -0
  302. package/src/lab/conformance/json-pointer.ts +39 -0
  303. package/src/lab/conformance/manifest.ts +180 -0
  304. package/src/lab/conformance/mcp-stub.ts +179 -0
  305. package/src/lab/conformance/negative-controls.ts +164 -0
  306. package/src/lab/conformance/observation.ts +355 -0
  307. package/src/lab/conformance/runner.ts +68 -0
  308. package/src/lab/conformance/sse-normalize.ts +59 -0
  309. package/src/lab/conformance/suite-manifest.ts +78 -0
  310. package/src/lab/conformance/types.ts +214 -0
  311. package/src/lab/constants.ts +126 -0
  312. package/src/lab/digest.ts +64 -0
  313. package/src/lab/events/errors.ts +9 -0
  314. package/src/lab/events/limits.ts +117 -0
  315. package/src/lab/events/types.ts +229 -0
  316. package/src/lab/events/validate.ts +781 -0
  317. package/src/lab/fabric/constants.ts +40 -0
  318. package/src/lab/fabric/executor.ts +492 -0
  319. package/src/lab/fabric/index.ts +80 -0
  320. package/src/lab/fabric/manifest.ts +222 -0
  321. package/src/lab/fabric/observe.ts +489 -0
  322. package/src/lab/fabric/patch.ts +79 -0
  323. package/src/lab/fabric/producer-child.ts +139 -0
  324. package/src/lab/fabric/producer-isolate.ts +276 -0
  325. package/src/lab/fabric/producer-protocol.ts +61 -0
  326. package/src/lab/fabric/scratch.ts +439 -0
  327. package/src/lab/fabric/subject.ts +106 -0
  328. package/src/lab/fabric/types.ts +134 -0
  329. package/src/lab/fabric/verifier.ts +98 -0
  330. package/src/lab/index.ts +54 -0
  331. package/src/lab/ledger/artifact-refs.ts +127 -0
  332. package/src/lab/ledger/invalidation.ts +136 -0
  333. package/src/lab/ledger/purge.ts +310 -0
  334. package/src/lab/ledger/store.ts +532 -0
  335. package/src/lab/live/credential-lease.ts +53 -0
  336. package/src/lab/live/destination.ts +155 -0
  337. package/src/lab/live/executor.ts +336 -0
  338. package/src/lab/live/inert-tools.ts +56 -0
  339. package/src/lab/live/manifest.ts +85 -0
  340. package/src/lab/live/mcp-loopback.ts +57 -0
  341. package/src/lab/live/runner.ts +19 -0
  342. package/src/lab/live/sandbox.ts +61 -0
  343. package/src/lab/live/suite-manifest.ts +41 -0
  344. package/src/lab/live/transport.ts +118 -0
  345. package/src/lab/live/types.ts +197 -0
  346. package/src/lab/observe/from-conformance.ts +301 -0
  347. package/src/lab/observe/from-live.ts +117 -0
  348. package/src/lab/paths.ts +153 -0
  349. package/src/lab/projection/rebuild.ts +495 -0
  350. package/src/lab/projection/schema.ts +135 -0
  351. package/src/lab/projection/verdicts.ts +474 -0
  352. package/src/lab/projection/verification.ts +412 -0
  353. package/src/lab/public/bundle.ts +217 -0
  354. package/src/lab/public/community-authority.ts +175 -0
  355. package/src/lab/public/community-files.ts +29 -0
  356. package/src/lab/public/community.ts +479 -0
  357. package/src/lab/public/file-safety.ts +155 -0
  358. package/src/lab/public/ids.ts +26 -0
  359. package/src/lab/public/index.ts +16 -0
  360. package/src/lab/public/mutation-lock.ts +424 -0
  361. package/src/lab/public/operator.ts +353 -0
  362. package/src/lab/public/origin-purge.ts +79 -0
  363. package/src/lab/public/origin.ts +203 -0
  364. package/src/lab/public/privacy.ts +143 -0
  365. package/src/lab/public/private-file.ts +261 -0
  366. package/src/lab/public/project.ts +124 -0
  367. package/src/lab/public/purge-test-fault.ts +21 -0
  368. package/src/lab/public/purge.ts +223 -0
  369. package/src/lab/public/registry.ts +44 -0
  370. package/src/lab/public/revocation.ts +252 -0
  371. package/src/lab/public/signature.ts +243 -0
  372. package/src/lab/public/storage.ts +105 -0
  373. package/src/lab/public/strict-json.ts +206 -0
  374. package/src/lab/public/time.ts +26 -0
  375. package/src/lab/public/types.ts +172 -0
  376. package/src/lab/public/validate.ts +391 -0
  377. package/src/lab/query/catalog.ts +101 -0
  378. package/src/lab/query/connection.ts +107 -0
  379. package/src/lab/query/constants.ts +4 -0
  380. package/src/lab/query/cursor.ts +132 -0
  381. package/src/lab/query/dto-map.ts +277 -0
  382. package/src/lab/query/errors.ts +22 -0
  383. package/src/lab/query/freshness.ts +53 -0
  384. package/src/lab/query/index.ts +45 -0
  385. package/src/lab/query/latest-observation.ts +59 -0
  386. package/src/lab/query/passive-production.ts +159 -0
  387. package/src/lab/query/queries.ts +444 -0
  388. package/src/lab/query/types.ts +266 -0
  389. package/src/lab/subject/behavior-fingerprint.ts +77 -0
  390. package/src/lab/subject/installation-salt.ts +112 -0
  391. package/src/lab/subject/protocol-subject.ts +80 -0
  392. package/src/lab/subject/route-subject.ts +74 -0
  393. package/src/lib/app-owned-memory-stores.ts +22 -0
  394. package/src/lib/bounded-body.ts +153 -9
  395. package/src/lib/bun-runtime.ts +125 -12
  396. package/src/lib/bun-stream-caps.ts +13 -9
  397. package/src/lib/codex-restart-contract.ts +120 -0
  398. package/src/lib/config-ownership.ts +6 -2
  399. package/src/lib/destination-policy.ts +65 -1
  400. package/src/lib/errors.ts +44 -2
  401. package/src/lib/fabric-task-execution-authority.ts +7 -0
  402. package/src/lib/fabric-task-host.ts +29 -0
  403. package/src/lib/lab-activation.ts +223 -0
  404. package/src/lib/lab-live-execution-authority.ts +13 -0
  405. package/src/lib/lab-live-host.ts +30 -0
  406. package/src/lib/lab-live-pinned-sender.ts +56 -0
  407. package/src/lib/lab-live-route-production.ts +130 -0
  408. package/src/lib/lab-passive-linker-registration.ts +26 -0
  409. package/src/lib/local-management-attestation.ts +51 -0
  410. package/src/lib/local-management-capability.ts +100 -0
  411. package/src/lib/local-provider-reload-contract.ts +100 -0
  412. package/src/lib/optional-shutdown-hooks.ts +57 -0
  413. package/src/lib/pinned-http.ts +145 -26
  414. package/src/lib/process-control.ts +4 -1
  415. package/src/lib/provider-outbound.ts +49 -9
  416. package/src/lib/redact.ts +419 -3
  417. package/src/lib/self-launch-argv.ts +15 -0
  418. package/src/lib/server-resource-ownership.ts +71 -0
  419. package/src/lib/shadow-call.ts +35 -4
  420. package/src/lib/sse-decoder.ts +41 -0
  421. package/src/lib/state-store-registrations.ts +10 -2
  422. package/src/lib/system-restart-contract.ts +73 -0
  423. package/src/lib/token-estimate.ts +19 -2
  424. package/src/lib/tool-argument-integers.ts +202 -0
  425. package/src/lib/translator-budget.ts +44 -0
  426. package/src/lib/upstream-http-version.ts +57 -0
  427. package/src/lib/upstream-reachability.ts +95 -0
  428. package/src/lib/upstream-retry.ts +156 -3
  429. package/src/lib/windows-atomic-replace.ts +155 -0
  430. package/src/lib/windows-elevation.ts +70 -2
  431. package/src/lib/windows-secret-acl.ts +409 -69
  432. package/src/lib/windows-service-wrappers.ts +72 -0
  433. package/src/lib/windows-text.ts +106 -0
  434. package/src/lib/windows-user-principal.ts +341 -0
  435. package/src/lib/winsw.ts +33 -5
  436. package/src/oauth/account-import/google-antigravity-adapter.ts +74 -0
  437. package/src/oauth/account-import/index.ts +15 -0
  438. package/src/oauth/account-import/parser.ts +83 -0
  439. package/src/oauth/account-import/registry.ts +18 -0
  440. package/src/oauth/account-import/service.ts +75 -0
  441. package/src/oauth/account-import/types.ts +91 -0
  442. package/src/oauth/anthropic.ts +12 -1
  443. package/src/oauth/callback-server.ts +8 -2
  444. package/src/oauth/chatgpt.ts +12 -1
  445. package/src/oauth/command-code.ts +239 -0
  446. package/src/oauth/cursor.ts +46 -5
  447. package/src/oauth/google-antigravity.ts +35 -3
  448. package/src/oauth/health.ts +20 -12
  449. package/src/oauth/index.ts +398 -66
  450. package/src/oauth/key-providers.ts +16 -0
  451. package/src/oauth/kimi.ts +16 -2
  452. package/src/oauth/kiro.ts +50 -6
  453. package/src/oauth/local-token-detect.ts +11 -2
  454. package/src/oauth/log.ts +3 -1
  455. package/src/oauth/login-cli.ts +88 -28
  456. package/src/oauth/nous.ts +798 -0
  457. package/src/oauth/store.ts +119 -21
  458. package/src/oauth/token-guardian.ts +9 -3
  459. package/src/pi/models.ts +2 -2
  460. package/src/providers/alibaba-region-migration.ts +1 -1
  461. package/src/providers/antigravity-models.ts +521 -31
  462. package/src/providers/base-url-choices.ts +10 -0
  463. package/src/providers/codex-capacity.ts +292 -0
  464. package/src/providers/command-code-efforts.ts +144 -0
  465. package/src/providers/context-cap.ts +22 -5
  466. package/src/providers/cursor-pool.ts +72 -0
  467. package/src/providers/derive.ts +253 -6
  468. package/src/providers/fastwire.ts +501 -0
  469. package/src/providers/free-directory.ts +10 -7
  470. package/src/providers/google-vertex-location.ts +14 -0
  471. package/src/providers/key-failover.ts +71 -3
  472. package/src/providers/label.ts +1 -1
  473. package/src/providers/model-discovery-limits.ts +16 -0
  474. package/src/providers/model-discovery.ts +115 -22
  475. package/src/providers/model-rename-migration.ts +255 -0
  476. package/src/providers/model-rename-startup.ts +28 -0
  477. package/src/providers/openai-sidecar.ts +72 -4
  478. package/src/providers/openai-tier-startup.ts +31 -2
  479. package/src/providers/openai-tiers.ts +119 -4
  480. package/src/providers/openai-virtual-models.ts +1 -0
  481. package/src/providers/opencode-zen-rate-limit.ts +102 -0
  482. package/src/providers/provider-id-rewrite.ts +29 -0
  483. package/src/providers/quota.ts +1319 -38
  484. package/src/providers/registry.ts +1429 -111
  485. package/src/providers/request-pacing.ts +310 -0
  486. package/src/providers/service-tier.ts +277 -0
  487. package/src/providers/slug-codec.ts +42 -6
  488. package/src/providers/static-model-discovery.ts +86 -0
  489. package/src/providers/xai-responses-opt-in.ts +15 -0
  490. package/src/providers/xai-transport.ts +11 -4
  491. package/src/reasoning-effort.ts +49 -1
  492. package/src/responses/compaction.ts +26 -1
  493. package/src/responses/custom-tool-compat.ts +266 -0
  494. package/src/responses/hosted-tool-policy.ts +9 -0
  495. package/src/responses/namespace-tool-compat.ts +355 -0
  496. package/src/responses/parser.ts +220 -38
  497. package/src/responses/provider-continuation.ts +98 -0
  498. package/src/responses/provider-opaque-metadata.ts +73 -0
  499. package/src/responses/reasoning-envelope.ts +9 -1
  500. package/src/responses/reasoning-replay-cache.ts +426 -0
  501. package/src/responses/schema.ts +7 -1
  502. package/src/responses/spill-store.ts +75 -10
  503. package/src/responses/state.ts +565 -27
  504. package/src/responses/thought-signature-replay.ts +347 -0
  505. package/src/responses/tool-search-compat.ts +301 -0
  506. package/src/responses/truncated-stop-reason.ts +60 -0
  507. package/src/router.ts +366 -30
  508. package/src/routing/analytics.ts +378 -0
  509. package/src/routing/capability.ts +244 -0
  510. package/src/routing/compatibility/assemble.ts +73 -0
  511. package/src/routing/compatibility/behavior.ts +278 -0
  512. package/src/routing/compatibility/catalog.ts +99 -0
  513. package/src/routing/compatibility/endpoint.ts +52 -0
  514. package/src/routing/compatibility/lab-evidence-provider.ts +130 -0
  515. package/src/routing/compatibility/policy.ts +181 -0
  516. package/src/routing/compatibility/provider-slot.ts +56 -0
  517. package/src/routing/compatibility/reader.ts +110 -0
  518. package/src/routing/compatibility/subject.ts +191 -0
  519. package/src/routing/compatibility/types.ts +64 -0
  520. package/src/routing/compatibility/version.ts +104 -0
  521. package/src/routing/cost.ts +77 -0
  522. package/src/routing/evaluator.ts +495 -0
  523. package/src/routing/health.ts +412 -0
  524. package/src/routing/history/cursor.ts +43 -0
  525. package/src/routing/history/indexer.ts +605 -0
  526. package/src/routing/history/schema.ts +72 -0
  527. package/src/routing/profile-namespace.ts +15 -0
  528. package/src/routing/profile.ts +547 -0
  529. package/src/routing/quota.ts +145 -0
  530. package/src/routing/request-evidence.ts +45 -0
  531. package/src/routing/trace.ts +776 -0
  532. package/src/server/adapter-resolve.ts +2 -29
  533. package/src/server/auth-cors.ts +267 -46
  534. package/src/server/background-lifecycle.ts +182 -0
  535. package/src/server/chat-completions.ts +130 -56
  536. package/src/server/chat-native-sse.ts +331 -0
  537. package/src/server/chat-native.ts +426 -0
  538. package/src/server/claude-messages.ts +159 -43
  539. package/src/server/direct-local-http.ts +347 -0
  540. package/src/server/effort-policy.ts +18 -0
  541. package/src/server/github-copilot-responses-repair.ts +338 -0
  542. package/src/server/gui-static.ts +39 -10
  543. package/src/server/images.ts +94 -12
  544. package/src/server/index.ts +865 -181
  545. package/src/server/lifecycle.ts +284 -13
  546. package/src/server/live.ts +136 -17
  547. package/src/server/local-management-read-client.ts +90 -0
  548. package/src/server/local-provider-reload-client.ts +137 -0
  549. package/src/server/management/agent-settings-routes.ts +398 -103
  550. package/src/server/management/api-key-usage.ts +31 -5
  551. package/src/server/management/body.ts +6 -0
  552. package/src/server/management/combo-routes.ts +62 -24
  553. package/src/server/management/config-routes.ts +464 -51
  554. package/src/server/management/context.ts +80 -2
  555. package/src/server/management/integration-routes.ts +498 -0
  556. package/src/server/management/lab-automation-routes.ts +206 -0
  557. package/src/server/management/lab-routes.ts +563 -0
  558. package/src/server/management/logs-usage-routes.ts +101 -32
  559. package/src/server/management/model-routes.ts +189 -131
  560. package/src/server/management/model-rows.ts +163 -0
  561. package/src/server/management/native-integration-routes.ts +769 -0
  562. package/src/server/management/oauth-account-routes.ts +80 -4
  563. package/src/server/management/provider-capability-config.ts +48 -0
  564. package/src/server/management/provider-routes.ts +764 -157
  565. package/src/server/management/request-history-routes.ts +191 -0
  566. package/src/server/management/routing-analytics-routes.ts +74 -0
  567. package/src/server/management/routing-profile-routes.ts +380 -0
  568. package/src/server/management/shared.ts +27 -11
  569. package/src/server/management/sidebar-routes.ts +47 -31
  570. package/src/server/management/storage-log-guard-routes.ts +186 -0
  571. package/src/server/management/sync-response.ts +69 -0
  572. package/src/server/management/system-restart.ts +288 -32
  573. package/src/server/management/system-routes.ts +77 -0
  574. package/src/server/management/usage-summary-cache.ts +9 -1
  575. package/src/server/management/vision-sidecar-options.ts +167 -0
  576. package/src/server/management/web-search-sidecar-options.ts +120 -0
  577. package/src/server/management-api.ts +115 -14
  578. package/src/server/management-auth.ts +220 -5
  579. package/src/server/passive-route-linker.ts +66 -0
  580. package/src/server/ports.ts +41 -1
  581. package/src/server/proxy-liveness.ts +132 -5
  582. package/src/server/readiness.ts +99 -0
  583. package/src/server/relay-eager.ts +82 -42
  584. package/src/server/relay.ts +236 -76
  585. package/src/server/request-decompress.ts +113 -6
  586. package/src/server/request-log.ts +235 -22
  587. package/src/server/responses/agent-task-recovery-cache.ts +143 -0
  588. package/src/server/responses/agent-task-recovery.ts +465 -0
  589. package/src/server/responses/collaboration.ts +204 -35
  590. package/src/server/responses/compact.ts +442 -55
  591. package/src/server/responses/core.ts +2872 -331
  592. package/src/server/responses/empty-completion-guard.ts +276 -0
  593. package/src/server/responses/encrypted-payload.ts +62 -39
  594. package/src/server/responses/fetch-helpers.ts +79 -4
  595. package/src/server/responses/input-admission.ts +185 -0
  596. package/src/server/responses/pacing-overload.ts +13 -0
  597. package/src/server/responses/policy-fallback.ts +178 -0
  598. package/src/server/responses/responses-field-backfill.ts +251 -0
  599. package/src/server/responses/terminal-guard.ts +26 -5
  600. package/src/server/responses/upstream-error.ts +5 -0
  601. package/src/server/responses/ws-upstream.ts +308 -0
  602. package/src/server/responses-custom-tool-repair.ts +282 -0
  603. package/src/server/responses-item-id-repair.ts +54 -6
  604. package/src/server/responses-json-events.ts +90 -0
  605. package/src/server/responses-model-rewrite.ts +29 -0
  606. package/src/server/responses-reasoning-summary-rewrite.ts +178 -0
  607. package/src/server/responses-snapshot-repair.ts +621 -0
  608. package/src/server/responses-terminal-repair.ts +342 -0
  609. package/src/server/responses-tool-search-repair.ts +267 -0
  610. package/src/server/responses-undeclared-tool-guard.ts +153 -0
  611. package/src/server/responses.ts +18 -2
  612. package/src/server/search.ts +78 -13
  613. package/src/server/sse-frame-buffer.ts +292 -0
  614. package/src/server/sse-payload-rewrite.ts +110 -22
  615. package/src/server/startup-action-control.ts +8 -1
  616. package/src/server/startup-health-cache.ts +19 -1
  617. package/src/server/system-env.ts +80 -9
  618. package/src/server/ws-bridge.ts +39 -38
  619. package/src/service-manager-probe.ts +892 -0
  620. package/src/service.ts +1111 -90
  621. package/src/sidecar/auth.ts +92 -0
  622. package/src/sidecar/candidates.ts +83 -0
  623. package/src/storage/cleanup.ts +2 -2
  624. package/src/storage/scanner.ts +1 -1
  625. package/src/storage/worker-lifecycle.ts +14 -14
  626. package/src/tray/windows-tray.ps1 +83 -9
  627. package/src/tray/windows.ts +43 -16
  628. package/src/types/accounts.ts +37 -0
  629. package/src/types/config.ts +845 -0
  630. package/src/types/provider.ts +545 -0
  631. package/src/types/request.ts +384 -0
  632. package/src/types/tools.ts +131 -0
  633. package/src/types/wire.ts +80 -0
  634. package/src/types.ts +104 -1236
  635. package/src/update/index.ts +32 -19
  636. package/src/update/job.ts +442 -67
  637. package/src/update/notify.ts +12 -6
  638. package/src/update/npm-cache-preflight.d.mts +47 -0
  639. package/src/update/npm-cache-preflight.mjs +201 -0
  640. package/src/update/transactional-install.d.mts +22 -0
  641. package/src/update/transactional-install.mjs +259 -0
  642. package/src/usage/cost.ts +0 -0
  643. package/src/usage/expected-prices.ts +268 -16
  644. package/src/usage/log.ts +606 -41
  645. package/src/usage/summary.ts +177 -9
  646. package/src/usage/user-cost-overlay-reconciler.ts +313 -0
  647. package/src/usage/user-cost-overlays.ts +314 -0
  648. package/src/vision/anthropic-describe.ts +10 -6
  649. package/src/vision/backends.ts +97 -0
  650. package/src/vision/describe.ts +9 -3
  651. package/src/vision/eligibility.ts +250 -0
  652. package/src/vision/index.ts +238 -24
  653. package/src/vision/reasoning.ts +55 -0
  654. package/src/vision/routed-describe.ts +175 -0
  655. package/src/vision/timeout-bounds.ts +9 -0
  656. package/src/web-search/anthropic-executor.ts +13 -7
  657. package/src/web-search/backends.ts +108 -0
  658. package/src/web-search/exa-executor.ts +88 -0
  659. package/src/web-search/executor.ts +11 -3
  660. package/src/web-search/gemini-executor.ts +141 -0
  661. package/src/web-search/index.ts +150 -15
  662. package/src/web-search/loop.ts +279 -50
  663. package/src/web-search/parse.ts +125 -30
  664. package/src/web-search/sources.ts +60 -0
  665. package/src/web-search/xai-executor.ts +219 -0
  666. package/gui/dist/assets/index-DTpMHS4F.js +0 -67
  667. package/gui/dist/assets/index-ZNVDE3C7.css +0 -1
  668. package/gui/dist/provider-icons/antigravity.svg +0 -1
  669. package/gui/dist/provider-icons/claude.svg +0 -1
  670. package/gui/dist/provider-icons/copilot.svg +0 -1
  671. package/gui/dist/provider-icons/cursor.svg +0 -2
  672. package/gui/dist/provider-icons/gemini.svg +0 -1
  673. package/gui/dist/provider-icons/grok-color.svg +0 -1
  674. package/gui/dist/provider-icons/kiro.svg +0 -14
  675. package/src/cli/internal-dispatch.ts +0 -20
@@ -1,7 +1,7 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { readFileSync } from "node:fs";
3
3
  import type { CatalogModel } from "../../codex/catalog";
4
- import { catalogModelSlug, invalidateCodexModelsCache, nativeModelRows, uniqueCatalogModelsForPublicList } from "../../codex/catalog";
4
+ import { catalogModelSlug, invalidateCodexModelsCache, nativeContextLimits, nativeModelRows, uniqueCatalogModelsForPublicList } from "../../codex/catalog";
5
5
  import {
6
6
  DEFAULT_SUBAGENT_MODELS,
7
7
  codexAutoStartEnabled,
@@ -37,9 +37,14 @@ import { deriveProviderPresets } from "../../providers/derive";
37
37
  import { providerCodexAccountMode } from "../../providers/registry";
38
38
  import { routedSlug, slugEquals } from "../../providers/slug-codec";
39
39
  import { clearProviderQuotaCache, fetchProviderQuotaReports } from "../../providers/quota";
40
- import { isCanonicalOpenAiForwardProvider } from "../../providers/openai-tiers";
40
+ import { isCanonicalOpenAiForwardProvider, OPENAI_CODEX_PROVIDER_ID } from "../../providers/openai-tiers";
41
41
  import { clearThreadAccountMap } from "../../codex/routing";
42
42
  import { primeCodexPoolQuotas } from "../../codex/auth-api";
43
+ import {
44
+ codexAccountPickerEnabled,
45
+ initializeDefaultCodexAccountNamespaces,
46
+ } from "../../codex/account-namespaces";
47
+ import { catalogRefreshIsPending } from "../../codex/catalog-refresh-status";
43
48
  import { DEFAULT_PROVIDER_CONTEXT_CAP, globalContextCapValue, providerContextCap, providerContextCaps, setAllProviderContextCaps, setGlobalContextCapValue, setProviderContextCap } from "../../providers/context-cap";
44
49
  import { resolveCodexHomeDir } from "../../codex/home";
45
50
  import { readUsageEntries } from "../../usage/log";
@@ -47,6 +52,31 @@ import { getUsageDebugLogEntries } from "../../usage/debug";
47
52
  import { parseRange, parseUsageSurface, summarizeUsage } from "../../usage/summary";
48
53
  import { stripCodexRuntimeProviderFields } from "../../codex/auth-context";
49
54
  import { getProviderRegistryEntry } from "../../providers/registry";
55
+ import { VISION_REASONING_EFFORTS, isVisionReasoningEffort } from "../../reasoning-effort";
56
+ import { normalizeVisionReasoningForModel } from "../../vision/reasoning";
57
+ import {
58
+ findAnthropicVisionProvider,
59
+ isValidVisionTimeoutMs,
60
+ MAX_VISION_TIMEOUT_MS,
61
+ MIN_VISION_TIMEOUT_MS,
62
+ resolveEffectiveVisionModel,
63
+ resolveMaxDescriptionsPerTurn,
64
+ resolveVisionBackend,
65
+ resolveVisionTimeoutMs,
66
+ } from "../../vision";
67
+ import {
68
+ visionCandidateRows,
69
+ visionDescriberIsProvablyBlind,
70
+ visionDescriberRejection,
71
+ visionModelOptionsFor,
72
+ } from "./vision-sidecar-options";
73
+ import {
74
+ webSearchCandidateRows,
75
+ webSearchModelIsRejected,
76
+ webSearchModelOptionsFrom,
77
+ webSearchModelRejection,
78
+ } from "./web-search-sidecar-options";
79
+ import { validateXaiSearchOptions } from "../../web-search/xai-executor";
50
80
  import { getDebugLogEntries } from "../../lib/debug-log-buffer";
51
81
  import { getInjectionDebugLogEntries } from "../../lib/injection-debug-log";
52
82
  import {
@@ -62,6 +92,7 @@ import { filterRequestLogs, getRequestLogEntries, type RequestLogEntry } from ".
62
92
  import { estimateComboCost, estimateRequestCost, normalizeCostTokens, tokensPerSecond } from "../../usage/cost";
63
93
  import type { PersistedUsageAttempt } from "../../usage/log";
64
94
  import { isAllowedRequestOrigin, jsonResponse, providerManagementConfigError, publicProviderBaseUrl, safeConfigDTO } from "../auth-cors";
95
+ import { withProviderServiceTierDTO } from "./provider-capability-config";
65
96
  import { applySystemEnvToggle } from "../system-env";
66
97
  import { getCachedStartupHealth, invalidateStartupHealthCache } from "../startup-health-cache";
67
98
  import { runWindowsTrayAction } from "../windows-tray-control";
@@ -73,10 +104,147 @@ import type { MetricUnavailableReason, TokPerSecondResult, CostEstimateReason, C
73
104
  import type { ManagementContext } from "./context";
74
105
  import { readManagementJsonBody, rethrowManagementBodyTooLarge } from "./body";
75
106
 
107
+ async function sidecarVisionResponseSettings(config: OcxConfig): Promise<{
108
+ model: string;
109
+ reasoning: string;
110
+ models: Awaited<ReturnType<typeof visionModelOptionsFor>>;
111
+ }> {
112
+ const vs = config.visionSidecar ?? {};
113
+ // Match the runtime's one selected Anthropic executor for both backend fallback
114
+ // and catalog reachability; resolving it once prevents the two projections drifting.
115
+ const anthropicSidecar = findAnthropicVisionProvider(config);
116
+ // The routed backend reports its own namespaced model verbatim: it is the
117
+ // dispatched value, and collapsing it through the legacy resolver would
118
+ // display a describer the runtime is not using (roadmap 190).
119
+ const routedActive = vs.backend === "routed" && !!vs.model && vs.model.includes("/");
120
+ const backend = routedActive ? "routed" as const : resolveVisionBackend(vs.backend, anthropicSidecar);
121
+ const model = routedActive && vs.model
122
+ ? vs.model
123
+ : resolveEffectiveVisionModel(config, backend === "routed" ? resolveVisionBackend(undefined, anthropicSidecar) : backend);
124
+ const reasoning = normalizeVisionReasoningForModel(model, vs.reasoning) ?? "low";
125
+ const models = await visionModelOptionsFor(config, anthropicSidecar);
126
+ // Display-only grandfather: a persisted id stays selectable, but the write gate
127
+ // remains stricter and rejects a model that is positively proven blind.
128
+ if (!models.some(option => option.value === model)) {
129
+ models.unshift({ value: model, label: model, backend });
130
+ }
131
+ return { model, reasoning, models };
132
+ }
133
+
134
+ /** One client's outcome from a fan-out sync. Absent from the list means "left alone". */
135
+ interface ClientIntegrationSyncOutcome {
136
+ readonly client: "grok" | "claude-desktop" | "mcode";
137
+ readonly ok: boolean;
138
+ readonly changed?: boolean;
139
+ readonly reason?: string;
140
+ }
141
+
142
+ /**
143
+ * Re-inject native clients that are switched ON and file integrations whose
144
+ * OpenCodex ownership record is the operator's durable opt-in.
145
+ *
146
+ * Only Codex used to run here, so a catalog change reached Codex and nothing else: a Grok
147
+ * fence or a written Desktop profile kept the context windows it was created with until the
148
+ * next `ocx start`. The startup path already gates each client on its own toggle
149
+ * (`src/cli/index.ts`), and this is that same fan-out for the on-demand command.
150
+ *
151
+ * A client that is OFF or never connected is omitted from the result rather than reported as skipped — the
152
+ * caller has to be able to tell "not touched" from "tried and failed". A client that fails
153
+ * does not fail the sync: Codex is the one that matters for routing, and a broken Grok file
154
+ * should surface as a warning, not as a 500 on a command that did its main job.
155
+ */
156
+ async function syncEnabledClientIntegrations(
157
+ port: number | undefined,
158
+ config: OcxConfig,
159
+ ): Promise<ClientIntegrationSyncOutcome[]> {
160
+ if (port === undefined) return [];
161
+ const { claudeDesktopIntegrationEnabled, grokIntegrationEnabled } = await import("../../codex/desired-state");
162
+ const out: ClientIntegrationSyncOutcome[] = [];
163
+
164
+ if (grokIntegrationEnabled(config)) {
165
+ try {
166
+ const { syncGrokConfig } = await import("../../grok/sync");
167
+ const r = await syncGrokConfig(port, config, config.hostname ? { hostname: config.hostname } : {});
168
+ out.push(r.ok
169
+ ? { client: "grok", ok: true, changed: r.changed === true }
170
+ : { client: "grok", ok: false, reason: r.message });
171
+ } catch (error) {
172
+ out.push({ client: "grok", ok: false, reason: error instanceof Error ? error.message : String(error) });
173
+ }
174
+ }
175
+
176
+ if (claudeDesktopIntegrationEnabled(config)) {
177
+ try {
178
+ const { writeDesktop3pConfig } = await import("../../claude/desktop-3p");
179
+ const { desktopVisibleNativeSlugs, filterCatalogVisibleModels } = await import("../../codex/catalog");
180
+ const { fetchAllModels } = await import("../management-api");
181
+ const routed = filterCatalogVisibleModels(await fetchAllModels(config), config)
182
+ .map(model => ({ provider: model.provider, id: model.id, contextWindow: model.contextWindow }));
183
+ const r = writeDesktop3pConfig(
184
+ port,
185
+ [...desktopVisibleNativeSlugs(config)],
186
+ routed,
187
+ config.apiKeys?.[0]?.key,
188
+ "static",
189
+ config.claudeCode?.desktopProfile,
190
+ nativeContextLimits(config),
191
+ );
192
+ out.push(r.written
193
+ ? { client: "claude-desktop", ok: true, changed: true }
194
+ : { client: "claude-desktop", ok: false, reason: r.reason ?? "Claude Desktop write failed" });
195
+ } catch (error) {
196
+ out.push({ client: "claude-desktop", ok: false, reason: error instanceof Error ? error.message : String(error) });
197
+ }
198
+ }
199
+
200
+ try {
201
+ const { refreshOwnedIntegration } = await import("../../integrations/owned-refresh");
202
+ const result = await refreshOwnedIntegration({
203
+ clientId: "mcode",
204
+ models: async () => {
205
+ const { loadExportModels } = await import("./model-rows");
206
+ return loadExportModels(config);
207
+ },
208
+ config,
209
+ port,
210
+ });
211
+ if (result) {
212
+ out.push(result.ok
213
+ ? {
214
+ client: "mcode",
215
+ ok: true,
216
+ changed: result.changed === true,
217
+ ...(result.reason ? { reason: result.reason } : {}),
218
+ }
219
+ : { client: "mcode", ok: false, reason: result.reason });
220
+ }
221
+ } catch (error) {
222
+ out.push({ client: "mcode", ok: false, reason: error instanceof Error ? error.message : String(error) });
223
+ }
224
+
225
+ return out;
226
+ }
227
+
228
+ function publicVisionSidecarSettings(
229
+ config: OcxConfig,
230
+ vision: Awaited<ReturnType<typeof sidecarVisionResponseSettings>>,
231
+ ) {
232
+ const vs = config.visionSidecar ?? {};
233
+ return {
234
+ enabled: vs.enabled !== false,
235
+ model: vision.model,
236
+ backend: vs.backend,
237
+ reasoning: vision.reasoning,
238
+ maxDescriptionsPerTurn: resolveMaxDescriptionsPerTurn(vs.maxDescriptionsPerTurn),
239
+ timeoutMs: resolveVisionTimeoutMs(vs.timeoutMs),
240
+ };
241
+ }
242
+
76
243
  export async function handleConfigRoutes(ctx: ManagementContext): Promise<Response | null> {
77
- const { req, url, config, deps, refreshCodexCatalogBestEffort, syncClaudeAgentDefsBestEffort } = ctx;
244
+ const { req, url, config, deps, convergeCodexCatalog, syncClaudeAgentDefsBestEffort } = ctx;
245
+ const readStartupHealth = deps.getCachedStartupHealth ?? getCachedStartupHealth;
78
246
  if (url.pathname === "/api/config" && req.method === "GET") {
79
- return jsonResponse(safeConfigDTO(config));
247
+ return jsonResponse(withProviderServiceTierDTO(safeConfigDTO(config), config));
80
248
  }
81
249
 
82
250
  if (url.pathname === "/api/config" && req.method === "PUT") {
@@ -129,7 +297,8 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
129
297
  hostname: config.hostname ?? "127.0.0.1",
130
298
  streamMode: config.streamMode ?? "auto",
131
299
  appOwnedMemoryBudgetMb: config.appOwnedMemoryBudgetMb ?? 256,
132
- startupHealth: await getCachedStartupHealth(config),
300
+ codexAccountPickerEnabled: codexAccountPickerEnabled(config),
301
+ startupHealth: await readStartupHealth(config),
133
302
  codexRuntime: {
134
303
  path: displayCodexRuntimePath(resolved.runtime.command),
135
304
  version: resolved.runtime.version,
@@ -151,7 +320,7 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
151
320
  }
152
321
 
153
322
  if (url.pathname === "/api/startup-health" && req.method === "GET") {
154
- return jsonResponse(await getCachedStartupHealth(config));
323
+ return jsonResponse(await readStartupHealth(config));
155
324
  }
156
325
 
157
326
  if (url.pathname === "/api/startup-action" && req.method === "POST") {
@@ -205,10 +374,20 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
205
374
  // (a Windows service does not inherit shell env). A stream-shape
206
375
  // change applies to NEW turns only — the config object is shared by
207
376
  // reference with the request handlers, no restart needed.
208
- let body: { codexAutoStart?: unknown; streamMode?: unknown; appOwnedMemoryBudgetMb?: unknown };
209
- try { body = await readManagementJsonBody(req); } catch (error) { rethrowManagementBodyTooLarge(error); return jsonResponse({ error: "invalid JSON body" }, 400); }
210
- if (body.codexAutoStart === undefined && body.streamMode === undefined && body.appOwnedMemoryBudgetMb === undefined) {
211
- return jsonResponse({ error: "provide codexAutoStart, streamMode, or appOwnedMemoryBudgetMb" }, 400);
377
+ let parsedBody: unknown;
378
+ try { parsedBody = await readManagementJsonBody(req); } catch (error) { rethrowManagementBodyTooLarge(error); return jsonResponse({ error: "invalid JSON body" }, 400); }
379
+ if (!isPlainRecord(parsedBody)) return jsonResponse({ error: "settings body must be an object" }, 400);
380
+ const body = parsedBody as {
381
+ codexAutoStart?: unknown;
382
+ streamMode?: unknown;
383
+ appOwnedMemoryBudgetMb?: unknown;
384
+ codexAccountPickerEnabled?: unknown;
385
+ };
386
+ if (body.codexAutoStart === undefined
387
+ && body.streamMode === undefined
388
+ && body.appOwnedMemoryBudgetMb === undefined
389
+ && body.codexAccountPickerEnabled === undefined) {
390
+ return jsonResponse({ error: "provide codexAutoStart, streamMode, appOwnedMemoryBudgetMb, or codexAccountPickerEnabled" }, 400);
212
391
  }
213
392
  if (body.codexAutoStart !== undefined && typeof body.codexAutoStart !== "boolean") {
214
393
  return jsonResponse({ error: "codexAutoStart boolean is required" }, 400);
@@ -216,6 +395,10 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
216
395
  if (body.streamMode !== undefined && !isStreamMode(body.streamMode)) {
217
396
  return jsonResponse({ error: "streamMode must be auto, legacy-tee, or eager-relay" }, 400);
218
397
  }
398
+ if (body.codexAccountPickerEnabled !== undefined
399
+ && typeof body.codexAccountPickerEnabled !== "boolean") {
400
+ return jsonResponse({ error: "codexAccountPickerEnabled boolean is required" }, 400);
401
+ }
219
402
  if (body.appOwnedMemoryBudgetMb !== undefined && (
220
403
  typeof body.appOwnedMemoryBudgetMb !== "number"
221
404
  || !Number.isInteger(body.appOwnedMemoryBudgetMb)
@@ -224,31 +407,77 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
224
407
  )) {
225
408
  return jsonResponse({ error: `appOwnedMemoryBudgetMb must be an integer from ${MIN_APP_OWNED_MEMORY_BUDGET_MB} to ${MAX_APP_OWNED_MEMORY_BUDGET_MB}` }, 400);
226
409
  }
227
- if (typeof body.codexAutoStart === "boolean") {
228
- config.codexAutoStart = body.codexAutoStart;
229
- }
230
- if (body.streamMode !== undefined) {
231
- if (body.streamMode === "auto") {
232
- delete config.streamMode;
233
- } else {
234
- config.streamMode = body.streamMode as "legacy-tee" | "eager-relay";
410
+ const previousSettings = {
411
+ codexAutoStart: config.codexAutoStart,
412
+ hasCodexAutoStart: Object.hasOwn(config, "codexAutoStart"),
413
+ streamMode: config.streamMode,
414
+ hasStreamMode: Object.hasOwn(config, "streamMode"),
415
+ appOwnedMemoryBudgetMb: config.appOwnedMemoryBudgetMb,
416
+ hasAppOwnedMemoryBudgetMb: Object.hasOwn(config, "appOwnedMemoryBudgetMb"),
417
+ codexAccountNamespaces: config.codexAccountNamespaces,
418
+ hasCodexAccountNamespaces: Object.hasOwn(config, "codexAccountNamespaces"),
419
+ codexAccountPickerEnabled: config.codexAccountPickerEnabled,
420
+ hasCodexAccountPickerEnabled: Object.hasOwn(config, "codexAccountPickerEnabled"),
421
+ };
422
+ const pickerWasEnabled = codexAccountPickerEnabled(config);
423
+ let pickerIsEnabled = pickerWasEnabled;
424
+ try {
425
+ if (typeof body.codexAutoStart === "boolean") {
426
+ config.codexAutoStart = body.codexAutoStart;
235
427
  }
428
+ if (body.streamMode !== undefined) {
429
+ if (body.streamMode === "auto") {
430
+ delete config.streamMode;
431
+ } else {
432
+ config.streamMode = body.streamMode as "legacy-tee" | "eager-relay";
433
+ }
434
+ }
435
+ if (typeof body.appOwnedMemoryBudgetMb === "number") {
436
+ config.appOwnedMemoryBudgetMb = body.appOwnedMemoryBudgetMb;
437
+ }
438
+ if (body.codexAccountPickerEnabled === true) {
439
+ config.codexAccountPickerEnabled = true;
440
+ initializeDefaultCodexAccountNamespaces(config);
441
+ } else if (body.codexAccountPickerEnabled === false) {
442
+ config.codexAccountPickerEnabled = false;
443
+ }
444
+ pickerIsEnabled = codexAccountPickerEnabled(config);
445
+ (deps.saveConfigPreservingClaudeCode ?? saveConfigPreservingClaudeCode)(config);
446
+ } catch (error) {
447
+ if (previousSettings.hasCodexAutoStart) config.codexAutoStart = previousSettings.codexAutoStart;
448
+ else delete config.codexAutoStart;
449
+ if (previousSettings.hasStreamMode) config.streamMode = previousSettings.streamMode;
450
+ else delete config.streamMode;
451
+ if (previousSettings.hasAppOwnedMemoryBudgetMb) {
452
+ config.appOwnedMemoryBudgetMb = previousSettings.appOwnedMemoryBudgetMb;
453
+ } else delete config.appOwnedMemoryBudgetMb;
454
+ if (previousSettings.hasCodexAccountNamespaces) {
455
+ config.codexAccountNamespaces = previousSettings.codexAccountNamespaces;
456
+ } else delete config.codexAccountNamespaces;
457
+ if (previousSettings.hasCodexAccountPickerEnabled) {
458
+ config.codexAccountPickerEnabled = previousSettings.codexAccountPickerEnabled;
459
+ } else delete config.codexAccountPickerEnabled;
460
+ throw error;
236
461
  }
237
- if (typeof body.appOwnedMemoryBudgetMb === "number") {
238
- config.appOwnedMemoryBudgetMb = body.appOwnedMemoryBudgetMb;
239
- }
240
- saveConfigPreservingClaudeCode(config);
241
462
  if (typeof body.appOwnedMemoryBudgetMb === "number") {
242
463
  configureAppOwnedMemoryBudget(resolveAppOwnedMemoryBudgetBytes(body.appOwnedMemoryBudgetMb));
243
464
  enforceAppOwnedMemoryBudget();
244
465
  }
466
+ const catalogRefresh = pickerWasEnabled !== pickerIsEnabled
467
+ ? await convergeCodexCatalog()
468
+ : undefined;
469
+ const catalogRefreshPending = catalogRefresh
470
+ ? catalogRefreshIsPending(catalogRefresh)
471
+ : false;
245
472
  invalidateStartupHealthCache();
246
473
  return jsonResponse({
247
474
  ok: true,
248
475
  codexAutoStart: codexAutoStartEnabled(config),
249
476
  streamMode: config.streamMode ?? "auto",
250
477
  appOwnedMemoryBudgetMb: config.appOwnedMemoryBudgetMb ?? 256,
251
- startupHealth: await getCachedStartupHealth(config),
478
+ codexAccountPickerEnabled: pickerIsEnabled,
479
+ catalogRefreshPending,
480
+ startupHealth: await readStartupHealth(config),
252
481
  });
253
482
  }
254
483
 
@@ -261,11 +490,25 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
261
490
  if (url.pathname === "/api/sync" && req.method === "POST") {
262
491
  const { syncModelsToCodex } = await import("../../codex/sync");
263
492
  const { attachStaleAppServerHint } = await import("../../codex/app-server-processes");
264
- const result = await syncModelsToCodex(undefined, config, null);
493
+ const { readRuntimePort, loadConfig } = await import("../../config");
494
+ // Never use the server-captured startup object for a durable integration
495
+ // decision. A toggle may have persisted while this process was gathering.
496
+ const runtime = readRuntimePort(process.pid);
497
+ const config = loadConfig();
498
+ const result = await syncModelsToCodex(runtime?.port, config, null);
499
+ // A sync used to stop here, so a Grok fence or a Desktop profile kept whatever
500
+ // context windows it was written with while the Codex catalog moved on. The
501
+ // startup path already fans out to every enabled client; this is the same fan-out
502
+ // for the on-demand command. Codex goes first because the others read its catalog.
503
+ const integrations = result.status === "refused"
504
+ ? []
505
+ : await syncEnabledClientIntegrations(runtime?.port, config);
506
+ const status = result.status === "refused" ? 409 : (result.status === "skipped" || result.ok ? 200 : 500);
265
507
  return jsonResponse({
266
508
  ...attachStaleAppServerHint(result),
509
+ ...(integrations.length > 0 ? { integrations } : {}),
267
510
  ...(result.ok ? {} : { error: result.message }),
268
- }, result.ok ? 200 : 500);
511
+ }, status);
269
512
  }
270
513
 
271
514
  if (url.pathname === "/api/update/check" && req.method === "GET") {
@@ -306,14 +549,20 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
306
549
 
307
550
  if (url.pathname === "/api/sidecar-settings" && req.method === "GET") {
308
551
  const ws = config.webSearchSidecar ?? {};
309
- const vs = config.visionSidecar ?? {};
552
+ const vision = await sidecarVisionResponseSettings(config);
553
+ const webSearchCandidates = await webSearchCandidateRows(config);
310
554
  return jsonResponse({
311
- webSearch: { model: ws.model ?? "gpt-5.6-luna", backend: ws.backend },
312
- vision: {
313
- model: vs.model ?? "gpt-5.6-luna",
314
- backend: vs.backend,
315
- maxDescriptionsPerTurn: vs.maxDescriptionsPerTurn,
555
+ webSearch: {
556
+ model: ws.model ?? "gpt-5.6-luna",
557
+ backend: ws.backend,
558
+ streamRoutedModelOutput: ws.streamRoutedModelOutput === true,
559
+ ...(ws.xSearch ? { xSearch: ws.xSearch } : {}),
316
560
  },
561
+ vision: publicVisionSidecarSettings(config, vision),
562
+ visionModels: vision.models,
563
+ // ALWAYS present: the dashboard treats an omitted list as "no filter" and
564
+ // falls back to the full model union, so empty must be [] (review B3).
565
+ webSearchModels: webSearchModelOptionsFrom(config, webSearchCandidates),
317
566
  });
318
567
  }
319
568
 
@@ -326,16 +575,32 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
326
575
  if (raw.webSearch !== undefined && !isPlainRecord(raw.webSearch)) return jsonResponse({ error: "webSearch must be an object" }, 400);
327
576
  if (raw.vision !== undefined && !isPlainRecord(raw.vision)) return jsonResponse({ error: "vision must be an object" }, 400);
328
577
  const body = raw as {
329
- webSearch?: { model?: unknown; backend?: unknown; reasoning?: unknown };
330
- vision?: { model?: unknown; backend?: unknown; maxDescriptionsPerTurn?: unknown };
578
+ webSearch?: { model?: unknown; backend?: unknown; reasoning?: unknown; streamRoutedModelOutput?: unknown; exaApiKey?: unknown; xSearch?: unknown };
579
+ vision?: {
580
+ model?: unknown;
581
+ backend?: unknown;
582
+ reasoning?: unknown;
583
+ maxDescriptionsPerTurn?: unknown;
584
+ enabled?: unknown;
585
+ timeoutMs?: unknown;
586
+ };
331
587
  };
588
+ const WEB_SEARCH_BACKENDS_UNION = ["openai", "anthropic", "xai", "gemini", "exa"] as const;
332
589
  if (body.webSearch && body.webSearch.backend !== undefined && body.webSearch.backend !== null
333
- && body.webSearch.backend !== "openai" && body.webSearch.backend !== "anthropic") {
334
- return jsonResponse({ error: "webSearch.backend must be openai, anthropic, or null" }, 400);
590
+ && !WEB_SEARCH_BACKENDS_UNION.includes(body.webSearch.backend as never)) {
591
+ return jsonResponse({ error: "webSearch.backend must be openai, anthropic, xai, gemini, exa, or null" }, 400);
592
+ }
593
+ if (body.webSearch?.model !== undefined && typeof body.webSearch.model !== "string") {
594
+ return jsonResponse({ error: "webSearch.model must be a string" }, 400);
595
+ }
596
+ if (body.webSearch && body.webSearch.streamRoutedModelOutput !== undefined
597
+ && typeof body.webSearch.streamRoutedModelOutput !== "boolean") {
598
+ return jsonResponse({ error: "webSearch.streamRoutedModelOutput must be a boolean" }, 400);
335
599
  }
336
600
  if (body.vision && body.vision.backend !== undefined
337
- && body.vision.backend !== null && body.vision.backend !== "openai" && body.vision.backend !== "anthropic") {
338
- return jsonResponse({ error: "vision.backend must be openai, anthropic, or null" }, 400);
601
+ && body.vision.backend !== null && body.vision.backend !== "openai" && body.vision.backend !== "anthropic"
602
+ && body.vision.backend !== "routed") {
603
+ return jsonResponse({ error: "vision.backend must be openai, anthropic, routed, or null" }, 400);
339
604
  }
340
605
  if (body.vision && body.vision.maxDescriptionsPerTurn !== undefined
341
606
  && (typeof body.vision.maxDescriptionsPerTurn !== "number"
@@ -343,17 +608,145 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
343
608
  || body.vision.maxDescriptionsPerTurn <= 0)) {
344
609
  return jsonResponse({ error: "vision.maxDescriptionsPerTurn must be a positive integer" }, 400);
345
610
  }
611
+ if (body.vision && body.vision.enabled !== undefined && typeof body.vision.enabled !== "boolean") {
612
+ return jsonResponse({ error: "vision.enabled must be a boolean" }, 400);
613
+ }
614
+ if (body.vision && body.vision.timeoutMs !== undefined && !isValidVisionTimeoutMs(body.vision.timeoutMs)) {
615
+ return jsonResponse({
616
+ error: `vision.timeoutMs must be an integer from ${MIN_VISION_TIMEOUT_MS} to ${MAX_VISION_TIMEOUT_MS}`,
617
+ }, 400);
618
+ }
619
+ if (body.vision?.reasoning !== undefined && !isVisionReasoningEffort(body.vision.reasoning)) {
620
+ return jsonResponse({ error: `vision.reasoning must be ${VISION_REASONING_EFFORTS.join(", ")}` }, 400);
621
+ }
622
+ // Reject ONLY a model we can prove is blind. An id nothing knows about stays
623
+ // allowed: the operator may be ahead of our catalog, and the runtime never
624
+ // required catalog membership (`tests/vision-reasoning-contract.test.ts`
625
+ // pins `custom-vision` → 200). The catalog is read ONCE and reused for the
626
+ // rejection body, so a 400 cannot cost two provider fetches.
627
+ if (body.vision && typeof body.vision.model === "string" && body.vision.model !== "") {
628
+ const requested = body.vision.model;
629
+ const candidates = await visionCandidateRows(config);
630
+ const hint = body.vision.backend === "anthropic" || body.vision.backend === "openai"
631
+ || body.vision.backend === "routed"
632
+ ? body.vision.backend
633
+ : config.visionSidecar?.backend;
634
+ // Coherence (roadmap 170 r2): the forward/OAuth executors POST the model
635
+ // string VERBATIM, so a namespaced id on those backends persists a wire
636
+ // id they cannot run; and "routed" without a namespace cannot route.
637
+ const effectiveBackend = hint ?? "openai";
638
+ const namespaced = requested.includes("/");
639
+ if (namespaced && effectiveBackend !== "routed") {
640
+ return jsonResponse({ error: `vision.model "${requested}" is provider-namespaced; it requires vision.backend "routed"` }, 400);
641
+ }
642
+ if (!namespaced && effectiveBackend === "routed") {
643
+ return jsonResponse({ error: `vision.backend "routed" requires a provider-namespaced vision.model ("provider/model"); got "${requested}"` }, 400);
644
+ }
645
+ if (visionDescriberIsProvablyBlind(config, requested, candidates, hint)) {
646
+ return jsonResponse(visionDescriberRejection("vision.model", requested, config, candidates), 400);
647
+ }
648
+ }
649
+
650
+ let normalizedVisionReasoning: ReturnType<typeof normalizeVisionReasoningForModel>;
651
+ let visionReasoningTouched = false;
652
+ if (body.vision && (body.vision.model !== undefined || body.vision.reasoning !== undefined)) {
653
+ visionReasoningTouched = true;
654
+ const model = typeof body.vision.model === "string"
655
+ ? (body.vision.model === "" ? "gpt-5.4-mini" : body.vision.model)
656
+ : (config.visionSidecar?.model || "gpt-5.4-mini");
657
+ const sourceReasoning = body.vision.reasoning ?? config.visionSidecar?.reasoning;
658
+ normalizedVisionReasoning = sourceReasoning === undefined
659
+ ? undefined
660
+ : normalizeVisionReasoningForModel(model, sourceReasoning);
661
+ }
662
+
346
663
  if (body.webSearch) {
347
- config.webSearchSidecar = { ...config.webSearchSidecar };
664
+ const pairTouched = body.webSearch.model !== undefined || body.webSearch.backend !== undefined;
665
+ const effectiveBackend = body.webSearch.backend === "anthropic"
666
+ ? "anthropic"
667
+ : body.webSearch.backend === "openai" || body.webSearch.backend === null
668
+ ? "openai"
669
+ : config.webSearchSidecar?.backend ?? "openai";
670
+ const effectiveModel = typeof body.webSearch.model === "string"
671
+ ? body.webSearch.model || undefined
672
+ : config.webSearchSidecar?.model;
673
+ if (pairTouched && effectiveModel) {
674
+ const candidates = await webSearchCandidateRows(config);
675
+ if (webSearchModelIsRejected(effectiveBackend, effectiveModel, candidates)) {
676
+ return jsonResponse(webSearchModelRejection("webSearch.model", effectiveBackend, effectiveModel, candidates), 400);
677
+ }
678
+ }
679
+ const webSearchCandidate = { ...config.webSearchSidecar };
348
680
  if (typeof body.webSearch.model === "string") {
349
- if (body.webSearch.model === "") delete config.webSearchSidecar.model;
350
- else config.webSearchSidecar.model = body.webSearch.model;
681
+ if (body.webSearch.model === "") delete webSearchCandidate.model;
682
+ else webSearchCandidate.model = body.webSearch.model;
351
683
  }
352
- if (body.webSearch.backend === null) delete config.webSearchSidecar.backend;
353
- else if (body.webSearch.backend === "openai" || body.webSearch.backend === "anthropic") {
354
- config.webSearchSidecar.backend = body.webSearch.backend;
684
+ if (body.webSearch.backend === null) delete webSearchCandidate.backend;
685
+ else if (WEB_SEARCH_BACKENDS_UNION.includes(body.webSearch.backend as never)) {
686
+ webSearchCandidate.backend = body.webSearch.backend as typeof WEB_SEARCH_BACKENDS_UNION[number];
355
687
  }
356
- if (typeof body.webSearch.reasoning === "string") config.webSearchSidecar.reasoning = body.webSearch.reasoning;
688
+ if (typeof body.webSearch.reasoning === "string") webSearchCandidate.reasoning = body.webSearch.reasoning;
689
+ // Operator secret for the exa backend: string sets, empty string clears. The GET
690
+ // payload deliberately never carries it and redact.ts strips the key from logs.
691
+ if (typeof body.webSearch.exaApiKey === "string") {
692
+ if (body.webSearch.exaApiKey === "") delete webSearchCandidate.exaApiKey;
693
+ else webSearchCandidate.exaApiKey = body.webSearch.exaApiKey;
694
+ }
695
+ // Opt-in x_search block (L7): null clears; an object is doc-validated before persisting.
696
+ if (body.webSearch.xSearch === null) delete webSearchCandidate.xSearch;
697
+ else if (body.webSearch.xSearch !== undefined) {
698
+ if (!isPlainRecord(body.webSearch.xSearch)) {
699
+ return jsonResponse({ error: "webSearch.xSearch must be an object or null" }, 400);
700
+ }
701
+ const x = body.webSearch.xSearch as Record<string, unknown>;
702
+ const allowedXSearchKeys = new Set([
703
+ "enabled",
704
+ "allowedXHandles",
705
+ "excludedXHandles",
706
+ "fromDate",
707
+ "toDate",
708
+ ]);
709
+ const unknownKey = Object.keys(x).find(key => !allowedXSearchKeys.has(key));
710
+ if (unknownKey !== undefined) {
711
+ return jsonResponse({ error: `webSearch.xSearch.${unknownKey} is not a supported field` }, 400);
712
+ }
713
+ if (x.enabled !== undefined && typeof x.enabled !== "boolean") {
714
+ return jsonResponse({ error: "webSearch.xSearch.enabled must be a boolean" }, 400);
715
+ }
716
+ for (const field of ["allowedXHandles", "excludedXHandles"] as const) {
717
+ const value = x[field];
718
+ if (value !== undefined && (!Array.isArray(value) || !value.every(handle => typeof handle === "string"))) {
719
+ return jsonResponse({ error: `webSearch.xSearch.${field} must be an array of strings` }, 400);
720
+ }
721
+ }
722
+ for (const field of ["fromDate", "toDate"] as const) {
723
+ if (x[field] !== undefined && typeof x[field] !== "string") {
724
+ return jsonResponse({ error: `webSearch.xSearch.${field} must be an ISO-8601 date (YYYY-MM-DD)` }, 400);
725
+ }
726
+ }
727
+ const candidate = {
728
+ ...(x.enabled === true ? { enabled: true } : {}),
729
+ ...(x.allowedXHandles !== undefined ? { allowedXHandles: x.allowedXHandles as string[] } : {}),
730
+ ...(x.excludedXHandles !== undefined ? { excludedXHandles: x.excludedXHandles as string[] } : {}),
731
+ ...(x.fromDate !== undefined ? { fromDate: x.fromDate as string } : {}),
732
+ ...(x.toDate !== undefined ? { toDate: x.toDate as string } : {}),
733
+ };
734
+ const invalid = validateXaiSearchOptions({
735
+ xSearch: candidate.enabled,
736
+ allowedXHandles: candidate.allowedXHandles,
737
+ excludedXHandles: candidate.excludedXHandles,
738
+ fromDate: candidate.fromDate,
739
+ toDate: candidate.toDate,
740
+ });
741
+ if (invalid) return jsonResponse({ error: `webSearch.xSearch invalid: ${invalid}` }, 400);
742
+ webSearchCandidate.xSearch = candidate;
743
+ }
744
+ if (typeof body.webSearch.streamRoutedModelOutput === "boolean") {
745
+ // `false` is the default — drop the key so config files stay minimal.
746
+ if (body.webSearch.streamRoutedModelOutput) webSearchCandidate.streamRoutedModelOutput = true;
747
+ else delete webSearchCandidate.streamRoutedModelOutput;
748
+ }
749
+ config.webSearchSidecar = webSearchCandidate;
357
750
  }
358
751
  if (body.vision) {
359
752
  config.visionSidecar = { ...config.visionSidecar };
@@ -362,24 +755,44 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
362
755
  else config.visionSidecar.model = body.vision.model;
363
756
  }
364
757
  if (body.vision.backend === null) delete config.visionSidecar.backend;
365
- else if (body.vision.backend === "openai" || body.vision.backend === "anthropic") {
758
+ else if (body.vision.backend === "openai" || body.vision.backend === "anthropic"
759
+ || body.vision.backend === "routed") {
366
760
  config.visionSidecar.backend = body.vision.backend;
367
761
  }
368
762
  if (typeof body.vision.maxDescriptionsPerTurn === "number") {
369
763
  config.visionSidecar.maxDescriptionsPerTurn = body.vision.maxDescriptionsPerTurn;
370
764
  }
765
+ if (typeof body.vision.enabled === "boolean") {
766
+ // `true` is the default — drop the key so disable/re-enable does not rewrite the file.
767
+ if (body.vision.enabled) delete config.visionSidecar.enabled;
768
+ else config.visionSidecar.enabled = false;
769
+ }
770
+ if (typeof body.vision.timeoutMs === "number") {
771
+ config.visionSidecar.timeoutMs = body.vision.timeoutMs;
772
+ }
773
+ if (visionReasoningTouched) {
774
+ if (normalizedVisionReasoning === undefined) delete config.visionSidecar.reasoning;
775
+ else config.visionSidecar.reasoning = normalizedVisionReasoning;
776
+ }
371
777
  }
372
778
  saveConfigPreservingClaudeCode(config);
373
779
  const ws = config.webSearchSidecar ?? {};
374
- const vs = config.visionSidecar ?? {};
780
+ const vision = await sidecarVisionResponseSettings(config);
781
+ const savedWebSearchCandidates = await webSearchCandidateRows(config);
375
782
  return jsonResponse({
376
783
  ok: true,
377
- webSearch: { model: ws.model ?? "gpt-5.6-luna", backend: ws.backend },
378
- vision: {
379
- model: vs.model ?? "gpt-5.6-luna",
380
- backend: vs.backend,
381
- maxDescriptionsPerTurn: vs.maxDescriptionsPerTurn,
784
+ webSearch: {
785
+ model: ws.model ?? "gpt-5.6-luna",
786
+ backend: ws.backend,
787
+ streamRoutedModelOutput: ws.streamRoutedModelOutput === true,
788
+ ...(ws.xSearch ? { xSearch: ws.xSearch } : {}),
382
789
  },
790
+ vision: publicVisionSidecarSettings(config, vision),
791
+ visionModels: vision.models,
792
+ // Echoed for the same reason GET always carries it: the dashboard rebuilds
793
+ // its sidecar state from this body, and an omitted key reads as "old
794
+ // server" and falls back to the full union (review F1).
795
+ webSearchModels: webSearchModelOptionsFrom(config, savedWebSearchCandidates),
383
796
  });
384
797
  }
385
798