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