@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
@@ -2,15 +2,23 @@ import { randomUUID } from "node:crypto";
2
2
  import { readFileSync } from "node:fs";
3
3
  import type { CatalogModel } from "../../codex/catalog";
4
4
  import { catalogModelSlug, invalidateCodexModelsCache, nativeModelRows, uniqueCatalogModelsForPublicList } from "../../codex/catalog";
5
+ import { clearGatherRoutedModelsInflight, fetchProviderModels } from "../../codex/catalog/provider-fetch";
5
6
  import {
6
7
  DEFAULT_SUBAGENT_MODELS,
8
+ adoptPersistedProviderIntoLiveConfig,
7
9
  codexAutoStartEnabled,
8
10
  hasOwnProvider,
9
11
  isValidProviderName,
10
12
  multiAgentGuidanceEnabled,
13
+ nonBlankStringArrayConfigError,
14
+ normalizeNonBlankStringArray,
11
15
  providerBaseUrlConfigError,
12
16
  providerHeadersConfigError,
17
+ requestPacingConfigError,
18
+ readConfigAdmissionSnapshot,
13
19
  saveConfigPreservingClaudeCode,
20
+ upstreamHttpVersionConfigError,
21
+ withConfigMutationLockSync,
14
22
  } from "../../config";
15
23
  import {
16
24
  clearLoginState,
@@ -21,13 +29,15 @@ import {
21
29
  submitManualLoginCode,
22
30
  upsertOAuthProvider,
23
31
  } from "../../oauth";
24
- import { removeCredential } from "../../oauth/store";
32
+ import { replaceProviderAccountSet } from "../../oauth/store";
25
33
  import { providerDestinationResolvedError } from "../../lib/destination-policy";
26
34
  import { reconcileLiveStateStores } from "../../lib/state-store-registrations";
27
- import { ProviderOutboundPolicyError, providerOutboundGet, providerRedirectError } from "../../lib/provider-outbound";
35
+ import { ProviderOutboundPolicyError, providerOutboundGet, providerOutboundPost, providerRedirectError } from "../../lib/provider-outbound";
36
+ import { fetchCursorUsableModels } from "../../adapters/cursor/live-models";
37
+ import { parseAntigravityAvailableModels } from "../../providers/antigravity-models";
28
38
  import { enrichProviderFromCatalog, listKeyLoginProviders } from "../../oauth/key-providers";
29
- import { deriveProviderPresets } from "../../providers/derive";
30
- import { providerCodexAccountMode } from "../../providers/registry";
39
+ import { deriveProviderPresets, enrichProviderFromRegistry, providerConfigSeed } from "../../providers/derive";
40
+ import { effectiveGoogleMode, providerCodexAccountMode, providerMatchesRegistryTransport } from "../../providers/registry";
31
41
  import {
32
42
  extractModelEnvelopeRows,
33
43
  extractProviderModelItems,
@@ -35,13 +45,21 @@ import {
35
45
  resolveProviderModelDiscovery,
36
46
  } from "../../providers/model-discovery";
37
47
  import { routedSlug, slugEquals } from "../../providers/slug-codec";
38
- import { clearProviderQuotaCache, fetchProviderQuotaReports } from "../../providers/quota";
48
+ import { clearAccountQuotaCache, clearProviderQuotaCache, fetchProviderQuotaReports } from "../../providers/quota";
49
+ import { clearKeyCooldowns } from "../../providers/key-failover";
50
+ import { providerRequestPacingStatus } from "../../providers/request-pacing";
39
51
  import { CODEX_FORWARD_BASE_URL, isCanonicalOpenAiForwardProvider } from "../../providers/openai-tiers";
40
52
  import { codexAccountNamespaceProviderCollisionError } from "../../codex/account-namespace-match";
41
53
  import { clearThreadAccountMap } from "../../codex/routing";
42
54
  import { primeCodexPoolQuotas } from "../../codex/auth-api";
43
- import { getProviderDiscoveryStatus } from "../../codex/model-cache";
55
+ import {
56
+ clearModelCache,
57
+ DEFAULT_MODEL_CACHE_TTL_MS,
58
+ getProviderDiscoveryStatus,
59
+ getProviderLiveModelCount,
60
+ } from "../../codex/model-cache";
44
61
  import { DEFAULT_PROVIDER_CONTEXT_CAP, globalContextCapValue, providerContextCap, providerContextCaps, setAllProviderContextCaps, setGlobalContextCapValue, setProviderContextCap } from "../../providers/context-cap";
62
+ import { modelAutoCompactTokenLimitsConfigError } from "../../providers/auto-compact-budget";
45
63
  import { resolveCodexHomeDir } from "../../codex/home";
46
64
  import { readUsageEntries } from "../../usage/log";
47
65
  import { getUsageDebugLogEntries } from "../../usage/debug";
@@ -63,36 +81,466 @@ import { filterRequestLogs, getRequestLogEntries, type RequestLogEntry } from ".
63
81
  import { estimateComboCost, estimateRequestCost, normalizeCostTokens, tokensPerSecond } from "../../usage/cost";
64
82
  import type { PersistedUsageAttempt } from "../../usage/log";
65
83
  import { isAllowedRequestOrigin, jsonResponse, providerManagementConfigError, publicProviderBaseUrl, safeConfigDTO } from "../auth-cors";
84
+ import { providerServiceTierConfigError } from "./provider-capability-config";
66
85
  import { applySystemEnvToggle } from "../system-env";
86
+ import {
87
+ LOCAL_PROVIDER_RELOAD_NAME_HEADER,
88
+ LOCAL_PROVIDER_RELOAD_PATH,
89
+ } from "../../lib/local-provider-reload-contract";
90
+ import { refreshUserCostOverlays } from "../../usage/user-cost-overlays";
91
+ import {
92
+ XAI_RESPONSES_OPT_IN_MODELS,
93
+ xaiResponsesOptInState,
94
+ } from "../../providers/xai-responses-opt-in";
67
95
 
68
96
  import { isPlainRecord, parseDebugLogQuery, tokPerSecondResult, unavailableCostReason, costResult, requestLogDto, stripRegistryOnlyStaticHeaders, fetchAllModels } from "./shared";
69
97
  import type { MetricUnavailableReason, TokPerSecondResult, CostEstimateReason, CostResult, MetricSource } from "./shared";
70
98
  import type { ManagementContext } from "./context";
71
99
  import { readManagementJsonBody, rethrowManagementBodyTooLarge } from "./body";
72
100
 
101
+ type ProviderPatchApplication =
102
+ | { error: string }
103
+ | {
104
+ next: OcxProviderConfig;
105
+ touched: boolean;
106
+ editorTouched: boolean;
107
+ enablingOpenAi: boolean;
108
+ headersTouched: boolean;
109
+ };
110
+
111
+ /**
112
+ * Apply the recognized PATCH field mask onto a provider copy. The caller runs this once
113
+ * for validation and again inside the config mutation lock against the newest provider,
114
+ * so a concurrent PATCH cannot be erased by a save of a stale snapshot. Only synchronous
115
+ * checks live here; the async destination probe stays in the route.
116
+ */
117
+ function applyProviderPatchFields(
118
+ name: string,
119
+ provider: OcxProviderConfig,
120
+ rawBody: Record<string, unknown>,
121
+ keys: string[],
122
+ config: OcxConfig,
123
+ ): ProviderPatchApplication {
124
+ const next: OcxProviderConfig = { ...provider };
125
+ let touched = false;
126
+ let headersTouched = false;
127
+
128
+ if (Object.hasOwn(rawBody, "disabled")) {
129
+ if (typeof rawBody.disabled !== "boolean") return { error: "disabled must be a boolean" };
130
+ if (rawBody.disabled && name === config.defaultProvider) {
131
+ return { error: "cannot disable the default provider; set another default first" };
132
+ }
133
+ next.disabled = rawBody.disabled;
134
+ touched = true;
135
+ }
136
+ if (Object.hasOwn(rawBody, "adapter")) {
137
+ if (typeof rawBody.adapter !== "string" || !rawBody.adapter.trim()) return { error: "adapter must be a non-empty string" };
138
+ next.adapter = rawBody.adapter.trim();
139
+ touched = true;
140
+ }
141
+ if (Object.hasOwn(rawBody, "baseUrl")) {
142
+ if (typeof rawBody.baseUrl !== "string" || !rawBody.baseUrl.trim()) return { error: "baseUrl must be a non-empty string" };
143
+ next.baseUrl = rawBody.baseUrl.trim();
144
+ touched = true;
145
+ }
146
+ if (Object.hasOwn(rawBody, "defaultModel")) {
147
+ if (typeof rawBody.defaultModel !== "string") return { error: "defaultModel must be a string" };
148
+ const dm = rawBody.defaultModel.trim();
149
+ if (dm) next.defaultModel = dm;
150
+ else delete next.defaultModel;
151
+ touched = true;
152
+ }
153
+ if (Object.hasOwn(rawBody, "authMode")) {
154
+ if (typeof rawBody.authMode !== "string") return { error: "authMode must be a string" };
155
+ const mode = rawBody.authMode.trim();
156
+ if (mode === "key" || mode === "forward" || mode === "oauth" || mode === "local") {
157
+ next.authMode = mode;
158
+ touched = true;
159
+ } else if (mode === "") {
160
+ delete next.authMode;
161
+ touched = true;
162
+ } else {
163
+ return { error: "authMode must be key, forward, oauth, or local" };
164
+ }
165
+ }
166
+ if (Object.hasOwn(rawBody, "apiKeyTransport")) {
167
+ const transport = rawBody.apiKeyTransport;
168
+ if (transport === "x-api-key" || transport === "bearer") {
169
+ next.apiKeyTransport = transport;
170
+ touched = true;
171
+ } else if (transport === "") {
172
+ delete next.apiKeyTransport;
173
+ touched = true;
174
+ } else {
175
+ return { error: "apiKeyTransport must be x-api-key, bearer, or empty to clear" };
176
+ }
177
+ }
178
+ if (Object.hasOwn(rawBody, "note")) {
179
+ if (typeof rawBody.note !== "string") return { error: "note must be a string" };
180
+ const note = rawBody.note.trim();
181
+ if (note) next.note = note;
182
+ else delete next.note;
183
+ touched = true;
184
+ }
185
+ if (Object.hasOwn(rawBody, "allowPrivateNetwork")) {
186
+ if (typeof rawBody.allowPrivateNetwork !== "boolean") return { error: "allowPrivateNetwork must be a boolean" };
187
+ next.allowPrivateNetwork = rawBody.allowPrivateNetwork;
188
+ touched = true;
189
+ }
190
+ if (Object.hasOwn(rawBody, "liveModels")) {
191
+ if (typeof rawBody.liveModels !== "boolean") return { error: "liveModels must be a boolean" };
192
+ next.liveModels = rawBody.liveModels;
193
+ touched = true;
194
+ }
195
+ if (Object.hasOwn(rawBody, "xaiResponsesOptIn")) {
196
+ if (name !== "xai") return { error: "xaiResponsesOptIn is valid only for provider xai" };
197
+ if (typeof rawBody.xaiResponsesOptIn !== "boolean") {
198
+ return { error: "xaiResponsesOptIn must be a boolean" };
199
+ }
200
+ const modelAdapters = { ...(next.modelAdapters ?? {}) };
201
+ for (const model of XAI_RESPONSES_OPT_IN_MODELS) {
202
+ if (rawBody.xaiResponsesOptIn) modelAdapters[model] = "openai-responses";
203
+ else delete modelAdapters[model];
204
+ }
205
+ if (Object.keys(modelAdapters).length > 0) next.modelAdapters = modelAdapters;
206
+ else delete next.modelAdapters;
207
+ touched = true;
208
+ }
209
+ if (Object.hasOwn(rawBody, "requestPacing")) {
210
+ const value = rawBody.requestPacing;
211
+ if (value === null) {
212
+ delete next.requestPacing;
213
+ } else {
214
+ if (!isPlainRecord(value)) return { error: "requestPacing must be a plain object or null" };
215
+ const pacingError = requestPacingConfigError(value);
216
+ if (pacingError) return { error: pacingError };
217
+ // `requestPacingConfigError` is the runtime narrowing boundary above; keep the
218
+ // assertion explicit because a generic plain record cannot express `enabled`.
219
+ next.requestPacing = structuredClone(value) as unknown as OcxProviderConfig["requestPacing"];
220
+ }
221
+ touched = true;
222
+ }
223
+ if (Object.hasOwn(rawBody, "upstreamHttpVersion")) {
224
+ const value = rawBody.upstreamHttpVersion;
225
+ if (value === null || value === "") {
226
+ delete next.upstreamHttpVersion;
227
+ } else {
228
+ const versionError = upstreamHttpVersionConfigError(value);
229
+ if (versionError) return { error: versionError };
230
+ // `upstreamHttpVersionConfigError` is the shared write boundary; the assertion is
231
+ // explicit because the incoming value is an unknown JSON scalar.
232
+ next.upstreamHttpVersion = value as OcxProviderConfig["upstreamHttpVersion"];
233
+ }
234
+ touched = true;
235
+ }
236
+ // The Models page edits the catalog hints in place; keep them on the existing
237
+ // provider mutation path so validation, cache invalidation, and convergence stay unified (#1073).
238
+ if (Object.hasOwn(rawBody, "contextWindow")) {
239
+ const value = rawBody.contextWindow;
240
+ if (value === null) {
241
+ delete next.contextWindow;
242
+ // `Number.isInteger(1e100)` is true, so an integer check alone admits a value that
243
+ // serializes into the catalog as an enormous number and can make Codex reject the whole
244
+ // file. Safe-integer is the real bound for something that ends up in a JSON int field.
245
+ } else if (typeof value === "number" && Number.isSafeInteger(value) && value > 0) {
246
+ next.contextWindow = value;
247
+ } else {
248
+ return { error: "contextWindow must be a positive safe integer or null" };
249
+ }
250
+ touched = true;
251
+ }
252
+ if (Object.hasOwn(rawBody, "modelContextWindows")) {
253
+ const value = rawBody.modelContextWindows;
254
+ if (value === null) {
255
+ delete next.modelContextWindows;
256
+ } else {
257
+ if (!isPlainRecord(value)) return { error: "modelContextWindows must be a plain object or null" };
258
+ const windows: Record<string, number> = { ...(next.modelContextWindows ?? {}) };
259
+ for (const [model, window] of Object.entries(value)) {
260
+ if (!model.trim()) return { error: "modelContextWindows keys must be nonblank model ids" };
261
+ if (window === null) {
262
+ delete windows[model];
263
+ continue;
264
+ }
265
+ if (typeof window !== "number" || !Number.isSafeInteger(window) || window <= 0) {
266
+ return { error: "modelContextWindows values must be positive safe integers or null" };
267
+ }
268
+ windows[model] = window;
269
+ }
270
+ if (Object.keys(windows).length > 0) next.modelContextWindows = windows;
271
+ else delete next.modelContextWindows;
272
+ }
273
+ touched = true;
274
+ }
275
+ if (Object.hasOwn(rawBody, "modelAutoCompactTokenLimits")) {
276
+ const value = rawBody.modelAutoCompactTokenLimits;
277
+ const error = modelAutoCompactTokenLimitsConfigError(value, {
278
+ allowTombstones: true,
279
+ requireNativeIds: name === "openai",
280
+ });
281
+ if (error) return { error };
282
+ if (value === null) {
283
+ delete next.modelAutoCompactTokenLimits;
284
+ } else {
285
+ const budgets: Record<string, number> = Object.assign(
286
+ Object.create(null) as Record<string, number>,
287
+ next.modelAutoCompactTokenLimits ?? {},
288
+ );
289
+ for (const [model, budget] of Object.entries(value as Record<string, number | null>)) {
290
+ if (budget === null) delete budgets[model];
291
+ else budgets[model] = budget;
292
+ }
293
+ if (Object.keys(budgets).length > 0) next.modelAutoCompactTokenLimits = budgets;
294
+ else delete next.modelAutoCompactTokenLimits;
295
+ }
296
+ touched = true;
297
+ }
298
+ if (Object.hasOwn(rawBody, "modelSupportsServiceTier")) {
299
+ const value = rawBody.modelSupportsServiceTier;
300
+ if (value === null) {
301
+ delete next.modelSupportsServiceTier;
302
+ } else {
303
+ if (!isPlainRecord(value)) return { error: "modelSupportsServiceTier must be a plain object or null" };
304
+ const capabilities: Record<string, boolean> = { ...(next.modelSupportsServiceTier ?? {}) };
305
+ for (const [model, supported] of Object.entries(value)) {
306
+ if (!model.trim()) return { error: "modelSupportsServiceTier keys must be nonblank model ids" };
307
+ if (supported === null) {
308
+ delete capabilities[model];
309
+ continue;
310
+ }
311
+ if (typeof supported !== "boolean") {
312
+ return { error: "modelSupportsServiceTier values must be booleans or null" };
313
+ }
314
+ capabilities[model] = supported;
315
+ }
316
+ if (Object.keys(capabilities).length > 0) next.modelSupportsServiceTier = capabilities;
317
+ else delete next.modelSupportsServiceTier;
318
+ }
319
+ touched = true;
320
+ }
321
+ if (Object.hasOwn(rawBody, "noStructuredOutputModels")) {
322
+ const value = rawBody.noStructuredOutputModels;
323
+ if (value === null) {
324
+ delete next.noStructuredOutputModels;
325
+ } else {
326
+ const error = nonBlankStringArrayConfigError(value, "noStructuredOutputModels");
327
+ if (error) return { error };
328
+ const models = normalizeNonBlankStringArray(value as string[]);
329
+ if (models.length > 0) next.noStructuredOutputModels = models;
330
+ else delete next.noStructuredOutputModels;
331
+ }
332
+ touched = true;
333
+ }
334
+
335
+ // headers is the one object-valued field in the mask. PATCH semantics merge it
336
+ // shallowly into the existing block so a single fingerprint header can be added
337
+ // without wiping the rest; null or an empty object clears user-managed headers.
338
+ if (Object.hasOwn(rawBody, "headers")) {
339
+ const headersValue = rawBody.headers;
340
+ if (headersValue === null || (isPlainRecord(headersValue) && Object.keys(headersValue).length === 0)) {
341
+ // Registry-owned static metadata (e.g. opencode-free's x-opencode-client marker)
342
+ // is not user-managed: restoring it keeps the upstream transport intact after a
343
+ // clear instead of deleting the whole block.
344
+ const entry = getProviderRegistryEntry(name);
345
+ if (entry?.staticHeaders && providerMatchesRegistryTransport(name, next)) {
346
+ next.headers = { ...entry.staticHeaders };
347
+ } else {
348
+ delete next.headers;
349
+ }
350
+ } else {
351
+ if (!isPlainRecord(headersValue)) return { error: "headers must be an object" };
352
+ const headersError = providerHeadersConfigError(headersValue);
353
+ if (headersError) return { error: headersError };
354
+ // Header names are case-insensitive on the wire. Drop any existing key whose
355
+ // lowercase name collides with an incoming one, or Headers normalization would
356
+ // send a combined "x-custom: v1, v2" value upstream.
357
+ const incoming = new Map(
358
+ Object.entries(headersValue as Record<string, string>).map(([key, value]) => [key.toLowerCase(), [key, value] as const]),
359
+ );
360
+ const merged: Record<string, string> = {};
361
+ for (const [key, value] of Object.entries(next.headers ?? {})) {
362
+ if (!incoming.has(key.toLowerCase())) merged[key] = value;
363
+ }
364
+ for (const [key, value] of incoming.values()) merged[key] = value;
365
+ next.headers = merged;
366
+ }
367
+ touched = true;
368
+ headersTouched = true;
369
+ }
370
+
371
+ if (!touched) return { error: "no recognized fields to update" };
372
+
373
+ // A disabled-only toggle preserves the v2 fast lane for non-openai providers: it changes
374
+ // routing eligibility, not the provider shape. Re-enabling `openai` is different — a
375
+ // malformed disabled row must not come back online unchanged, so canonicalize/reject
376
+ // against the same built-in gate used by mode PATCH and POST.
377
+ const editorTouched = keys.some(key => key !== "disabled");
378
+ const enablingOpenAi = name === "openai"
379
+ && Object.hasOwn(rawBody, "disabled")
380
+ && rawBody.disabled === false
381
+ && provider.disabled === true;
382
+ if (!editorTouched && enablingOpenAi) {
383
+ if (!isCanonicalOpenAiForwardProvider(next)) {
384
+ return { error: "provider openai must be the canonical built-in provider" };
385
+ }
386
+ // Persist the byte-identical canonical URL so config.ts startup checks (case-sensitive)
387
+ // accept the row after we fill mode. Equivalent hosts like CHATGPT.com/:443 normalize here.
388
+ next.baseUrl = CODEX_FORWARD_BASE_URL;
389
+ // Fill missing mode so a disabled canonical row becomes a complete live openai entry.
390
+ if (next.codexAccountMode !== "pool" && next.codexAccountMode !== "direct") {
391
+ next.codexAccountMode = "pool";
392
+ }
393
+ if (next.disabled === false) delete next.disabled;
394
+ // Canonical openai never uses private-network opt-in; drop a stale flag that
395
+ // was ignored for the DNS probe so it cannot linger on the live row.
396
+ delete next.allowPrivateNetwork;
397
+ }
398
+ return { next, touched, editorTouched, enablingOpenAi, headersTouched };
399
+ }
400
+
401
+ /** Validate the canonical OpenAI soft-budget overlay against a fresh registry seed. */
402
+ function canonicalOpenAiBudgetPatchError(
403
+ provider: OcxProviderConfig,
404
+ rawBody: Record<string, unknown>,
405
+ keys: string[],
406
+ config: OcxConfig,
407
+ ): string | null {
408
+ if (!isCanonicalOpenAiForwardProvider(provider)) {
409
+ return "provider openai must be the canonical built-in provider";
410
+ }
411
+ const entry = getProviderRegistryEntry("openai");
412
+ if (!entry) return "provider openai registry seed is unavailable";
413
+ const seed = providerConfigSeed(entry);
414
+ if (provider.codexAccountMode !== undefined) seed.codexAccountMode = provider.codexAccountMode;
415
+ if (provider.modelAutoCompactTokenLimits !== undefined) {
416
+ seed.modelAutoCompactTokenLimits = { ...provider.modelAutoCompactTokenLimits };
417
+ }
418
+ const applied = applyProviderPatchFields("openai", seed, rawBody, keys, config);
419
+ if ("error" in applied) return applied.error;
420
+ return providerManagementConfigError("openai", applied.next);
421
+ }
422
+
73
423
  export async function handleProviderRoutes(ctx: ManagementContext): Promise<Response | null> {
74
- const { req, url, config, deps, refreshCodexCatalogBestEffort, syncClaudeAgentDefsBestEffort } = ctx;
424
+ const { req, url, config, deps, principal, convergeCodexCatalog, syncClaudeAgentDefsBestEffort } = ctx;
75
425
 
76
426
  if (url.pathname === "/api/provider-quotas" && req.method === "GET") {
77
427
  const forceRefresh = url.searchParams.get("refresh") === "1" || url.searchParams.get("refresh") === "true";
78
428
  return jsonResponse(await fetchProviderQuotaReports(config, forceRefresh));
79
429
  }
80
430
 
431
+ if (url.pathname === "/api/provider-request-pacing" && req.method === "GET") {
432
+ const name = url.searchParams.get("name")?.trim();
433
+ if (name) {
434
+ if (!isValidProviderName(name) || !hasOwnProvider(config.providers, name)) {
435
+ return jsonResponse({ error: "unknown provider" }, 404);
436
+ }
437
+ return jsonResponse(providerRequestPacingStatus(name, config.providers[name]!));
438
+ }
439
+ return jsonResponse(Object.fromEntries(
440
+ Object.entries(config.providers).map(([providerName, provider]) => [
441
+ providerName,
442
+ providerRequestPacingStatus(providerName, provider),
443
+ ]),
444
+ ));
445
+ }
446
+
81
447
  if (url.pathname === "/api/providers" && req.method === "GET") {
82
448
  return jsonResponse(Object.entries(config.providers).map(([name, p]) => ({
83
449
  name, adapter: p.adapter, baseUrl: publicProviderBaseUrl(p.baseUrl), defaultModel: p.defaultModel,
84
450
  hasApiKey: !!p.apiKey,
451
+ // Presence only (#959 review): header names and values never leave the process.
452
+ hasHeaders: !!p.headers && Object.keys(p.headers).length > 0,
85
453
  allowPrivateNetwork: p.allowPrivateNetwork === true,
86
454
  liveModels: p.liveModels !== false,
455
+ requestPacing: p.requestPacing,
87
456
  models: p.models ?? [],
457
+ contextWindow: p.contextWindow,
458
+ modelContextWindows: p.modelContextWindows,
459
+ modelAutoCompactTokenLimits: p.modelAutoCompactTokenLimits,
460
+ modelSupportsServiceTier: p.modelSupportsServiceTier,
461
+ noStructuredOutputModels: p.noStructuredOutputModels,
462
+ upstreamHttpVersion: p.upstreamHttpVersion,
88
463
  authMode: p.authMode,
89
464
  apiKeyTransport: p.apiKeyTransport,
90
465
  disabled: p.disabled === true,
91
466
  codexAccountMode: providerCodexAccountMode(name, p),
467
+ ...(name === "xai" ? { xaiResponsesOptInState: xaiResponsesOptInState(p) } : {}),
92
468
  discovery: p.liveModels === false ? undefined : getProviderDiscoveryStatus(name),
93
469
  })));
94
470
  }
95
471
 
472
+ if (url.pathname === LOCAL_PROVIDER_RELOAD_PATH && req.method === "POST") {
473
+ if (principal !== "local-provider-reload-capability") {
474
+ return jsonResponse({ error: "provider reload capability required" }, 403);
475
+ }
476
+ const name = req.headers.get(LOCAL_PROVIDER_RELOAD_NAME_HEADER) ?? "";
477
+ if (!isValidProviderName(name)) return jsonResponse({ error: "invalid provider reload target" }, 400);
478
+
479
+ const admitted = readConfigAdmissionSnapshot();
480
+ if (
481
+ admitted.kind !== "read"
482
+ || admitted.diagnostics.source !== "file"
483
+ || admitted.diagnostics.error !== null
484
+ ) {
485
+ return jsonResponse({ error: "provider reload source unavailable" }, 409);
486
+ }
487
+ const diskConfig = admitted.diagnostics.config;
488
+ if (!hasOwnProvider(diskConfig.providers, name)) {
489
+ return jsonResponse({ error: "provider reload target unavailable" }, 404);
490
+ }
491
+ const provider = diskConfig.providers[name]!;
492
+ const providerError = providerManagementConfigError(name, provider);
493
+ if (providerError) return jsonResponse({ error: "provider reload target invalid" }, 409);
494
+ const namespaceCollision = codexAccountNamespaceProviderCollisionError(
495
+ diskConfig.codexAccountNamespaces,
496
+ name,
497
+ );
498
+ if (namespaceCollision) return jsonResponse({ error: "provider reload target conflicts with routing" }, 409);
499
+ const allowBenchmarkAddresses = name === "openai" && isCanonicalOpenAiForwardProvider(provider);
500
+ const resolvedError = await providerDestinationResolvedError(name, provider, { allowBenchmarkAddresses });
501
+ if (resolvedError) return jsonResponse({ error: "provider reload target rejected" }, 409);
502
+
503
+ // Destination validation awaits DNS. A cooperating writer holds the same SQLite
504
+ // mutation lock, so the final exact-byte check and live adoption happen as one
505
+ // synchronous authority decision. The route does not save or reserialize disk.
506
+ let currentDiskConfig: OcxConfig | null = null;
507
+ let sourceChanged = false;
508
+ withConfigMutationLockSync(() => {
509
+ const current = readConfigAdmissionSnapshot();
510
+ if (
511
+ current.kind !== "read"
512
+ || current.diagnostics.source !== "file"
513
+ || current.diagnostics.error !== null
514
+ || current.contentSha256 !== admitted.contentSha256
515
+ ) {
516
+ sourceChanged = true;
517
+ return;
518
+ }
519
+ currentDiskConfig = current.diagnostics.config;
520
+ adoptPersistedProviderIntoLiveConfig(
521
+ config,
522
+ name,
523
+ current.diagnostics.config.providers[name]!,
524
+ current.diagnostics.config,
525
+ );
526
+ });
527
+ if (sourceChanged || currentDiskConfig === null) {
528
+ return jsonResponse({ error: "provider reload source changed" }, 409);
529
+ }
530
+ reconcileLiveStateStores();
531
+ // The complete disk snapshot owns display overlays, including providers that this
532
+ // live routing instance deliberately does not adopt.
533
+ refreshUserCostOverlays(currentDiskConfig);
534
+ clearGatherRoutedModelsInflight();
535
+ (deps.clearProviderQuotaCache ?? clearProviderQuotaCache)();
536
+ clearAccountQuotaCache(name);
537
+ clearKeyCooldowns(name);
538
+ clearModelCache(name);
539
+ if (name === "openai") (deps.clearThreadAccountMap ?? clearThreadAccountMap)();
540
+ const catalogRefresh = await convergeCodexCatalog();
541
+ return jsonResponse({ success: true, name, catalogRefresh });
542
+ }
543
+
96
544
  // Add (or overwrite) a single provider. Merges into the live in-memory config and
97
545
  // persists — existing providers' real keys are never round-tripped (unlike PUT /api/config,
98
546
  // which would re-save the masked keys from GET). Live routing picks it up immediately.
@@ -102,7 +550,13 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
102
550
  const name = typeof body.name === "string" ? body.name.trim() : "";
103
551
  const providerError = providerManagementConfigError(name, body.provider);
104
552
  if (providerError) return jsonResponse({ error: providerError }, 400);
553
+ const serviceTierError = providerServiceTierConfigError(name, body.provider);
554
+ if (serviceTierError) return jsonResponse({ error: serviceTierError }, 400);
105
555
  const prov = body.provider ? stripCodexRuntimeProviderFields(body.provider as OcxProviderConfig) : undefined;
556
+ // PATCH already clears on null; POST persisted the body as submitted, so a `null` here
557
+ // reached disk and the next loadConfig() refused it. Canonicalize to absent, which is what
558
+ // "clear" means everywhere else.
559
+ if (prov && prov.upstreamHttpVersion === null) delete prov.upstreamHttpVersion;
106
560
  if (!name || !prov?.adapter || !prov?.baseUrl) {
107
561
  return jsonResponse({ error: "name, provider.adapter and provider.baseUrl are required" }, 400);
108
562
  }
@@ -128,12 +582,56 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
128
582
  }
129
583
  // Catalog providers (e.g. ollama-cloud) carry a models + vision/reasoning classification the GUI
130
584
  // doesn't send — merge it in so the sidecars are gated correctly.
585
+ // Sample request ownership BEFORE enrichment. Enrichment fills absent fields from the
586
+ // registry seed, after which "the client omitted this" and "the registry supplied it" are
587
+ // indistinguishable — so a carry-over guard written as `prov.x === undefined` after this
588
+ // call can never fire.
589
+ const submittedContextWindow = Object.hasOwn(prov, "contextWindow");
590
+ const submittedModelContextWindows = Object.hasOwn(prov, "modelContextWindows");
591
+ const submittedModelAutoCompactTokenLimits = Object.hasOwn(prov, "modelAutoCompactTokenLimits");
592
+ const submittedRequestPacing = Object.hasOwn(prov, "requestPacing");
131
593
  enrichProviderFromCatalog(name, prov);
132
594
  const { saveConfigPreservingClaudeCode: save } = await import("../../config");
133
595
  // Overwriting an existing provider must not drop its multi-key pool: carry it over, then
134
596
  // let the (possibly new) apiKey join the pool as the active entry.
135
597
  const existingPool = config.providers[name]?.apiKeyPool;
136
598
  if (existingPool && !prov.apiKeyPool) prov.apiKeyPool = existingPool;
599
+ // The same rule applies to user-configured price overlays: the dashboard's
600
+ // add/edit form does not send modelCosts, so an overwrite must not silently
601
+ // erase hand-edited per-model prices from Logs/Usage estimates.
602
+ const existingCosts = config.providers[name]?.modelCosts;
603
+ if (existingCosts && !prov.modelCosts) prov.modelCosts = existingCosts;
604
+ // And to the per-provider account-failover opt-out (#2568d). `ProviderPayload` has no
605
+ // member for it either, so an add/edit save structurally cannot carry it — and dropping it
606
+ // silently ENABLES rotation, because activation is presence-driven once the knob is gone.
607
+ // An overwrite must not spend a second subscription account's quota as a side effect.
608
+ const existingFailover = config.providers[name]?.oauthAccountFailover;
609
+ if (existingFailover && !prov.oauthAccountFailover) prov.oauthAccountFailover = existingFailover;
610
+ // ...and to hand-edited context windows. `ProviderPayload` (gui/src/provider-payload.ts)
611
+ // has no member for either field, so the add/edit form structurally cannot send them:
612
+ // absence in the request means "not carried", never "the user deleted it". Deletion goes
613
+ // through PATCH with an explicit null (#1409).
614
+ const existing = config.providers[name];
615
+ if (!submittedRequestPacing && existing?.requestPacing) {
616
+ prov.requestPacing = structuredClone(existing.requestPacing);
617
+ }
618
+ if (!submittedContextWindow && existing?.contextWindow !== undefined) {
619
+ prov.contextWindow = existing.contextWindow;
620
+ }
621
+ if (existing?.modelContextWindows) {
622
+ // When the client did send a map, its keys win and the user's other keys survive. When
623
+ // it did not, the stored value is the user's map alone: merging the registry seed in
624
+ // would persist seed keys into user config as a side effect of an unrelated save, and
625
+ // router.ts already fills registry values beneath user entries at resolve time.
626
+ prov.modelContextWindows = submittedModelContextWindows
627
+ ? { ...existing.modelContextWindows, ...(prov.modelContextWindows ?? {}) }
628
+ : { ...existing.modelContextWindows };
629
+ }
630
+ if (existing?.modelAutoCompactTokenLimits) {
631
+ prov.modelAutoCompactTokenLimits = submittedModelAutoCompactTokenLimits
632
+ ? { ...existing.modelAutoCompactTokenLimits, ...(prov.modelAutoCompactTokenLimits ?? {}) }
633
+ : { ...existing.modelAutoCompactTokenLimits };
634
+ }
137
635
  config.providers[name] = stripRegistryOnlyStaticHeaders(name, prov);
138
636
  if (body.setDefault === true) config.defaultProvider = name;
139
637
  save(config);
@@ -144,8 +642,8 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
144
642
  }
145
643
  const { clearModelCache } = await import("../../codex/model-cache");
146
644
  clearModelCache(name);
147
- await refreshCodexCatalogBestEffort();
148
- return jsonResponse({ success: true, name });
645
+ const catalogRefresh = await convergeCodexCatalog();
646
+ return jsonResponse({ success: true, name, catalogRefresh });
149
647
  }
150
648
 
151
649
  if (url.pathname === "/api/providers" && req.method === "PATCH") {
@@ -157,6 +655,9 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
157
655
  const keys = Object.keys(rawBody);
158
656
  const hasMode = Object.hasOwn(rawBody, "codexAccountMode");
159
657
  const hasSetDefault = Object.hasOwn(rawBody, "setDefault");
658
+ const canonicalBudgetOnly = name === "openai"
659
+ && keys.length === 1
660
+ && keys[0] === "modelAutoCompactTokenLimits";
160
661
 
161
662
  // codexAccountMode keeps its dedicated side-effect path (quota cache clear, thread map
162
663
  // clear, pool prime) and is mutually exclusive with every other patch field.
@@ -213,106 +714,23 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
213
714
  if (Object.hasOwn(rawBody, "apiKey")) {
214
715
  return jsonResponse({ error: "apiKey cannot be patched here; use the provider API-key endpoints" }, 400);
215
716
  }
216
- const next: OcxProviderConfig = { ...config.providers[name]! };
217
- let touched = false;
717
+ const applied = applyProviderPatchFields(name, config.providers[name]!, rawBody, keys, config);
718
+ if ("error" in applied) return jsonResponse({ error: applied.error }, 400);
719
+ const next = applied.next;
218
720
 
219
- if (Object.hasOwn(rawBody, "disabled")) {
220
- if (typeof rawBody.disabled !== "boolean") return jsonResponse({ error: "disabled must be a boolean" }, 400);
221
- if (rawBody.disabled && name === config.defaultProvider) {
222
- return jsonResponse({ error: "cannot disable the default provider; set another default first" }, 400);
223
- }
224
- next.disabled = rawBody.disabled;
225
- touched = true;
226
- }
227
- if (Object.hasOwn(rawBody, "adapter")) {
228
- if (typeof rawBody.adapter !== "string" || !rawBody.adapter.trim()) return jsonResponse({ error: "adapter must be a non-empty string" }, 400);
229
- next.adapter = rawBody.adapter.trim();
230
- touched = true;
231
- }
232
- if (Object.hasOwn(rawBody, "baseUrl")) {
233
- if (typeof rawBody.baseUrl !== "string" || !rawBody.baseUrl.trim()) return jsonResponse({ error: "baseUrl must be a non-empty string" }, 400);
234
- next.baseUrl = rawBody.baseUrl.trim();
235
- touched = true;
236
- }
237
- if (Object.hasOwn(rawBody, "defaultModel")) {
238
- if (typeof rawBody.defaultModel !== "string") return jsonResponse({ error: "defaultModel must be a string" }, 400);
239
- const dm = rawBody.defaultModel.trim();
240
- if (dm) next.defaultModel = dm;
241
- else delete next.defaultModel;
242
- touched = true;
243
- }
244
- if (Object.hasOwn(rawBody, "authMode")) {
245
- if (typeof rawBody.authMode !== "string") return jsonResponse({ error: "authMode must be a string" }, 400);
246
- const mode = rawBody.authMode.trim();
247
- if (mode === "key" || mode === "forward" || mode === "oauth" || mode === "local") {
248
- next.authMode = mode;
249
- touched = true;
250
- } else if (mode === "") {
251
- delete next.authMode;
252
- touched = true;
253
- } else {
254
- return jsonResponse({ error: "authMode must be key, forward, oauth, or local" }, 400);
255
- }
256
- }
257
- if (Object.hasOwn(rawBody, "apiKeyTransport")) {
258
- const transport = rawBody.apiKeyTransport;
259
- if (transport === "x-api-key" || transport === "bearer") {
260
- next.apiKeyTransport = transport;
261
- touched = true;
262
- } else if (transport === "") {
263
- delete next.apiKeyTransport;
264
- touched = true;
265
- } else {
266
- return jsonResponse({ error: "apiKeyTransport must be x-api-key, bearer, or empty to clear" }, 400);
267
- }
268
- }
269
- if (Object.hasOwn(rawBody, "note")) {
270
- if (typeof rawBody.note !== "string") return jsonResponse({ error: "note must be a string" }, 400);
271
- const note = rawBody.note.trim();
272
- if (note) next.note = note;
273
- else delete next.note;
274
- touched = true;
275
- }
276
-
277
- if (Object.hasOwn(rawBody, "allowPrivateNetwork")) {
278
- if (typeof rawBody.allowPrivateNetwork !== "boolean") return jsonResponse({ error: "allowPrivateNetwork must be a boolean" }, 400);
279
- next.allowPrivateNetwork = rawBody.allowPrivateNetwork;
280
- touched = true;
281
- }
282
-
283
- if (Object.hasOwn(rawBody, "liveModels")) {
284
- if (typeof rawBody.liveModels !== "boolean") return jsonResponse({ error: "liveModels must be a boolean" }, 400);
285
- next.liveModels = rawBody.liveModels;
286
- touched = true;
287
- }
288
-
289
- if (!touched) return jsonResponse({ error: "no recognized fields to update" }, 400);
290
-
291
- // A disabled-only toggle preserves the v2 fast lane for non-openai providers: it changes
292
- // routing eligibility, not the provider shape. Re-enabling `openai` is different — a
293
- // malformed disabled row must not come back online unchanged, so canonicalize/reject
294
- // against the same built-in gate used by mode PATCH and POST.
295
- const editorTouched = keys.some(key => key !== "disabled");
296
- const enablingOpenAi = name === "openai"
297
- && Object.hasOwn(rawBody, "disabled")
298
- && rawBody.disabled === false
299
- && config.providers[name]?.disabled === true;
300
- if (editorTouched) {
301
- const providerError = providerManagementConfigError(name, next);
721
+ const pacingOnly = keys.every(key => key === "requestPacing");
722
+ if (applied.editorTouched && !pacingOnly) {
723
+ const providerError = canonicalBudgetOnly
724
+ ? canonicalOpenAiBudgetPatchError(next, rawBody, keys, config)
725
+ : providerManagementConfigError(name, next);
302
726
  if (providerError) return jsonResponse({ error: providerError }, 400);
303
- const resolvedError = await providerDestinationResolvedError(name, next);
304
- if (resolvedError) return jsonResponse({ error: resolvedError }, 400);
305
- } else if (enablingOpenAi) {
306
- if (!isCanonicalOpenAiForwardProvider(next)) {
307
- return jsonResponse({ error: "provider openai must be the canonical built-in provider" }, 400);
308
- }
309
- // Persist the byte-identical canonical URL so config.ts startup checks (case-sensitive)
310
- // accept the row after we fill mode. Equivalent hosts like CHATGPT.com/:443 normalize here.
311
- next.baseUrl = CODEX_FORWARD_BASE_URL;
312
- // Fill missing mode so a disabled canonical row becomes a complete live openai entry.
313
- if (next.codexAccountMode !== "pool" && next.codexAccountMode !== "direct") {
314
- next.codexAccountMode = "pool";
727
+ if (!canonicalBudgetOnly) {
728
+ const serviceTierError = providerServiceTierConfigError(name, next);
729
+ if (serviceTierError) return jsonResponse({ error: serviceTierError }, 400);
730
+ const resolvedError = await providerDestinationResolvedError(name, next);
731
+ if (resolvedError) return jsonResponse({ error: resolvedError }, 400);
315
732
  }
733
+ } else if (applied.enablingOpenAi) {
316
734
  // Same DNS gate as POST: Clash fake-IP only. Never honor a persisted
317
735
  // allowPrivateNetwork on this path — it must not bypass the built-in guard.
318
736
  const resolvedError = await providerDestinationResolvedError(
@@ -321,26 +739,184 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
321
739
  { allowBenchmarkAddresses: true },
322
740
  );
323
741
  if (resolvedError) return jsonResponse({ error: resolvedError }, 400);
324
- if (next.disabled === false) delete next.disabled;
325
- // Canonical openai never uses private-network opt-in; drop a stale flag that
326
- // was ignored for the DNS probe so it cannot linger on the live row.
327
- delete next.allowPrivateNetwork;
328
742
  }
329
743
 
330
- const { saveConfigPreservingClaudeCode: save } = await import("../../config");
331
- config.providers[name] = stripRegistryOnlyStaticHeaders(name, next);
332
- save(config);
744
+ // The live config is shared and the destination probe above is awaited. Re-apply the
745
+ // mask onto the newest provider under the mutation lock right before saving, so two
746
+ // concurrent PATCHes updating different fields/headers both survive instead of the
747
+ // later save clobbering the earlier snapshot.
748
+ let replayError: string | undefined;
749
+ withConfigMutationLockSync(() => {
750
+ const replay = applyProviderPatchFields(name, config.providers[name]!, rawBody, keys, config);
751
+ if ("error" in replay) {
752
+ replayError = replay.error;
753
+ return;
754
+ }
755
+ if (replay.editorTouched && !pacingOnly) {
756
+ const syncError = canonicalBudgetOnly
757
+ ? canonicalOpenAiBudgetPatchError(replay.next, rawBody, keys, config)
758
+ : providerManagementConfigError(name, replay.next);
759
+ if (syncError) {
760
+ replayError = syncError;
761
+ return;
762
+ }
763
+ if (!canonicalBudgetOnly) {
764
+ const serviceTierError = providerServiceTierConfigError(name, replay.next);
765
+ if (serviceTierError) {
766
+ replayError = serviceTierError;
767
+ return;
768
+ }
769
+ }
770
+ } else if (replay.enablingOpenAi && !isCanonicalOpenAiForwardProvider(replay.next)) {
771
+ replayError = "provider openai must be the canonical built-in provider";
772
+ return;
773
+ }
774
+ // A PATCH that managed headers owns the resulting block: the clear path restores
775
+ // registry static headers, so exact-match stripping must not erase them again.
776
+ config.providers[name] = replay.headersTouched ? replay.next : stripRegistryOnlyStaticHeaders(name, replay.next);
777
+ saveConfigPreservingClaudeCode(config);
778
+ });
779
+ if (replayError !== undefined) return jsonResponse({ error: replayError }, 409);
333
780
  reconcileLiveStateStores();
334
- if (editorTouched) {
781
+ if (applied.editorTouched && !pacingOnly) {
335
782
  const { clearModelCache } = await import("../../codex/model-cache");
336
783
  clearModelCache(name);
337
784
  }
338
- await refreshCodexCatalogBestEffort();
785
+ const catalogRefresh = pacingOnly ? null : await convergeCodexCatalog();
339
786
  return jsonResponse({
340
787
  success: true,
341
788
  name,
342
789
  disabled: config.providers[name]!.disabled === true,
343
790
  hasApiKey: !!config.providers[name]!.apiKey,
791
+ ...(name === "xai"
792
+ ? { xaiResponsesOptInState: xaiResponsesOptInState(config.providers[name]!) }
793
+ : {}),
794
+ catalogRefresh,
795
+ });
796
+ }
797
+
798
+ // Force-refresh one provider's live model list: drop the per-provider TTL cache (and any
799
+ // in-flight multi-provider gather that could re-serve the old payload), re-fetch, then best-
800
+ // effort rebuild the Codex catalog so the Models page and Codex pickers pick up the new rows.
801
+ if (url.pathname === "/api/providers/refresh-models" && req.method === "POST") {
802
+ const name = url.searchParams.get("name")?.trim();
803
+ if (!name || !isValidProviderName(name) || !hasOwnProvider(config.providers, name)) {
804
+ return jsonResponse({ error: "unknown provider" }, 404);
805
+ }
806
+ const raw = config.providers[name]!;
807
+ if (raw.disabled) {
808
+ return jsonResponse({
809
+ ok: false,
810
+ provider: name,
811
+ models: [],
812
+ count: 0,
813
+ error: "Provider is disabled",
814
+ });
815
+ }
816
+ const prov = { ...raw };
817
+ enrichProviderFromRegistry(name, prov);
818
+ const started = Date.now();
819
+
820
+ if (prov.authMode === "forward") {
821
+ return jsonResponse({
822
+ ok: true,
823
+ provider: name,
824
+ models: [],
825
+ count: 0,
826
+ source: "forward",
827
+ latencyMs: Date.now() - started,
828
+ message: "Passthrough provider has no upstream /models catalog.",
829
+ });
830
+ }
831
+
832
+ if (prov.liveModels === false) {
833
+ const models = [...(prov.models ?? [])];
834
+ if (models.length === 0 && prov.defaultModel) models.push(prov.defaultModel);
835
+ return jsonResponse({
836
+ ok: true,
837
+ provider: name,
838
+ models,
839
+ count: models.length,
840
+ source: "static",
841
+ latencyMs: Date.now() - started,
842
+ message: "Live discovery is off; returning configured/static models only.",
843
+ });
844
+ }
845
+
846
+ clearModelCache(name);
847
+ clearGatherRoutedModelsInflight();
848
+ const ttlMs = config.modelCacheTtlMs ?? DEFAULT_MODEL_CACHE_TTL_MS;
849
+ const catalogModels = await fetchProviderModels(
850
+ name,
851
+ prov,
852
+ ttlMs,
853
+ providerContextCap(config, name),
854
+ );
855
+ const models = catalogModels.map(model => model.id);
856
+ const discovery = getProviderDiscoveryStatus(name);
857
+ const liveModelCount = getProviderLiveModelCount(name);
858
+ const latencyMs = Date.now() - started;
859
+
860
+ if (discovery?.status === "failed") {
861
+ const error = discovery.reason === "http"
862
+ ? `upstream /models returned ${discovery.httpStatus}`
863
+ : discovery.reason === "blocked"
864
+ ? "upstream /models blocked by destination policy"
865
+ : discovery.reason === "invalid_response"
866
+ ? "upstream /models returned an unexpected response"
867
+ : discovery.reason === "network"
868
+ ? "upstream /models network error"
869
+ : "provider model discovery failed";
870
+ // Do not rewrite providers[name].models from a failed probe — keep the last good seed.
871
+ await convergeCodexCatalog();
872
+ return jsonResponse({
873
+ ok: false,
874
+ provider: name,
875
+ models,
876
+ count: models.length,
877
+ source: "fallback",
878
+ persisted: false,
879
+ discovery,
880
+ ...(liveModelCount !== undefined ? { liveModelCount } : {}),
881
+ latencyMs,
882
+ error,
883
+ });
884
+ }
885
+
886
+ // Live success: write the discovered ids into the provider config so the Models tab,
887
+ // Settings default-model picker, and static fallback all see the same list after restart.
888
+ // Only discovery.status === "ok" means upstream actually answered; "configured" without a
889
+ // live status is the no-token / no-fetch path and must not clobber an existing models seed.
890
+ let persisted = false;
891
+ if (discovery?.status === "ok") {
892
+ const existing = config.providers[name]!;
893
+ if (models.length > 0) existing.models = models;
894
+ else delete existing.models;
895
+ // Drop allowlist entries that no longer exist upstream so selectedModels cannot hide
896
+ // every newly discovered row while pointing at dead ids.
897
+ if (Array.isArray(existing.selectedModels) && existing.selectedModels.length > 0) {
898
+ const liveIds = new Set(models);
899
+ const kept = existing.selectedModels.filter(id => liveIds.has(id));
900
+ if (kept.length > 0) existing.selectedModels = kept;
901
+ else delete existing.selectedModels;
902
+ }
903
+ const { saveConfigPreservingClaudeCode: save } = await import("../../config");
904
+ save(config);
905
+ reconcileLiveStateStores();
906
+ persisted = true;
907
+ }
908
+
909
+ await convergeCodexCatalog();
910
+ return jsonResponse({
911
+ ok: true,
912
+ provider: name,
913
+ models,
914
+ count: models.length,
915
+ source: discovery?.status === "ok" ? "live" : "configured",
916
+ persisted,
917
+ ...(discovery ? { discovery } : {}),
918
+ ...(liveModelCount !== undefined ? { liveModelCount } : {}),
919
+ latencyMs,
344
920
  });
345
921
  }
346
922
 
@@ -370,19 +946,54 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
370
946
  // credential resolution/network access for providers such as Antigravity.
371
947
  return jsonResponse({ applicable: false, reason: "static_catalog", latencyMs: 0 });
372
948
  }
373
- const { resolveModelsAuthToken, buildModelsRequest } = await import("../../oauth");
374
- const apiKey = await resolveModelsAuthToken(name, prov);
949
+ const { buildModelsRequest, getValidAccessTokenSnapshot, resolveModelsAuthToken } = await import("../../oauth");
950
+ const antigravity = effectiveGoogleMode(name, prov) === "cloud-code-assist";
951
+ const snapshot = antigravity
952
+ ? await getValidAccessTokenSnapshot(name).catch(() => undefined)
953
+ : undefined;
954
+ const apiKey = snapshot?.accessToken ?? await resolveModelsAuthToken(name, prov);
375
955
  if (prov.authMode === "oauth" && !apiKey) {
376
956
  return jsonResponse({ ok: false, latencyMs: 0, error: "static catalog only — upstream not verified (not logged in)" });
377
957
  }
378
- const { url: modelsUrl, headers } = buildModelsRequest(prov, apiKey, name);
958
+ if (prov.adapter === "cursor") {
959
+ const started = Date.now();
960
+ const live = await fetchCursorUsableModels({
961
+ apiKey: apiKey ?? "",
962
+ baseUrl: prov.baseUrl,
963
+ });
964
+ const latencyMs = Date.now() - started;
965
+ if (!live.ok) {
966
+ return jsonResponse({
967
+ ok: false,
968
+ latencyMs,
969
+ error: `cursor discovery ${live.error}${live.detail ? `: ${live.detail}` : ""}`,
970
+ });
971
+ }
972
+ return jsonResponse({
973
+ ok: true,
974
+ latencyMs,
975
+ models: live.models.length,
976
+ message: `Connected. ${live.models.length} models.`,
977
+ });
978
+ }
979
+ const project = prov.project ?? snapshot?.projectId;
980
+ if (antigravity && !project) {
981
+ return jsonResponse({ ok: false, latencyMs: 0, error: "Antigravity project unavailable — re-run `ocx login google-antigravity`" });
982
+ }
983
+ const { method, url: modelsUrl, headers } = buildModelsRequest(prov, apiKey, name);
379
984
  const discovery = resolveProviderModelDiscovery(name, prov);
380
985
  const started = Date.now();
381
986
  try {
382
- const res = await providerOutboundGet(name, prov, modelsUrl, {
383
- headers,
384
- signal: AbortSignal.timeout(8000),
385
- });
987
+ const res = method === "POST"
988
+ ? await providerOutboundPost(name, prov, modelsUrl, {
989
+ headers,
990
+ body: JSON.stringify({ project }),
991
+ signal: AbortSignal.timeout(8000),
992
+ })
993
+ : await providerOutboundGet(name, prov, modelsUrl, {
994
+ headers,
995
+ signal: AbortSignal.timeout(8000),
996
+ });
386
997
  const latencyMs = Date.now() - started;
387
998
  const redirectError = await providerRedirectError(res, modelsUrl);
388
999
  if (redirectError) {
@@ -398,7 +1009,7 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
398
1009
  } catch {
399
1010
  // Best-effort release for non-conforming response streams.
400
1011
  }
401
- return jsonResponse({ ok: false, latencyMs, error: `upstream /models returned ${res.status}` });
1012
+ return jsonResponse({ ok: false, latencyMs, error: `upstream model discovery returned ${res.status}` });
402
1013
  }
403
1014
  const bounded = await readBoundedDiscoveryJson(res, discovery.maxResponseBytes);
404
1015
  if (!bounded.ok) {
@@ -406,20 +1017,26 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
406
1017
  ok: false,
407
1018
  latencyMs,
408
1019
  error: bounded.reason === "response_too_large"
409
- ? `upstream /models exceeded the ${discovery.maxResponseBytes}-byte response limit`
410
- : "upstream /models returned invalid JSON",
1020
+ ? `upstream model discovery exceeded the ${discovery.maxResponseBytes}-byte response limit`
1021
+ : "upstream model discovery returned invalid JSON",
411
1022
  });
412
1023
  }
1024
+ const ccaModels = antigravity ? parseAntigravityAvailableModels(bounded.value, discovery.maxModels) : undefined;
1025
+ if (antigravity && !ccaModels) {
1026
+ return jsonResponse({ ok: false, latencyMs, error: "upstream CCA model discovery returned an unexpected shape" });
1027
+ }
413
1028
  // OpenAI-style lists (and Together top-level arrays) use the same validation/dedupe/filter
414
1029
  // as catalog discovery. Google's /v1beta/models uses `models[].name` and remains a
415
1030
  // connectivity-only count because it is not an authoritative catalog source.
416
1031
  const record = bounded.value !== null && typeof bounded.value === "object" && !Array.isArray(bounded.value)
417
1032
  ? bounded.value as Record<string, unknown>
418
1033
  : undefined;
419
- const extracted = Array.isArray(bounded.value) || Array.isArray(record?.data)
1034
+ const extracted = ccaModels
1035
+ ? undefined
1036
+ : Array.isArray(bounded.value) || Array.isArray(record?.data)
420
1037
  ? extractProviderModelItems(bounded.value, discovery)
421
1038
  : extractModelEnvelopeRows(bounded.value, discovery.maxModels, ["models"]);
422
- if (!extracted.ok) {
1039
+ if (extracted && !extracted.ok) {
423
1040
  return jsonResponse({
424
1041
  ok: false,
425
1042
  latencyMs,
@@ -428,7 +1045,7 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
428
1045
  : "upstream /models returned an unexpected shape",
429
1046
  });
430
1047
  }
431
- const models = "items" in extracted ? extracted.items.length : extracted.rows.length;
1048
+ const models = ccaModels?.length ?? ("items" in extracted! ? extracted!.items.length : extracted!.rows.length);
432
1049
  return jsonResponse({
433
1050
  ok: true,
434
1051
  latencyMs,
@@ -479,13 +1096,21 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
479
1096
  const { saveConfigPreservingClaudeCode: save } = await import("../../config");
480
1097
  if (fallbackDefault) config.defaultProvider = fallbackDefault;
481
1098
  delete config.providers[name];
1099
+ const { dropProviderCustomModels } = await import("../../providers/provider-id-rewrite");
1100
+ const droppedCustomModels = dropProviderCustomModels(config, name);
482
1101
  setProviderContextCap(config, name, false);
483
1102
  save(config);
1103
+ await replaceProviderAccountSet(name, null);
484
1104
  reconcileLiveStateStores();
485
1105
  const { clearModelCache: clearCache } = await import("../../codex/model-cache");
486
1106
  clearCache(name);
487
- await refreshCodexCatalogBestEffort();
488
- return jsonResponse({ success: true, ...(fallbackDefault ? { defaultProvider: fallbackDefault } : {}) });
1107
+ const catalogRefresh = await convergeCodexCatalog();
1108
+ return jsonResponse({
1109
+ success: true,
1110
+ ...(fallbackDefault ? { defaultProvider: fallbackDefault } : {}),
1111
+ ...(droppedCustomModels > 0 ? { droppedCustomModels } : {}),
1112
+ catalogRefresh,
1113
+ });
489
1114
  }
490
1115
 
491
1116
  if (url.pathname === "/api/provider-context-caps" && req.method === "GET") {
@@ -493,27 +1118,94 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
493
1118
  }
494
1119
 
495
1120
  if (url.pathname === "/api/provider-context-caps" && req.method === "PUT") {
496
- let body: { provider?: unknown; enabled?: unknown; value?: unknown; setAll?: unknown };
497
- try { body = await readManagementJsonBody(req); } catch (error) { rethrowManagementBodyTooLarge(error); return jsonResponse({ error: "invalid JSON body" }, 400); }
1121
+ let rawBody: unknown;
1122
+ try { rawBody = await readManagementJsonBody(req); } catch (error) { rethrowManagementBodyTooLarge(error); return jsonResponse({ error: "invalid JSON body" }, 400); }
1123
+ // Reject non-object payloads (e.g. `{"provider": true}` or a bare array) before any
1124
+ // property access, with the route's consistent 400 response.
1125
+ if (!isPlainRecord(rawBody)) return jsonResponse({ error: "provider-context-caps body must be a plain object" }, 400);
1126
+ const body = rawBody as { provider?: unknown; enabled?: unknown; value?: unknown; setAll?: unknown };
498
1127
  const { saveConfigPreservingClaudeCode: save } = await import("../../config");
499
1128
  const { clearModelCache } = await import("../../codex/model-cache");
500
- const respond = () => jsonResponse({ ok: true, cap: DEFAULT_PROVIDER_CONTEXT_CAP, value: globalContextCapValue(config), caps: providerContextCaps(config) });
1129
+ const respond = (catalogRefresh: Awaited<ReturnType<typeof convergeCodexCatalog>>) => jsonResponse({
1130
+ ok: true,
1131
+ cap: DEFAULT_PROVIDER_CONTEXT_CAP,
1132
+ value: globalContextCapValue(config),
1133
+ caps: providerContextCaps(config),
1134
+ catalogRefresh,
1135
+ });
501
1136
 
502
- // Branch 1: set the global cap value and re-point every enabled provider to it.
1137
+ // Reject malformed and mixed payloads before branch selection: `provider` and `enabled`
1138
+ // must appear together with their expected types, and provider updates cannot be
1139
+ // combined with `setAll` (which would otherwise be silently ignored).
1140
+ const hasProviderFields = Object.hasOwn(body, "provider") || Object.hasOwn(body, "enabled");
1141
+ if (hasProviderFields) {
1142
+ if (typeof body.provider !== "string" || typeof body.enabled !== "boolean") {
1143
+ return jsonResponse({ error: "provider and enabled are required together" }, 400);
1144
+ }
1145
+ if (Object.hasOwn(body, "setAll")) {
1146
+ return jsonResponse({ error: "setAll cannot be combined with provider updates" }, 400);
1147
+ }
1148
+ }
1149
+
1150
+ // Branch 1: per-provider toggle (checked first: a per-provider request may carry an
1151
+ // explicit `value`, which must never fall through to the global-value branch). Enable
1152
+ // writes the current global default unless an explicit per-provider value is supplied;
1153
+ // that value is never copied to other providers.
1154
+ if (typeof body.provider === "string" && typeof body.enabled === "boolean") {
1155
+ const provider = body.provider.trim();
1156
+ if (!isValidProviderName(provider)) {
1157
+ return jsonResponse({ error: "provider name must use letters, numbers, dot, underscore, or hyphen and cannot be a reserved object key" }, 400);
1158
+ }
1159
+ if (!hasOwnProvider(config.providers, provider)) {
1160
+ return jsonResponse({ error: "unknown provider" }, 404);
1161
+ }
1162
+ // Validate a supplied per-provider value before mutating anything: it must be a
1163
+ // finite number, and after flooring it must still be >= 1 — a value like 0.5 would
1164
+ // otherwise floor to 0 and silently fall back to the global default.
1165
+ if (body.value !== undefined && (typeof body.value !== "number" || !Number.isFinite(body.value))) {
1166
+ return jsonResponse({ error: "value must be a positive number" }, 400);
1167
+ }
1168
+ const perProviderValue = typeof body.value === "number" ? Math.floor(body.value) : undefined;
1169
+ if (perProviderValue !== undefined && perProviderValue < 1) {
1170
+ return jsonResponse({ error: "value must be a positive number" }, 400);
1171
+ }
1172
+ setProviderContextCap(config, provider, body.enabled, perProviderValue);
1173
+ save(config);
1174
+ reconcileLiveStateStores();
1175
+ clearModelCache(provider);
1176
+ const catalogRefresh = await convergeCodexCatalog();
1177
+ return respond(catalogRefresh);
1178
+ }
1179
+
1180
+ // Branch 2: set the global cap value. When `setAll` accompanies it (dashboard "apply to
1181
+ // every routed provider" toggle), re-point every enabled provider; otherwise keep each
1182
+ // provider's own cap value and only change the default for future toggles.
503
1183
  if (body.value !== undefined) {
504
- if (typeof body.value !== "number" || !Number.isFinite(body.value) || body.value <= 0) {
1184
+ // Same normalization as the per-provider branch: floor before validating so a value
1185
+ // that floors to zero is rejected rather than silently stored.
1186
+ if (typeof body.value !== "number" || !Number.isFinite(body.value)) {
505
1187
  return jsonResponse({ error: "value must be a positive number" }, 400);
506
1188
  }
1189
+ const normalizedValue = Math.floor(body.value);
1190
+ if (normalizedValue < 1) {
1191
+ return jsonResponse({ error: "value must be a positive number" }, 400);
1192
+ }
1193
+ if (body.setAll !== undefined && typeof body.setAll !== "boolean") {
1194
+ return jsonResponse({ error: "setAll must be a boolean" }, 400);
1195
+ }
507
1196
  const affected = Object.keys(providerContextCaps(config));
508
- setGlobalContextCapValue(config, body.value);
1197
+ const applyToAll = body.setAll === true;
1198
+ setGlobalContextCapValue(config, normalizedValue, applyToAll);
509
1199
  save(config);
510
1200
  reconcileLiveStateStores();
511
- for (const provider of affected) clearModelCache(provider);
512
- await refreshCodexCatalogBestEffort();
513
- return respond();
1201
+ if (applyToAll) {
1202
+ for (const provider of affected) clearModelCache(provider);
1203
+ }
1204
+ const catalogRefresh = await convergeCodexCatalog();
1205
+ return respond(catalogRefresh);
514
1206
  }
515
1207
 
516
- // Branch 2: enable/clear the cap for every provider at once.
1208
+ // Branch 3: enable/clear the cap for every provider at once.
517
1209
  if (body.setAll !== undefined) {
518
1210
  if (typeof body.setAll !== "boolean") {
519
1211
  return jsonResponse({ error: "setAll must be a boolean" }, 400);
@@ -524,27 +1216,12 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
524
1216
  save(config);
525
1217
  reconcileLiveStateStores();
526
1218
  for (const provider of new Set([...before, ...names])) clearModelCache(provider);
527
- await refreshCodexCatalogBestEffort();
528
- return respond();
1219
+ const catalogRefresh = await convergeCodexCatalog();
1220
+ return respond(catalogRefresh);
529
1221
  }
530
1222
 
531
- // Branch 3: existing per-provider toggle (enable writes the current global value).
532
- if (typeof body.provider !== "string" || typeof body.enabled !== "boolean") {
533
- return jsonResponse({ error: "provider string and enabled boolean are required" }, 400);
534
- }
535
- const provider = body.provider.trim();
536
- if (!isValidProviderName(provider)) {
537
- return jsonResponse({ error: "provider name must use letters, numbers, dot, underscore, or hyphen and cannot be a reserved object key" }, 400);
538
- }
539
- if (!hasOwnProvider(config.providers, provider)) {
540
- return jsonResponse({ error: "unknown provider" }, 404);
541
- }
542
- setProviderContextCap(config, provider, body.enabled);
543
- save(config);
544
- reconcileLiveStateStores();
545
- clearModelCache(provider);
546
- await refreshCodexCatalogBestEffort();
547
- return respond();
1223
+ // Unrecognized payload: reject rather than silently succeeding.
1224
+ return jsonResponse({ error: "provider string and enabled boolean are required" }, 400);
548
1225
  }
549
1226
 
550
1227
  // Complete GUI picker presets, derived from the canonical provider registry. The GUI is a