@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
@@ -29,8 +29,48 @@ function readInputModalities(raw: unknown): { values?: string[]; error?: string
29
29
  }
30
30
  return { values: raw as string[] };
31
31
  }
32
+
33
+ /**
34
+ * Custom-row reasoning ladder. Labels are validated against the Codex ladder (low..ultra)
35
+ * exactly like provider `modelReasoningEfforts` values; unknown labels would otherwise
36
+ * surface in a catalog the upstream never accepts. An empty array is meaningful (explicit
37
+ * "no reasoning" hides the effort control) and must be preserved, not cleared.
38
+ */
39
+ function readReasoningEfforts(raw: unknown): { values?: string[]; error?: string } {
40
+ if (raw === undefined) return {};
41
+ if (!Array.isArray(raw)) return { error: "reasoningEfforts must be an array" };
42
+ const rejected: string[] = [];
43
+ const values: string[] = [];
44
+ for (const value of raw) {
45
+ if (typeof value !== "string") return { error: "reasoningEfforts must contain only strings" };
46
+ if (!isDeclaredReasoningEffort(value)) { rejected.push(value); continue; }
47
+ if (!values.includes(value)) values.push(value);
48
+ }
49
+ if (rejected.length > 0) {
50
+ return { error: `unsupported reasoning effort: ${rejected.join(", ")} (allowed: none, minimal, low, medium, high, xhigh, max, ultra)` };
51
+ }
52
+ // Canonical order: the catalog writes supported_reasoning_levels in input order and the
53
+ // fallback default picks the first entry, so a caller-chosen order must not leak through.
54
+ return { values: canonicalizeReasoningEfforts(values) };
55
+ }
56
+
57
+ /** Default effort must be a ladder member that the declared ladder actually includes. */
58
+ function readDefaultReasoningEffort(raw: unknown, efforts: string[] | undefined): { value?: string; error?: string } {
59
+ if (raw === undefined) return {};
60
+ if (raw === null) return { value: undefined };
61
+ if (typeof raw !== "string" || !isDeclaredReasoningEffort(raw)) {
62
+ return { error: "defaultReasoningEffort must be one of: none, minimal, low, medium, high, xhigh, max, ultra" };
63
+ }
64
+ if (efforts === undefined || efforts.length === 0) {
65
+ return { error: "defaultReasoningEffort requires a non-empty reasoningEfforts ladder" };
66
+ }
67
+ if (!efforts.includes(raw)) {
68
+ return { error: `defaultReasoningEffort "${raw}" is not in the declared reasoningEfforts ladder` };
69
+ }
70
+ return { value: raw };
71
+ }
32
72
  import type { CatalogModel } from "../../codex/catalog";
33
- import { catalogModelSlug, disabledNativeSlugs, invalidateCodexModelsCache, nativeModelRows, uniqueCatalogModelsForPublicList } from "../../codex/catalog";
73
+ import { accountBoundNativeOpenAiSlugsBySelector, catalogModelSlug, configuredNativeAliasSlugs, disabledNativeSlugs, invalidateCodexModelsCache, nativeModelRows, shouldIncludeAccountBoundNativeOpenAi, uniqueCatalogModelsForPublicList } from "../../codex/catalog";
34
74
  import { CatalogGatherBusyError } from "../../codex/catalog/provider-fetch";
35
75
  import { getProviderLiveModelCount } from "../../codex/model-cache";
36
76
  import {
@@ -57,8 +97,11 @@ import { providerDestinationResolvedError } from "../../lib/destination-policy";
57
97
  import { enrichProviderFromCatalog, listKeyLoginProviders } from "../../oauth/key-providers";
58
98
  import { deriveProviderPresets } from "../../providers/derive";
59
99
  import { providerCodexAccountMode } from "../../providers/registry";
60
- import { routedSlug, slugEquals } from "../../providers/slug-codec";
61
- import { COMBO_NAMESPACE, comboModelId, comboPublicModelId, preservesPhysicalComboProvider } from "../../combos";
100
+ import { encodedModelIdCollides, routedSlug, slugEquals } from "../../providers/slug-codec";
101
+ import { knownModelIdsForProvider } from "../../router";
102
+ import { effectiveModelAliases, MODEL_ALIAS_PATTERN } from "../../providers/default-aliases";
103
+ import { comboPublicModelId } from "../../combos/types";
104
+ import { COMBO_NAMESPACE, comboDisabledModelSelectors, comboModelId, preservesPhysicalComboProvider } from "../../combos";
62
105
  import { clearProviderQuotaCache, fetchProviderQuotaReports } from "../../providers/quota";
63
106
  import { isCanonicalOpenAiForwardProvider } from "../../providers/openai-tiers";
64
107
  import { clearThreadAccountMap } from "../../codex/routing";
@@ -71,6 +114,7 @@ import { parseRange, parseUsageSurface, summarizeUsage } from "../../usage/summa
71
114
  import { stripCodexRuntimeProviderFields } from "../../codex/auth-context";
72
115
  import { getProviderRegistryEntry } from "../../providers/registry";
73
116
  import { getDebugLogEntries } from "../../lib/debug-log-buffer";
117
+ import { canonicalizeReasoningEfforts, isDeclaredReasoningEffort } from "../../reasoning-effort";
74
118
  import { getInjectionDebugLogEntries } from "../../lib/injection-debug-log";
75
119
  import {
76
120
  clearDebugSettings,
@@ -87,10 +131,11 @@ import type { PersistedUsageAttempt } from "../../usage/log";
87
131
  import { isAllowedRequestOrigin, jsonResponse, providerManagementConfigError, publicProviderBaseUrl, safeConfigDTO, corsHeaders } from "../auth-cors";
88
132
  import { applySystemEnvToggle } from "../system-env";
89
133
  import {
134
+ ClientPathError,
90
135
  EXPORT_CLIENTS,
91
136
  EXPORT_CLIENT_IDS,
92
137
  OPENCODE_PROVIDER_ID,
93
- buildClientConfig,
138
+ buildClientConfigText,
94
139
  isExportClientId,
95
140
  opencodeProxyBaseUrl,
96
141
  } from "../../clients/config-export";
@@ -104,92 +149,14 @@ import type {
104
149
  import { isPlainRecord, parseDebugLogQuery, tokPerSecondResult, unavailableCostReason, costResult, requestLogDto, stripRegistryOnlyStaticHeaders, fetchAllModels } from "./shared";
105
150
  import type { MetricUnavailableReason, TokPerSecondResult, CostEstimateReason, CostResult, MetricSource } from "./shared";
106
151
  import type { ManagementContext } from "./context";
152
+ import { listManagementModelRows, loadExportModels } from "./model-rows";
107
153
  import { readManagementJsonBody, rethrowManagementBodyTooLarge } from "./body";
108
-
109
- /**
110
- * One row of the `/api/models` list. Routed rows spread a `CatalogModel`, so the shape is
111
- * that model plus the identity/visibility fields this boundary computes for every row
112
- * regardless of source. `disabled` is always present; the rest vary by row origin.
113
- */
114
- type ManagementModelRow = Partial<CatalogModel> & {
115
- provider: string;
116
- id: string;
117
- namespaced: string;
118
- disabled: boolean;
119
- native?: boolean;
120
- custom?: boolean;
121
- customId?: string;
122
- };
123
-
124
- /**
125
- * The exact row list `/api/models` returns. Extracted so `/api/client-config` exports the
126
- * models the GUI's Models tab shows — including this function's `disabled` computation,
127
- * which the export core (src/clients/config-export.ts) deliberately does not perform.
128
- */
129
- async function listManagementModelRows(config: OcxConfig): Promise<ManagementModelRow[]> {
130
- const models = await fetchAllModels(config);
131
- const disabled = new Set(config.disabledModels ?? []);
132
- // Native GPT passthrough rows lead (provider "openai", bare-slug namespaced ids): sourced
133
- // from the static supported set so a disabled model stays listed and re-enableable.
134
- const native: ManagementModelRow[] = nativeModelRows(config).map(row => ({
135
- provider: "openai",
136
- id: row.slug,
137
- namespaced: row.slug,
138
- disabled: row.disabled,
139
- native: true,
140
- ...(row.contextWindow !== undefined ? { contextWindow: row.contextWindow } : {}),
141
- }));
142
- const customModels: ManagementModelRow[] = (config.customModels ?? []).map(cm => {
143
- const namespaced = routedSlug(cm.provider, cm.modelId);
144
- return {
145
- provider: cm.provider,
146
- id: cm.modelId,
147
- namespaced,
148
- disabled: [...disabled].some(stored => slugEquals(stored, cm.provider, cm.modelId)),
149
- custom: true,
150
- customId: cm.id,
151
- displayName: cm.displayName,
152
- ...(cm.contextWindow ? { contextWindow: cm.contextWindow } : {}),
153
- ...(cm.inputModalities ? { inputModalities: cm.inputModalities } : {}),
154
- };
155
- });
156
- const publicModels = uniqueCatalogModelsForPublicList(models);
157
- const comboNamespaced = new Set(
158
- publicModels.filter(model => model.provider === "combo").map(catalogModelSlug),
159
- );
160
- const visibleCustomModels = customModels.filter(model => !comboNamespaced.has(model.namespaced));
161
- // Custom metadata wins when a physical live/static row resolves to the same Codex-facing
162
- // slug, while a combo keeps the same precedence it has in routing and /v1/models.
163
- const customNamespaced = new Set(visibleCustomModels.map(c => c.namespaced));
164
- const dedupedRouted = publicModels.map((m): ManagementModelRow | null => {
165
- // Codex-facing slug (one "/", slug-codec); disabledModels compares tolerate both forms.
166
- const namespaced = catalogModelSlug(m);
167
- if (m.provider !== "combo" && customNamespaced.has(namespaced)) return null;
168
- const contextCap = providerContextCap(config, m.provider);
169
- return {
170
- ...m,
171
- namespaced,
172
- disabled: [...disabled].some(stored => (
173
- stored === namespaced || slugEquals(stored, m.provider, m.id)
174
- )),
175
- ...(contextCap !== undefined ? { contextCap, contextCapped: m.contextCapped === true } : {}),
176
- };
177
- }).filter((row): row is ManagementModelRow => row !== null);
178
- return [...native, ...dedupedRouted, ...visibleCustomModels];
179
- }
180
-
181
- /** `/api/models` row → the narrower input the client-config serializers accept. */
182
- function toExportModel(row: ManagementModelRow): ExportModel {
183
- return {
184
- namespaced: row.namespaced,
185
- provider: row.provider,
186
- id: row.id,
187
- ...(row.native ? { native: true } : {}),
188
- ...(row.displayName ? { displayName: row.displayName } : {}),
189
- ...(row.contextWindow !== undefined ? { contextWindow: row.contextWindow } : {}),
190
- ...(row.inputModalities ? { inputModalities: row.inputModalities } : {}),
191
- };
192
- }
154
+ import {
155
+ hasModelPreset,
156
+ markModelPresetDiverged,
157
+ materializeModelPreset,
158
+ modelPresetFor,
159
+ } from "../../providers/model-presets";
193
160
 
194
161
  /**
195
162
  * Counts read back off the SERIALIZED document rather than recomputed from the input rows.
@@ -198,23 +165,172 @@ function toExportModel(row: ManagementModelRow): ExportModel {
198
165
  * core's "authoritative context window" rule would be free to drift from it silently.
199
166
  */
200
167
  function summarizeExportedModels(client: ExportClientId, document: unknown): { modelCount: number; modelsWithoutLimits: number } {
201
- if (client === "opencode") {
202
- const models = (document as OpencodeGeneratedConfig).provider[OPENCODE_PROVIDER_ID].models;
203
- const entries = Object.values(models);
204
- return { modelCount: entries.length, modelsWithoutLimits: entries.filter(entry => entry.limit === undefined).length };
205
- }
206
- const models = (document as PiGeneratedConfig).providers[OPENCODE_PROVIDER_ID].models;
207
- return { modelCount: models.length, modelsWithoutLimits: models.filter(entry => entry.contextWindow === undefined).length };
168
+ // Each client counts its own document shape. The previous branch assumed
169
+ // "anything that is not OpenCode must be Pi", which silently misread the
170
+ // moment a third client existed.
171
+ return EXPORT_CLIENTS[client].summarize(document);
208
172
  }
209
173
 
210
174
  export async function handleModelRoutes(ctx: ManagementContext): Promise<Response | null> {
211
- const { req, url, config, deps, refreshCodexCatalogBestEffort, syncClaudeAgentDefsBestEffort } = ctx;
175
+ const { req, url, config, deps, convergeCodexCatalog, syncClaudeAgentDefsBestEffort } = ctx;
212
176
  // A handler persists the exact config object passed in. Production defaults to
213
177
  // the real store; tests that pass an in-memory fixture inject a no-op/spy. Do not
214
178
  // bypass this seam with a dynamic config import — doing so replaced a user's
215
179
  // ~/.opencodex/config.json with the `existing-uuid` test fixture.
216
180
  const persistConfig = deps.saveConfigPreservingClaudeCode ?? saveConfigPreservingClaudeCode;
217
181
 
182
+ if (url.pathname === "/api/model-discovery" && req.method === "GET") {
183
+ const providers = Object.fromEntries(Object.entries(config.providers).map(([name, provider]) => [
184
+ name, provider.newModelPolicy ?? "inherit",
185
+ ]));
186
+ const recentArrivals = Object.fromEntries(Object.entries(config.modelDiscovery?.recentArrivals ?? {}).map(([name, rows]) => [
187
+ name,
188
+ rows.map(row => ({
189
+ ...row,
190
+ state: (config.disabledModels ?? []).some(slug => slugEquals(slug, name, row.id))
191
+ ? "auto-disabled" : "enabled",
192
+ })),
193
+ ]));
194
+ const baselineCounts = Object.fromEntries(Object.entries(config.modelDiscovery?.knownModels ?? {}).map(([name, baseline]) => [
195
+ name, baseline.ids.length,
196
+ ]));
197
+ return jsonResponse({
198
+ policy: config.modelDiscovery?.newModelPolicy ?? "on", providers, recentArrivals, baselineCounts,
199
+ });
200
+ }
201
+
202
+ if (url.pathname === "/api/model-discovery" && req.method === "PUT") {
203
+ let body: { policy?: unknown; provider?: unknown };
204
+ try { body = await readManagementJsonBody(req); } catch (error) { rethrowManagementBodyTooLarge(error); return jsonResponse({ error: "invalid JSON body" }, 400); }
205
+ if (body.policy !== "on" && body.policy !== "off") return jsonResponse({ error: "policy must be on or off" }, 400);
206
+ const provider = typeof body.provider === "string" && body.provider.trim() ? body.provider.trim() : null;
207
+ let baselineBootstrapped = false;
208
+ if (provider) {
209
+ if (!hasOwnProvider(config.providers, provider)) return jsonResponse({ error: "unknown provider" }, 404);
210
+ config.providers[provider].newModelPolicy = body.policy;
211
+ } else {
212
+ const wasAbsent = config.modelDiscovery?.newModelPolicy === undefined;
213
+ config.modelDiscovery ??= {};
214
+ config.modelDiscovery.newModelPolicy = body.policy;
215
+ if (body.policy === "off" && wasAbsent) {
216
+ const models = await fetchAllModels(config);
217
+ const known = config.modelDiscovery.knownModels ??= {};
218
+ const at = new Date().toISOString();
219
+ for (const name of Object.keys(config.providers)) {
220
+ known[name] ??= { ids: [...new Set(models.filter(m => m.provider === name).map(m => m.id))].sort(), removed: [], updatedAt: at };
221
+ }
222
+ baselineBootstrapped = true;
223
+ }
224
+ }
225
+ persistConfig(config);
226
+ const catalogRefresh = await convergeCodexCatalog();
227
+ return jsonResponse({ ok: true, policy: body.policy, provider, ...(baselineBootstrapped ? { baselineBootstrapped } : {}), catalogRefresh });
228
+ }
229
+
230
+ if (url.pathname === "/api/model-discovery/acknowledge" && req.method === "POST") {
231
+ let body: { provider?: unknown; ids?: unknown };
232
+ try { body = await readManagementJsonBody(req); } catch (error) { rethrowManagementBodyTooLarge(error); return jsonResponse({ error: "invalid JSON body" }, 400); }
233
+ const provider = typeof body.provider === "string" ? body.provider.trim() : "";
234
+ if (!provider || !Array.isArray(body.ids) || body.ids.some(id => typeof id !== "string")) {
235
+ return jsonResponse({ error: "provider and string ids are required" }, 400);
236
+ }
237
+ const acknowledged = new Set(body.ids as string[]);
238
+ const recent = config.modelDiscovery?.recentArrivals;
239
+ if (recent?.[provider]) recent[provider] = recent[provider].filter(row => !acknowledged.has(row.id));
240
+ persistConfig(config);
241
+ const catalogRefresh = await convergeCodexCatalog();
242
+ return jsonResponse({ ok: true, provider, acknowledged: [...acknowledged], catalogRefresh });
243
+ }
244
+
245
+ if (url.pathname === "/api/aliases" && req.method === "GET") {
246
+ const providers: Record<string, string> = {};
247
+ const models: Record<string, Record<string, { alias: string; source: "user" | "builtin"; stale?: boolean }>> = {};
248
+ for (const [name, provider] of Object.entries(config.providers)) {
249
+ if (provider.alias) providers[name] = provider.alias;
250
+ const known = knownModelIdsForProvider(name, provider, config);
251
+ const knownSet = new Set(known);
252
+ const rows: Record<string, { alias: string; source: "user" | "builtin"; stale?: boolean }> = {};
253
+ for (const [id, value] of effectiveModelAliases(config, provider, new Set([...known, ...Object.keys(provider.modelAliases ?? {})]))) {
254
+ rows[id] = { ...value, ...(!knownSet.has(id) ? { stale: true } : {}) };
255
+ }
256
+ if (Object.keys(rows).length) models[name] = rows;
257
+ }
258
+ return jsonResponse({ providers, models, defaults: {
259
+ global: config.defaultModelAliases ?? false,
260
+ providers: Object.fromEntries(Object.entries(config.providers).filter(([, p]) => p.defaultAliases !== undefined).map(([n, p]) => [n, p.defaultAliases])),
261
+ } });
262
+ }
263
+
264
+ const providerAliasMatch = url.pathname.match(/^\/api\/providers\/([^/]+)\/alias$/);
265
+ if (providerAliasMatch && req.method === "PUT") {
266
+ const name = decodeURIComponent(providerAliasMatch[1]!);
267
+ // `keys` is not a provider name: `/api/providers/keys/alias` is the API-KEY POOL's rename
268
+ // endpoint (oauth-account-routes.ts), and model routes are dispatched BEFORE it. Without
269
+ // this guard the alias route matched `name = "keys"`, found no such provider, and returned
270
+ // 404 for every key-pool rename.
271
+ if (name === "keys") return null;
272
+ const provider = config.providers[name];
273
+ if (!provider) return jsonResponse({ error: `provider '${name}' not found` }, 404, req, config);
274
+ let raw: unknown;
275
+ try { raw = await readManagementJsonBody(req); } catch (error) { rethrowManagementBodyTooLarge(error); return jsonResponse({ error: "invalid JSON body" }, 400); }
276
+ if (!isPlainRecord(raw) || (raw.alias !== null && typeof raw.alias !== "string")) return jsonResponse({ error: "alias must be a string or null" }, 400, req, config);
277
+ const alias = typeof raw.alias === "string" ? raw.alias.trim() : null;
278
+ if (alias && !isValidProviderName(alias)) return jsonResponse({ error: "invalid provider alias" }, 400, req, config);
279
+ const lower = alias?.toLowerCase();
280
+ const collision = lower && Object.entries(config.providers).find(([other, p]) =>
281
+ other !== name && (other.toLowerCase() === lower || p.alias?.toLowerCase() === lower));
282
+ const comboCollision = lower && Object.entries(config.combos ?? {}).find(([, combo]) => comboPublicModelId("", combo).toLowerCase() === lower);
283
+ const accountCollision = lower && Object.keys(config.codexAccountNamespaces ?? {}).find(value => value.toLowerCase() === lower);
284
+ if (collision || comboCollision || accountCollision) return jsonResponse({ error: `alias conflicts with '${collision?.[0] ?? comboCollision?.[0] ?? accountCollision}'` }, 409, req, config);
285
+ if (alias) provider.alias = alias; else delete provider.alias;
286
+ persistConfig(config);
287
+ const catalogRefresh = await convergeCodexCatalog();
288
+ return jsonResponse({ ok: true, provider: name, alias, catalogRefresh });
289
+ }
290
+
291
+ const modelAliasMatch = url.pathname.match(/^\/api\/providers\/([^/]+)\/model-aliases$/);
292
+ if (modelAliasMatch && req.method === "PUT") {
293
+ const name = decodeURIComponent(modelAliasMatch[1]!);
294
+ if (name === "keys") return null;
295
+ const provider = config.providers[name];
296
+ if (!provider) return jsonResponse({ error: `provider '${name}' not found` }, 404, req, config);
297
+ let raw: unknown;
298
+ try { raw = await readManagementJsonBody(req); } catch (error) { rethrowManagementBodyTooLarge(error); return jsonResponse({ error: "invalid JSON body" }, 400); }
299
+ if (!isPlainRecord(raw) || (raw.set !== undefined && !isPlainRecord(raw.set)) || (raw.remove !== undefined && !Array.isArray(raw.remove))) return jsonResponse({ error: "invalid model alias update" }, 400, req, config);
300
+ const next = { ...(provider.modelAliases ?? {}) };
301
+ for (const id of (raw.remove ?? []) as unknown[]) if (typeof id === "string") delete next[id];
302
+ const conflicts: Array<{ alias: string; heldBy: string }> = [];
303
+ const known = knownModelIdsForProvider(name, provider, config);
304
+ for (const [id, value] of Object.entries((raw.set ?? {}) as Record<string, unknown>)) {
305
+ if (typeof value !== "string" || !MODEL_ALIAS_PATTERN.test(value)) return jsonResponse({ error: `invalid model alias for '${id}'` }, 400, req, config);
306
+ const lower = value.toLowerCase();
307
+ const heldBy = Object.entries(next).find(([other, alias]) => other !== id && alias.toLowerCase() === lower)?.[0]
308
+ ?? known.find(native => native.toLowerCase() === lower)
309
+ ?? Object.entries(config.combos ?? {}).find(([, combo]) => comboPublicModelId("", combo).toLowerCase() === lower)?.[0];
310
+ if (heldBy || /^(?:gpt-|o1-|o3-|o4-|codex-)/i.test(value)) conflicts.push({ alias: value, heldBy: heldBy ?? "native OpenAI family" });
311
+ else next[id] = value;
312
+ }
313
+ if (conflicts.length) return jsonResponse({ error: "model alias collision", conflicts }, 409, req, config);
314
+ provider.modelAliases = next;
315
+ persistConfig(config);
316
+ const catalogRefresh = await convergeCodexCatalog();
317
+ return jsonResponse({ ok: true, aliases: next, catalogRefresh });
318
+ }
319
+
320
+ if (url.pathname === "/api/default-aliases" && req.method === "PUT") {
321
+ let raw: unknown;
322
+ try { raw = await readManagementJsonBody(req); } catch (error) { rethrowManagementBodyTooLarge(error); return jsonResponse({ error: "invalid JSON body" }, 400); }
323
+ if (!isPlainRecord(raw) || typeof raw.enabled !== "boolean" || (raw.provider !== undefined && typeof raw.provider !== "string")) return jsonResponse({ error: "enabled must be boolean" }, 400, req, config);
324
+ if (typeof raw.provider === "string") {
325
+ const provider = config.providers[raw.provider];
326
+ if (!provider) return jsonResponse({ error: `provider '${raw.provider}' not found` }, 404, req, config);
327
+ provider.defaultAliases = raw.enabled;
328
+ } else config.defaultModelAliases = raw.enabled;
329
+ persistConfig(config);
330
+ const catalogRefresh = await convergeCodexCatalog();
331
+ return jsonResponse({ ok: true, catalogRefresh });
332
+ }
333
+
218
334
  if (url.pathname === "/api/catalog" && req.method === "GET") {
219
335
  const { readCatalog, readCodexCatalogPath } = await import("../../codex/catalog");
220
336
  const catalog = readCatalog(readCodexCatalogPath());
@@ -249,9 +365,33 @@ export async function handleModelRoutes(ctx: ManagementContext): Promise<Respons
249
365
  );
250
366
  }
251
367
  const spec = EXPORT_CLIENTS[requested];
252
- let rows: ManagementModelRow[];
368
+ // Resolved before the catalog load on purpose. A refused override is a
369
+ // property of the request, not of the catalog: validating it afterwards
370
+ // let a busy or failing catalog answer 503 first, so a user with a
371
+ // relative override never saw the message that says how to fix it — and
372
+ // the route did the enumeration work anyway for input it was going to
373
+ // reject.
374
+ let destination: string;
253
375
  try {
254
- rows = await listManagementModelRows(config);
376
+ destination = spec.destination(process.env);
377
+ } catch (error) {
378
+ // A client's own environment override can name a path the resolver
379
+ // refuses — a relative value, which this process and the client would
380
+ // resolve against different working directories. That is a
381
+ // user-correctable configuration error, not a server fault, so it leaves
382
+ // this boundary as a bounded 400 instead of escaping handleManagementAPI
383
+ // as a generic 500 and stripping the message that says how to fix it.
384
+ // `integrations/state.ts` and `integrations/writer.ts` already catch the
385
+ // same error on their paths; this route was the one that did not.
386
+ if (!(error instanceof ClientPathError)) throw error;
387
+ return jsonResponse({ error: error.message }, 400, req, config);
388
+ }
389
+ let models: ExportModel[];
390
+ try {
391
+ // The ONE loader every export surface uses. It carries the visibility
392
+ // filter with it, so this route and the integration routes cannot end up
393
+ // telling a client about different models.
394
+ models = await loadExportModels(config);
255
395
  } catch (error) {
256
396
  // A partial or empty `models` block reads as a valid config while offering nothing,
257
397
  // so a catalog failure is surfaced as unavailable rather than serialized. The
@@ -264,21 +404,24 @@ export async function handleModelRoutes(ctx: ManagementContext): Promise<Respons
264
404
  config,
265
405
  );
266
406
  }
267
- // The export core does not filter visibility — it serializes what it is given. A model the
268
- // user disabled in the Models tab is absent from /v1/models, so exporting it would hand the
269
- // client a selector the proxy refuses to route.
270
- const models = rows.filter(row => !row.disabled).map(toExportModel);
271
- const document = buildClientConfig(requested, {
407
+ const built = buildClientConfigText(requested, {
272
408
  baseUrl: opencodeProxyBaseUrl(Number(url.port) || config.port, config.hostname),
273
409
  models,
274
410
  config,
275
411
  });
412
+ const document = built.document;
276
413
  return jsonResponse({
277
414
  client: spec.id,
278
415
  filename: spec.filename,
279
- destination: spec.destination(process.env),
416
+ destination,
280
417
  apiKeyEnv: spec.apiKeyEnv,
281
418
  exportHint: spec.exportHint,
419
+ // The client's own format and the exact bytes for it. The GUI previously
420
+ // re-serialized `config` as JSON, which is wrong for four of the six
421
+ // clients; `mediaType` also drives the download blob.
422
+ format: built.format,
423
+ mediaType: built.mediaType,
424
+ text: built.text,
282
425
  ...summarizeExportedModels(requested, document),
283
426
  config: document,
284
427
  }, 200, req, config);
@@ -292,8 +435,8 @@ export async function handleModelRoutes(ctx: ManagementContext): Promise<Respons
292
435
  const disabled = Array.isArray(body.models) ? body.models.filter((m): m is string => typeof m === "string") : [];
293
436
  config.disabledModels = disabled;
294
437
  persistConfig(config);
295
- await refreshCodexCatalogBestEffort();
296
- return jsonResponse({ ok: true, disabled });
438
+ const catalogRefresh = await convergeCodexCatalog();
439
+ return jsonResponse({ ok: true, disabled, catalogRefresh });
297
440
  }
298
441
 
299
442
  // One user-facing visibility switch spans two persisted filters: a provider allowlist and the
@@ -315,7 +458,14 @@ export async function handleModelRoutes(ctx: ManagementContext): Promise<Respons
315
458
  if (!providerConfig && provider !== "openai" && !isVirtualComboNamespace) {
316
459
  return jsonResponse({ error: "unknown model visibility provider" }, 400);
317
460
  }
318
- const supportedNative = new Set(nativeModelRows(config).map(row => row.slug));
461
+ const accountNativeQualified = shouldIncludeAccountBoundNativeOpenAi(config)
462
+ ? [...accountBoundNativeOpenAiSlugsBySelector(config).entries()].flatMap(([selector, slugs]) =>
463
+ slugs.filter(slug => !nativeModelRows(config).some(row => row.slug === slug)).map(slug => `${selector}/${slug}`))
464
+ : [];
465
+ const supportedNative = new Set([
466
+ ...nativeModelRows(config).map(row => row.slug),
467
+ ...accountNativeQualified,
468
+ ]);
319
469
  const targets: Array<{ id: string; native: boolean }> = [];
320
470
  const seen = new Set<string>();
321
471
  for (const value of body.targets) {
@@ -336,17 +486,16 @@ export async function handleModelRoutes(ctx: ManagementContext): Promise<Respons
336
486
  if (targets.length === 0) return jsonResponse({ error: "model visibility targets required" }, 400);
337
487
 
338
488
  const knownComboSelectors = new Set(
339
- Object.entries(config.combos ?? {}).flatMap(([id, combo]) => [
340
- comboModelId(id),
341
- comboPublicModelId(id, combo),
342
- ]),
489
+ Object.entries(config.combos ?? {}).flatMap(([id, combo]) => (
490
+ comboDisabledModelSelectors(id, combo)
491
+ )),
343
492
  );
344
493
  const targetComboSelectors = new Map<string, Set<string>>();
345
494
  if (isVirtualComboNamespace) {
346
495
  for (const target of targets) {
347
496
  const combo = config.combos && Object.hasOwn(config.combos, target.id) ? config.combos[target.id] : undefined;
348
497
  if (!combo) return jsonResponse({ error: "invalid model visibility target" }, 400);
349
- targetComboSelectors.set(target.id, new Set([comboModelId(target.id), comboPublicModelId(target.id, combo)]));
498
+ targetComboSelectors.set(target.id, new Set(comboDisabledModelSelectors(target.id, combo)));
350
499
  }
351
500
  }
352
501
  const matchesTarget = (stored: string, target: { id: string; native: boolean }) => target.native
@@ -365,9 +514,14 @@ export async function handleModelRoutes(ctx: ManagementContext): Promise<Respons
365
514
  const nativeIds = provider === "openai"
366
515
  ? disabledNativeSlugs({ disabledModels: disabled })
367
516
  : new Set<string>();
517
+ const accountNativeIds = provider === "openai" ? new Set(accountNativeQualified) : new Set<string>();
518
+ const nativeAliasSlugs = provider === "openai"
519
+ ? configuredNativeAliasSlugs(config)
520
+ : new Set<string>();
368
521
  disabled = disabled.filter(stored => (
369
522
  knownComboSelectors.has(stored)
370
- || (!stored.startsWith(`${provider}/`) && !nativeIds.has(stored))
523
+ || nativeAliasSlugs.has(stored)
524
+ || (!stored.startsWith(`${provider}/`) && !nativeIds.has(stored) && !accountNativeIds.has(stored))
371
525
  ));
372
526
  }
373
527
  } else {
@@ -376,6 +530,10 @@ export async function handleModelRoutes(ctx: ManagementContext): Promise<Respons
376
530
  providerConfig.selectedModels = [...new Set([...providerConfig.selectedModels, ...additions])];
377
531
  }
378
532
  disabled = disabled.filter(stored => !targets.some(target => matchesTarget(stored, target)));
533
+ const arrivals = config.modelDiscovery?.recentArrivals?.[provider];
534
+ if (arrivals) config.modelDiscovery!.recentArrivals![provider] = arrivals.filter(row => (
535
+ !targets.some(target => !target.native && target.id === row.id)
536
+ ));
379
537
  }
380
538
  } else {
381
539
  for (const target of targets) {
@@ -391,8 +549,8 @@ export async function handleModelRoutes(ctx: ManagementContext): Promise<Respons
391
549
 
392
550
  config.disabledModels = disabled;
393
551
  persistConfig(config);
394
- await refreshCodexCatalogBestEffort();
395
- return jsonResponse({ ok: true, scope, provider, enabled: body.enabled, disabled });
552
+ const catalogRefresh = await convergeCodexCatalog();
553
+ return jsonResponse({ ok: true, scope, provider, enabled: body.enabled, disabled, catalogRefresh });
396
554
  }
397
555
 
398
556
  if (url.pathname === "/api/custom-models" && req.method === "GET") {
@@ -400,12 +558,13 @@ export async function handleModelRoutes(ctx: ManagementContext): Promise<Respons
400
558
  }
401
559
 
402
560
  if (url.pathname === "/api/custom-models" && req.method === "POST") {
403
- let body: { provider?: unknown; modelId?: unknown; displayName?: unknown; contextWindow?: unknown; inputModalities?: unknown };
404
- try { body = await readManagementJsonBody(req); } catch (error) { rethrowManagementBodyTooLarge(error); return jsonResponse({ error: "invalid JSON body" }, 400); }
561
+ let parsedBody: unknown;
562
+ try { parsedBody = await readManagementJsonBody(req); } catch (error) { rethrowManagementBodyTooLarge(error); return jsonResponse({ error: "invalid JSON body" }, 400); }
563
+ if (!isPlainRecord(parsedBody)) return jsonResponse({ error: "invalid JSON body" }, 400);
564
+ const body = parsedBody;
405
565
  const provider = typeof body.provider === "string" ? body.provider.trim() : "";
406
566
  const modelId = typeof body.modelId === "string" ? body.modelId.trim() : "";
407
567
  if (!provider || !modelId) return jsonResponse({ error: "provider and modelId are required" }, 400);
408
- if (modelId.includes("/")) return jsonResponse({ error: "modelId must not contain /" }, 400);
409
568
  if (!isValidProviderName(provider)) return jsonResponse({ error: "invalid provider name" }, 400);
410
569
  if (!hasOwnProvider(config.providers, provider)) return jsonResponse({ error: "provider not configured" }, 404);
411
570
  const displayName = typeof body.displayName === "string" && body.displayName.trim() ? body.displayName.trim() : undefined;
@@ -414,11 +573,19 @@ export async function handleModelRoutes(ctx: ManagementContext): Promise<Respons
414
573
  const modalities = readInputModalities(body.inputModalities);
415
574
  if (modalities.error) return jsonResponse({ error: modalities.error }, 400);
416
575
  const inputModalities = modalities.values;
576
+ const reasoning = readReasoningEfforts(body.reasoningEfforts);
577
+ if (reasoning.error) return jsonResponse({ error: reasoning.error }, 400);
578
+ const defaultEffort = readDefaultReasoningEffort(body.defaultReasoningEffort, reasoning.values);
579
+ if (defaultEffort.error) return jsonResponse({ error: defaultEffort.error }, 400);
417
580
  const existing = config.customModels ?? [];
418
581
  const newSlug = routedSlug(provider, modelId);
419
582
  if (existing.some(cm => routedSlug(cm.provider, cm.modelId) === newSlug)) {
420
583
  return jsonResponse({ error: "duplicate model" }, 409);
421
584
  }
585
+ const known = knownModelIdsForProvider(provider, config.providers[provider], config);
586
+ if (encodedModelIdCollides(modelId, known)) {
587
+ return jsonResponse({ error: "ambiguous model id" }, 409);
588
+ }
422
589
  const entry: OcxCustomModel = {
423
590
  id: randomUUID(),
424
591
  provider,
@@ -426,26 +593,29 @@ export async function handleModelRoutes(ctx: ManagementContext): Promise<Respons
426
593
  ...(displayName ? { displayName } : {}),
427
594
  ...(contextWindow ? { contextWindow } : {}),
428
595
  ...(inputModalities && inputModalities.length > 0 ? { inputModalities } : {}),
596
+ ...(reasoning.values !== undefined ? { reasoningEfforts: reasoning.values } : {}),
597
+ ...(defaultEffort.value ? { defaultReasoningEffort: defaultEffort.value } : {}),
429
598
  addedAt: new Date().toISOString(),
430
599
  };
431
600
  config.customModels = [...existing, entry];
432
601
  persistConfig(config);
433
- await refreshCodexCatalogBestEffort();
434
- return jsonResponse(entry, 201);
602
+ const catalogRefresh = await convergeCodexCatalog();
603
+ return jsonResponse({ ...entry, catalogRefresh }, 201);
435
604
  }
436
605
 
437
606
  const customPutMatch = url.pathname.match(/^\/api\/custom-models\/([^/]+)$/);
438
607
  if (customPutMatch && req.method === "PUT") {
439
608
  let id: string;
440
609
  try { id = decodeURIComponent(customPutMatch[1]); } catch { return jsonResponse({ error: "invalid id encoding" }, 400); }
441
- let body: { displayName?: unknown; contextWindow?: unknown; inputModalities?: unknown; modelId?: unknown };
442
- try { body = await readManagementJsonBody(req); } catch (error) { rethrowManagementBodyTooLarge(error); return jsonResponse({ error: "invalid JSON body" }, 400); }
610
+ let parsedBody: unknown;
611
+ try { parsedBody = await readManagementJsonBody(req); } catch (error) { rethrowManagementBodyTooLarge(error); return jsonResponse({ error: "invalid JSON body" }, 400); }
612
+ if (!isPlainRecord(parsedBody)) return jsonResponse({ error: "invalid JSON body" }, 400);
613
+ const body = parsedBody;
443
614
  const list = config.customModels ?? [];
444
615
  const idx = list.findIndex(cm => cm.id === id);
445
616
  if (idx === -1) return jsonResponse({ error: "not found" }, 404);
446
617
  const cm = { ...list[idx] };
447
618
  if (typeof body.modelId === "string" && body.modelId.trim()) {
448
- if (body.modelId.includes("/")) return jsonResponse({ error: "modelId must not contain /" }, 400);
449
619
  cm.modelId = body.modelId.trim();
450
620
  }
451
621
  if (body.displayName !== undefined) {
@@ -461,15 +631,48 @@ export async function handleModelRoutes(ctx: ManagementContext): Promise<Respons
461
631
  if (edited.error) return jsonResponse({ error: edited.error }, 400);
462
632
  cm.inputModalities = edited.values && edited.values.length > 0 ? edited.values : undefined;
463
633
  }
634
+ // `null` clears the stored ladder back to "inherit from the provider row"; `[]` stays
635
+ // stored as an explicit "no reasoning" override. The default effort rides along and is
636
+ // validated against the ladder the row ends up with.
637
+ if (body.reasoningEfforts !== undefined) {
638
+ if (body.reasoningEfforts === null) {
639
+ cm.reasoningEfforts = undefined;
640
+ } else {
641
+ const edited = readReasoningEfforts(body.reasoningEfforts);
642
+ if (edited.error) return jsonResponse({ error: edited.error }, 400);
643
+ cm.reasoningEfforts = edited.values;
644
+ }
645
+ }
646
+ if (body.defaultReasoningEffort !== undefined) {
647
+ const edited = readDefaultReasoningEffort(body.defaultReasoningEffort, cm.reasoningEfforts);
648
+ if (edited.error) return jsonResponse({ error: edited.error }, 400);
649
+ cm.defaultReasoningEffort = edited.value;
650
+ }
651
+ // Mirror of the POST invariant: a default only survives as a member of the final ladder.
652
+ // Without this, a ladder shrink/clear on a row that was created with a default leaves a
653
+ // stale default that re-applies itself onto the inherited ladder in the generated catalog
654
+ // (the GUI toggle-off path sends only reasoningEfforts, never the default).
655
+ if (cm.defaultReasoningEffort !== undefined) {
656
+ const ladder = cm.reasoningEfforts;
657
+ if (!ladder || ladder.length === 0 || !ladder.includes(cm.defaultReasoningEffort)) {
658
+ cm.defaultReasoningEffort = undefined;
659
+ }
660
+ }
464
661
  const updatedSlug = routedSlug(cm.provider, cm.modelId);
465
662
  if (list.some((other, i) => i !== idx && routedSlug(other.provider, other.modelId) === updatedSlug)) {
466
663
  return jsonResponse({ error: "duplicate model" }, 409);
467
664
  }
665
+ const known = knownModelIdsForProvider(cm.provider, config.providers[cm.provider], {
666
+ customModels: list.filter((_, i) => i !== idx),
667
+ });
668
+ if (encodedModelIdCollides(cm.modelId, known)) {
669
+ return jsonResponse({ error: "ambiguous model id" }, 409);
670
+ }
468
671
  list[idx] = cm;
469
672
  config.customModels = list;
470
673
  persistConfig(config);
471
- await refreshCodexCatalogBestEffort();
472
- return jsonResponse(cm);
674
+ const catalogRefresh = await convergeCodexCatalog();
675
+ return jsonResponse({ ...cm, catalogRefresh });
473
676
  }
474
677
 
475
678
  const customDelMatch = url.pathname.match(/^\/api\/custom-models\/([^/]+)$/);
@@ -482,15 +685,24 @@ export async function handleModelRoutes(ctx: ManagementContext): Promise<Respons
482
685
  list.splice(idx, 1);
483
686
  config.customModels = list.length > 0 ? list : undefined;
484
687
  persistConfig(config);
485
- await refreshCodexCatalogBestEffort();
486
- return jsonResponse({ ok: true });
688
+ const catalogRefresh = await convergeCodexCatalog();
689
+ return jsonResponse({ ok: true, catalogRefresh });
487
690
  }
488
691
 
489
692
  // Per-provider catalog allowlist (issue #52): when a provider has a non-empty selectedModels list,
490
693
  // only those ids ship to Codex's catalog / /v1/models. GET returns the CURRENT selection plus the
491
- // FULL available set per provider (unfiltered the picker needs everything to choose from).
694
+ // FULL available set per provider for the management picker.
695
+ //
696
+ // `fetchAllModels` hides media-generation ids (image/video) from the Codex-facing catalog, but the
697
+ // Providers → Models tab must still list every id the user fetched and saved on the provider
698
+ // (e.g. grok2api's grok-imagine-*). Merge `providers[name].models` on top so those rows are not
699
+ // silently dropped after a successful refresh-models persist.
700
+ //
701
+ // Cold path: prefer cache/configured seeds so the Providers page paints in milliseconds instead
702
+ // of waiting on every upstream `/models` (often multi-second). When any live provider still needs
703
+ // a network probe, kick a background gather and set `refreshing: true` so the GUI can re-poll.
492
704
  if (url.pathname === "/api/selected-models" && req.method === "GET") {
493
- const models = await fetchAllModels(config);
705
+ const models = await fetchAllModels(config, { preferCached: true });
494
706
  const available: Record<string, string[]> = {};
495
707
  for (const m of models) (available[m.provider] ??= []).push(m.id);
496
708
  const selected: Record<string, string[]> = {};
@@ -501,8 +713,120 @@ export async function handleModelRoutes(ctx: ManagementContext): Promise<Respons
501
713
  if (Array.isArray(prov.selectedModels) && prov.selectedModels.length > 0) selected[name] = [...prov.selectedModels];
502
714
  const liveCount = getProviderLiveModelCount(name);
503
715
  if (liveCount !== undefined) liveModelCounts[name] = liveCount;
716
+ const configured = prov.models;
717
+ if (!Array.isArray(configured) || configured.length === 0) continue;
718
+ const bucket = available[name] ?? (available[name] = []);
719
+ const seen = new Set(bucket);
720
+ for (const id of configured) {
721
+ if (typeof id !== "string" || !id || seen.has(id)) continue;
722
+ seen.add(id);
723
+ bucket.push(id);
724
+ }
725
+ }
726
+ // Re-check after the local gather: preferCached may have kicked a background flight whose
727
+ // first probe already filled some caches, but `refreshing` still means "more work in flight
728
+ // or still needed" for the GUI re-poll loop.
729
+ const { catalogGatherNeedsLiveRefresh } = await import("../../codex/catalog");
730
+ const refreshing = catalogGatherNeedsLiveRefresh(config);
731
+ return jsonResponse({ selected, available, liveModelCounts, refreshing });
732
+ }
733
+ if (url.pathname === "/api/model-presets" && req.method === "GET") {
734
+ // Preview without applying: rules evaluated against the CURRENT catalog, so the count the
735
+ // user sees is the count they would get.
736
+ const models = await fetchAllModels(config);
737
+ const byProvider = new Map<string, string[]>();
738
+ for (const m of models) {
739
+ const ids = byProvider.get(m.provider) ?? [];
740
+ ids.push(m.id);
741
+ byProvider.set(m.provider, ids);
742
+ }
743
+ const providers: Record<string, unknown> = {};
744
+ for (const [name, prov] of Object.entries(config.providers)) {
745
+ const preset = modelPresetFor(name);
746
+ if (!preset) continue;
747
+ const catalogIds = byProvider.get(name) ?? [];
748
+ const presetIds = materializeModelPreset(name, catalogIds);
749
+ providers[name] = {
750
+ mode: prov.modelPreset?.mode ?? "all",
751
+ ...(prov.modelPreset?.appliedVersion !== undefined
752
+ ? { appliedVersion: prov.modelPreset.appliedVersion }
753
+ : {}),
754
+ availableVersion: preset.version,
755
+ presetIds,
756
+ presetCount: presetIds.length,
757
+ totalCount: catalogIds.length,
758
+ ...(prov.modelPreset?.fallback ? { fallback: prov.modelPreset.fallback } : {}),
759
+ };
504
760
  }
505
- return jsonResponse({ selected, available, liveModelCounts });
761
+ return jsonResponse({ providers });
762
+ }
763
+ if (url.pathname === "/api/model-presets" && req.method === "PUT") {
764
+ let body: { provider?: unknown; mode?: unknown };
765
+ try { body = await readManagementJsonBody(req); } catch (error) { rethrowManagementBodyTooLarge(error); return jsonResponse({ error: "invalid JSON body" }, 400); }
766
+ const provider = typeof body.provider === "string" ? body.provider : "";
767
+ if (!provider || !hasOwnProvider(config.providers, provider)) {
768
+ return jsonResponse({ error: "unknown provider" }, provider ? 404 : 400);
769
+ }
770
+ const mode = body.mode;
771
+ if (mode !== "preset" && mode !== "all" && mode !== "custom") {
772
+ return jsonResponse({ error: "mode must be preset, all, or custom" }, 400);
773
+ }
774
+ const target = config.providers[provider];
775
+ if (mode === "all") {
776
+ // Same effect as today's empty-list PUT: no allowlist, no marker to reconcile.
777
+ delete target.selectedModels;
778
+ delete target.modelPreset;
779
+ persistConfig(config);
780
+ return jsonResponse({ ok: true, provider, mode, selected: [], catalogRefresh: await convergeCodexCatalog() });
781
+ }
782
+ if (mode === "custom") {
783
+ // Keep whatever is selected; only the marker changes, so a user can pin their edits
784
+ // without the proxy re-materializing over them.
785
+ target.modelPreset = { ...(target.modelPreset ?? {}), mode: "custom" };
786
+ persistConfig(config);
787
+ return jsonResponse({ ok: true, provider, mode, selected: [...(target.selectedModels ?? [])] });
788
+ }
789
+ if (!hasModelPreset(provider)) {
790
+ return jsonResponse({ error: `no model preset is shipped for provider '${provider}'` }, 400);
791
+ }
792
+ const models = await fetchAllModels(config);
793
+ const catalogIds = models.filter(m => m.provider === provider).map(m => m.id);
794
+ const presetIds = materializeModelPreset(provider, catalogIds);
795
+ const preset = modelPresetFor(provider)!;
796
+ if (presetIds.length === 0) {
797
+ // NEVER write an empty allowlist from a preset: empty means ALL, so it would silently
798
+ // un-curate instead of curating. Keep the previous selection and record the fallback so
799
+ // the next convergence can retry.
800
+ target.modelPreset = {
801
+ mode: "all",
802
+ appliedVersion: preset.version,
803
+ appliedAt: new Date().toISOString(),
804
+ fallback: "preset-empty",
805
+ };
806
+ persistConfig(config);
807
+ return jsonResponse({
808
+ ok: true,
809
+ provider,
810
+ mode: "all",
811
+ fallback: "preset-empty",
812
+ selected: [...(target.selectedModels ?? [])],
813
+ });
814
+ }
815
+ target.selectedModels = presetIds;
816
+ target.modelPreset = {
817
+ mode: "preset",
818
+ appliedVersion: preset.version,
819
+ appliedAt: new Date().toISOString(),
820
+ };
821
+ persistConfig(config);
822
+ return jsonResponse({
823
+ ok: true,
824
+ provider,
825
+ mode: "preset",
826
+ appliedVersion: preset.version,
827
+ selected: presetIds,
828
+ catalogRefresh: await convergeCodexCatalog(),
829
+ });
506
830
  }
507
831
  if (url.pathname === "/api/selected-models" && req.method === "PUT") {
508
832
  let body: { provider?: unknown; models?: unknown };
@@ -517,9 +841,13 @@ export async function handleModelRoutes(ctx: ManagementContext): Promise<Respons
517
841
  // Empty list clears the allowlist (provider reverts to exposing all models).
518
842
  if (models.length > 0) config.providers[provider].selectedModels = models;
519
843
  else delete config.providers[provider].selectedModels;
844
+ // Divergence is detected at the WRITE path, not by diffing (#2465): a user edit while the
845
+ // provider is in preset mode makes the selection theirs, and the proxy must never
846
+ // re-materialize over it afterwards.
847
+ markModelPresetDiverged(config.providers[provider]);
520
848
  persistConfig(config);
521
- await refreshCodexCatalogBestEffort();
522
- return jsonResponse({ ok: true, provider, selected: models });
849
+ const catalogRefresh = await convergeCodexCatalog();
850
+ return jsonResponse({ ok: true, provider, selected: models, catalogRefresh });
523
851
  }
524
852
  return null;
525
853
  }