@yansigit/opencodex 2.31.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (826) hide show
  1. package/AGENTS_INSTALL.md +109 -0
  2. package/LICENSE +21 -0
  3. package/README.md +303 -0
  4. package/assets/architecture.png +0 -0
  5. package/assets/banner.png +0 -0
  6. package/assets/claude-code-models.gif +0 -0
  7. package/assets/codex-app-picker.png +0 -0
  8. package/bin/ocx.mjs +587 -0
  9. package/bin/package-main.mjs +9 -0
  10. package/gui/dist/assets/index-BNJ7r4Gd.js +102 -0
  11. package/gui/dist/assets/index-CGoDO3uO.css +1 -0
  12. package/gui/dist/favicon.png +0 -0
  13. package/gui/dist/icons.svg +24 -0
  14. package/gui/dist/index.html +25 -0
  15. package/gui/dist/logo.png +0 -0
  16. package/gui/dist/provider-icons/alibaba-color.svg +1 -0
  17. package/gui/dist/provider-icons/antigravity-color.svg +1 -0
  18. package/gui/dist/provider-icons/claude-color.svg +1 -0
  19. package/gui/dist/provider-icons/cline-color.svg +16 -0
  20. package/gui/dist/provider-icons/cloudflare-ai-gateway-color.svg +1 -0
  21. package/gui/dist/provider-icons/commandcode-color.svg +1 -0
  22. package/gui/dist/provider-icons/copilot-color.svg +1 -0
  23. package/gui/dist/provider-icons/cursor-color.svg +2 -0
  24. package/gui/dist/provider-icons/deepseek-color.svg +1 -0
  25. package/gui/dist/provider-icons/discord.svg +1 -0
  26. package/gui/dist/provider-icons/firepass-color.svg +1 -0
  27. package/gui/dist/provider-icons/fireworks-color.svg +1 -0
  28. package/gui/dist/provider-icons/gemini-color.svg +1 -0
  29. package/gui/dist/provider-icons/github-copilot-color.svg +1 -0
  30. package/gui/dist/provider-icons/gitlab-duo-color.svg +1 -0
  31. package/gui/dist/provider-icons/grok.svg +1 -0
  32. package/gui/dist/provider-icons/groq-color.svg +1 -0
  33. package/gui/dist/provider-icons/huggingface-color.svg +1 -0
  34. package/gui/dist/provider-icons/kimi-color.svg +1 -0
  35. package/gui/dist/provider-icons/kiro-color.svg +15 -0
  36. package/gui/dist/provider-icons/lm-studio-color.svg +1 -0
  37. package/gui/dist/provider-icons/mistral-color.svg +1 -0
  38. package/gui/dist/provider-icons/moonshot-color.svg +1 -0
  39. package/gui/dist/provider-icons/nvidia-color.svg +1 -0
  40. package/gui/dist/provider-icons/ollama-color.svg +1 -0
  41. package/gui/dist/provider-icons/openai.svg +1 -0
  42. package/gui/dist/provider-icons/opencode.svg +2 -0
  43. package/gui/dist/provider-icons/openrouter-color.svg +1 -0
  44. package/gui/dist/provider-icons/pi.svg +21 -0
  45. package/gui/dist/provider-icons/qianfan-color.svg +1 -0
  46. package/gui/dist/provider-icons/qwen-portal-color.svg +1 -0
  47. package/gui/dist/provider-icons/telegram.svg +1 -0
  48. package/gui/dist/provider-icons/vercel-ai-gateway-color.svg +1 -0
  49. package/gui/dist/provider-icons/vllm-color.svg +1 -0
  50. package/gui/dist/provider-icons/xiaomi-color.svg +1 -0
  51. package/package.json +108 -0
  52. package/src/AGENTS.md +28 -0
  53. package/src/adapters/anthropic-image-guard.ts +251 -0
  54. package/src/adapters/anthropic-image-normalize.ts +518 -0
  55. package/src/adapters/anthropic-output-schema.ts +137 -0
  56. package/src/adapters/anthropic.ts +1327 -0
  57. package/src/adapters/azure.ts +36 -0
  58. package/src/adapters/base.ts +121 -0
  59. package/src/adapters/client-fingerprint.ts +65 -0
  60. package/src/adapters/cline-pass-deepseek-v4-tool-replay.ts +69 -0
  61. package/src/adapters/command-code.ts +601 -0
  62. package/src/adapters/cursor/arg-codec.ts +38 -0
  63. package/src/adapters/cursor/arg-normalize.ts +104 -0
  64. package/src/adapters/cursor/checkpoint-store.ts +303 -0
  65. package/src/adapters/cursor/cursor-errors.ts +288 -0
  66. package/src/adapters/cursor/discovery.ts +333 -0
  67. package/src/adapters/cursor/effort-map.ts +151 -0
  68. package/src/adapters/cursor/exec-policy.ts +88 -0
  69. package/src/adapters/cursor/framing.ts +250 -0
  70. package/src/adapters/cursor/gen/agent_pb.ts +15274 -0
  71. package/src/adapters/cursor/h2-pool.ts +123 -0
  72. package/src/adapters/cursor/http1-bidi.ts +361 -0
  73. package/src/adapters/cursor/images.ts +704 -0
  74. package/src/adapters/cursor/kv-store.ts +52 -0
  75. package/src/adapters/cursor/live-models.ts +269 -0
  76. package/src/adapters/cursor/live-smoke-gate.ts +41 -0
  77. package/src/adapters/cursor/live-transport.ts +1653 -0
  78. package/src/adapters/cursor/mcp-config.ts +42 -0
  79. package/src/adapters/cursor/mcp-manager.ts +333 -0
  80. package/src/adapters/cursor/message-mapper.ts +49 -0
  81. package/src/adapters/cursor/native-exec-common.ts +76 -0
  82. package/src/adapters/cursor/native-exec-desktop.ts +184 -0
  83. package/src/adapters/cursor/native-exec-fs.ts +332 -0
  84. package/src/adapters/cursor/native-exec-mcp.ts +153 -0
  85. package/src/adapters/cursor/native-exec-network.ts +43 -0
  86. package/src/adapters/cursor/native-exec-shell.ts +547 -0
  87. package/src/adapters/cursor/native-exec-tools.ts +118 -0
  88. package/src/adapters/cursor/native-exec.ts +663 -0
  89. package/src/adapters/cursor/protobuf-events.ts +1381 -0
  90. package/src/adapters/cursor/protobuf-request.ts +1032 -0
  91. package/src/adapters/cursor/request-builder.ts +461 -0
  92. package/src/adapters/cursor/thread-continuity.ts +67 -0
  93. package/src/adapters/cursor/tool-definitions.ts +735 -0
  94. package/src/adapters/cursor/tool-result-normalize.ts +92 -0
  95. package/src/adapters/cursor/transport-retry.ts +132 -0
  96. package/src/adapters/cursor/transport.ts +79 -0
  97. package/src/adapters/cursor/types.ts +79 -0
  98. package/src/adapters/cursor.ts +322 -0
  99. package/src/adapters/google-antigravity-hosts.ts +48 -0
  100. package/src/adapters/google-antigravity-replay.ts +827 -0
  101. package/src/adapters/google-antigravity-tools.ts +105 -0
  102. package/src/adapters/google-antigravity-wire.ts +141 -0
  103. package/src/adapters/google-errors.ts +92 -0
  104. package/src/adapters/google-http.ts +460 -0
  105. package/src/adapters/google-tool-schema.ts +238 -0
  106. package/src/adapters/google-truncation.ts +24 -0
  107. package/src/adapters/google-wire-compiler.ts +232 -0
  108. package/src/adapters/google.ts +1377 -0
  109. package/src/adapters/identity.ts +77 -0
  110. package/src/adapters/image.ts +23 -0
  111. package/src/adapters/kiro-constants.ts +16 -0
  112. package/src/adapters/kiro-errors.ts +208 -0
  113. package/src/adapters/kiro-events.ts +197 -0
  114. package/src/adapters/kiro-images.ts +129 -0
  115. package/src/adapters/kiro-retry.ts +312 -0
  116. package/src/adapters/kiro-thinking.ts +112 -0
  117. package/src/adapters/kiro-tool-fallback.ts +36 -0
  118. package/src/adapters/kiro-tools.ts +224 -0
  119. package/src/adapters/kiro-truncation.ts +33 -0
  120. package/src/adapters/kiro-wire.ts +129 -0
  121. package/src/adapters/kiro.ts +1936 -0
  122. package/src/adapters/mimo-free.ts +280 -0
  123. package/src/adapters/openai-chat-url.ts +11 -0
  124. package/src/adapters/openai-chat.ts +1980 -0
  125. package/src/adapters/openai-responses-url.ts +16 -0
  126. package/src/adapters/openai-responses.ts +1911 -0
  127. package/src/adapters/registry.ts +175 -0
  128. package/src/adapters/responses-tool-schema.ts +67 -0
  129. package/src/adapters/run-turn-queue.ts +114 -0
  130. package/src/adapters/tool-call-id.ts +119 -0
  131. package/src/adapters/tool-catalog-nudge.ts +154 -0
  132. package/src/adapters/upstream-http-error.ts +48 -0
  133. package/src/adapters/xai-web-search.ts +185 -0
  134. package/src/bridge.ts +1986 -0
  135. package/src/chat/inbound.ts +319 -0
  136. package/src/chat/outbound.ts +821 -0
  137. package/src/claude/agents-inject.ts +266 -0
  138. package/src/claude/alias.ts +149 -0
  139. package/src/claude/auth-detect.ts +229 -0
  140. package/src/claude/auth-mode-migration.ts +32 -0
  141. package/src/claude/auth-mode.ts +62 -0
  142. package/src/claude/context-windows.ts +205 -0
  143. package/src/claude/desktop-3p-guard.ts +35 -0
  144. package/src/claude/desktop-3p-paths.ts +84 -0
  145. package/src/claude/desktop-3p.ts +615 -0
  146. package/src/claude/desktop-health.ts +26 -0
  147. package/src/claude/desktop-profile.ts +263 -0
  148. package/src/claude/gateway-cache.ts +107 -0
  149. package/src/claude/inbound-debug.ts +163 -0
  150. package/src/claude/inbound.ts +578 -0
  151. package/src/claude/model-info.ts +174 -0
  152. package/src/claude/outbound.ts +926 -0
  153. package/src/cli/access.ts +108 -0
  154. package/src/cli/account-api.ts +302 -0
  155. package/src/cli/account-auth.ts +250 -0
  156. package/src/cli/account-catalog-refresh.ts +14 -0
  157. package/src/cli/account-extended.ts +737 -0
  158. package/src/cli/account-main.ts +317 -0
  159. package/src/cli/account.ts +299 -0
  160. package/src/cli/agent-driven.ts +70 -0
  161. package/src/cli/agent.ts +290 -0
  162. package/src/cli/catalog-prewarm.ts +27 -0
  163. package/src/cli/claude-agent-startup-sync.ts +73 -0
  164. package/src/cli/claude-desktop.ts +213 -0
  165. package/src/cli/claude.ts +355 -0
  166. package/src/cli/codex-log-guard-doctor.ts +103 -0
  167. package/src/cli/codex-shim-autorestore.ts +47 -0
  168. package/src/cli/codex-shim-readiness.ts +76 -0
  169. package/src/cli/combo.ts +127 -0
  170. package/src/cli/config-command.ts +209 -0
  171. package/src/cli/debug.ts +228 -0
  172. package/src/cli/dispatch.ts +585 -0
  173. package/src/cli/doctor.ts +1202 -0
  174. package/src/cli/ensure-desired-integrations.ts +152 -0
  175. package/src/cli/export-command.ts +213 -0
  176. package/src/cli/help.ts +101 -0
  177. package/src/cli/index.ts +973 -0
  178. package/src/cli/init.ts +211 -0
  179. package/src/cli/integrations.ts +260 -0
  180. package/src/cli/interactive-confirm.ts +133 -0
  181. package/src/cli/lab.ts +607 -0
  182. package/src/cli/launcher-context.ts +77 -0
  183. package/src/cli/minimax.ts +497 -0
  184. package/src/cli/models-runtime.ts +245 -0
  185. package/src/cli/models.ts +422 -0
  186. package/src/cli/observe.ts +206 -0
  187. package/src/cli/opencode.ts +588 -0
  188. package/src/cli/provider-replit.ts +232 -0
  189. package/src/cli/provider-runtime.ts +179 -0
  190. package/src/cli/provider.ts +492 -0
  191. package/src/cli/ready.ts +301 -0
  192. package/src/cli/registry.ts +422 -0
  193. package/src/cli/replit-gateway-key-input.ts +138 -0
  194. package/src/cli/root.ts +86 -0
  195. package/src/cli/route-policy.ts +92 -0
  196. package/src/cli/runtime-api.ts +328 -0
  197. package/src/cli/star-prompt.ts +211 -0
  198. package/src/cli/status-oauth.ts +78 -0
  199. package/src/cli/status.ts +328 -0
  200. package/src/cli/system-command.ts +112 -0
  201. package/src/cli/system-restart-client.ts +146 -0
  202. package/src/cli/tray-proxy.ts +199 -0
  203. package/src/cli/v2.ts +268 -0
  204. package/src/cli.ts +10 -0
  205. package/src/clients/config-export.ts +1704 -0
  206. package/src/codex/account-id.ts +34 -0
  207. package/src/codex/account-label.ts +47 -0
  208. package/src/codex/account-lifecycle.ts +172 -0
  209. package/src/codex/account-namespace-match.ts +63 -0
  210. package/src/codex/account-namespaces.ts +195 -0
  211. package/src/codex/account-pause.ts +20 -0
  212. package/src/codex/account-priority.ts +83 -0
  213. package/src/codex/account-runtime-state.ts +31 -0
  214. package/src/codex/account-store.ts +544 -0
  215. package/src/codex/account-usability.ts +43 -0
  216. package/src/codex/admission.ts +256 -0
  217. package/src/codex/affinity-debug.ts +162 -0
  218. package/src/codex/agent-roles-sync.ts +225 -0
  219. package/src/codex/agent-roles.ts +238 -0
  220. package/src/codex/app-server-processes.ts +1143 -0
  221. package/src/codex/app-server-restart-service.ts +232 -0
  222. package/src/codex/auth-api.ts +2147 -0
  223. package/src/codex/auth-collision.ts +109 -0
  224. package/src/codex/auth-context.ts +665 -0
  225. package/src/codex/autostart-health.ts +156 -0
  226. package/src/codex/catalog/account-models.ts +67 -0
  227. package/src/codex/catalog/aggregation.ts +436 -0
  228. package/src/codex/catalog/bundled.ts +549 -0
  229. package/src/codex/catalog/effort.ts +446 -0
  230. package/src/codex/catalog/filesystem-evidence.ts +302 -0
  231. package/src/codex/catalog/kinds.ts +2 -0
  232. package/src/codex/catalog/metadata.ts +664 -0
  233. package/src/codex/catalog/native-models.ts +72 -0
  234. package/src/codex/catalog/parsing.ts +650 -0
  235. package/src/codex/catalog/provider-fetch.ts +2064 -0
  236. package/src/codex/catalog/sync.ts +1883 -0
  237. package/src/codex/catalog-admission.ts +199 -0
  238. package/src/codex/catalog-refresh-status.ts +105 -0
  239. package/src/codex/catalog-write-serialization.ts +242 -0
  240. package/src/codex/catalog.ts +14 -0
  241. package/src/codex/codex-write-lock.ts +384 -0
  242. package/src/codex/convergence-types.ts +614 -0
  243. package/src/codex/convergence.ts +651 -0
  244. package/src/codex/coordinator-doctor.ts +332 -0
  245. package/src/codex/custom-model-catalog-migration.ts +176 -0
  246. package/src/codex/data/upstream-models.json +830 -0
  247. package/src/codex/desired-state.ts +230 -0
  248. package/src/codex/exec-invocation.ts +22 -0
  249. package/src/codex/features.ts +1566 -0
  250. package/src/codex/generation.ts +202 -0
  251. package/src/codex/history-job.ts +407 -0
  252. package/src/codex/history-lock.ts +242 -0
  253. package/src/codex/history-migration-guardian.ts +108 -0
  254. package/src/codex/history-provider.ts +979 -0
  255. package/src/codex/history-transition.ts +105 -0
  256. package/src/codex/history-worker.ts +220 -0
  257. package/src/codex/home.ts +206 -0
  258. package/src/codex/inject-coordination.ts +290 -0
  259. package/src/codex/inject.ts +1733 -0
  260. package/src/codex/injected-marker.ts +106 -0
  261. package/src/codex/integration-record.ts +266 -0
  262. package/src/codex/internal/catalog-writer.ts +203 -0
  263. package/src/codex/internal/history-writer.ts +80 -0
  264. package/src/codex/journal.ts +225 -0
  265. package/src/codex/log-guard/inspect.ts +506 -0
  266. package/src/codex/log-guard/lock.ts +150 -0
  267. package/src/codex/log-guard/maintenance.ts +403 -0
  268. package/src/codex/log-guard/path-safety.ts +88 -0
  269. package/src/codex/log-guard/policy.ts +44 -0
  270. package/src/codex/log-guard/processes.ts +205 -0
  271. package/src/codex/log-guard/protection.ts +489 -0
  272. package/src/codex/log-guard/sqlite-errors.ts +9 -0
  273. package/src/codex/main-account-cache.ts +56 -0
  274. package/src/codex/main-account.ts +68 -0
  275. package/src/codex/management-convergence.ts +167 -0
  276. package/src/codex/model-cache.ts +273 -0
  277. package/src/codex/model-entitlements.ts +353 -0
  278. package/src/codex/native-main-admission.ts +47 -0
  279. package/src/codex/native-main-auth-temp.ts +187 -0
  280. package/src/codex/native-main-claim.ts +178 -0
  281. package/src/codex/native-main-lock-file.ts +162 -0
  282. package/src/codex/native-main-owner.ts +329 -0
  283. package/src/codex/native-profile-api.ts +247 -0
  284. package/src/codex/native-profile-manager.ts +1531 -0
  285. package/src/codex/native-profile-processes.ts +121 -0
  286. package/src/codex/native-profile-recovery.ts +99 -0
  287. package/src/codex/native-profile-stage-store.ts +387 -0
  288. package/src/codex/native-profile-startup.ts +492 -0
  289. package/src/codex/native-profile-store.ts +855 -0
  290. package/src/codex/native-profile-types.ts +120 -0
  291. package/src/codex/native-residue.ts +682 -0
  292. package/src/codex/paths.ts +144 -0
  293. package/src/codex/plan-from-token.ts +140 -0
  294. package/src/codex/plan.ts +40 -0
  295. package/src/codex/plugins-doctor.ts +242 -0
  296. package/src/codex/pool-rotation.ts +295 -0
  297. package/src/codex/project-config-warnings.ts +425 -0
  298. package/src/codex/prompt-journal.ts +352 -0
  299. package/src/codex/prompt-layers.ts +967 -0
  300. package/src/codex/prompt-lock.ts +143 -0
  301. package/src/codex/quota-rejection.ts +298 -0
  302. package/src/codex/quota.ts +573 -0
  303. package/src/codex/refresh.ts +62 -0
  304. package/src/codex/reset-credit-recovery.ts +1044 -0
  305. package/src/codex/routing.ts +1888 -0
  306. package/src/codex/runtime.ts +659 -0
  307. package/src/codex/shim.ts +2170 -0
  308. package/src/codex/subagent-defaults.ts +550 -0
  309. package/src/codex/subagent-model-fallback.ts +784 -0
  310. package/src/codex/sync.ts +319 -0
  311. package/src/codex/transition-state.ts +612 -0
  312. package/src/codex/upstream-host-health.ts +368 -0
  313. package/src/codex/user-identity.ts +557 -0
  314. package/src/codex/warmup.ts +298 -0
  315. package/src/codex/websocket-registry.ts +100 -0
  316. package/src/codex/write-coordination.ts +114 -0
  317. package/src/combos/failover.ts +160 -0
  318. package/src/combos/index.ts +45 -0
  319. package/src/combos/request.ts +94 -0
  320. package/src/combos/resolve.ts +232 -0
  321. package/src/combos/types.ts +398 -0
  322. package/src/config/provider-name.ts +24 -0
  323. package/src/config.ts +4041 -0
  324. package/src/fork/register.ts +3 -0
  325. package/src/generated/compatibility-version.json +3116 -0
  326. package/src/generated/model-metadata.ts +106 -0
  327. package/src/github/star-state.ts +203 -0
  328. package/src/grok/inject.ts +530 -0
  329. package/src/grok/inspect.ts +45 -0
  330. package/src/grok/status.ts +121 -0
  331. package/src/grok/sync.ts +66 -0
  332. package/src/images/artifacts.ts +516 -0
  333. package/src/images/fulfill-video.ts +163 -0
  334. package/src/images/fulfill.ts +149 -0
  335. package/src/images/index.ts +4 -0
  336. package/src/images/loop.ts +955 -0
  337. package/src/images/plan.ts +143 -0
  338. package/src/images/synthetic-tool.ts +133 -0
  339. package/src/images/types.ts +41 -0
  340. package/src/images/xai-client.ts +141 -0
  341. package/src/images/xai-video-client.ts +163 -0
  342. package/src/index.ts +22 -0
  343. package/src/integrations/config-io.ts +269 -0
  344. package/src/integrations/journal.ts +315 -0
  345. package/src/integrations/merge.ts +135 -0
  346. package/src/integrations/mutation-flight.ts +71 -0
  347. package/src/integrations/native/ownership-preflight.ts +202 -0
  348. package/src/integrations/omp-yaml-source.ts +358 -0
  349. package/src/integrations/owned-refresh.ts +74 -0
  350. package/src/integrations/ownership.ts +111 -0
  351. package/src/integrations/registry.ts +159 -0
  352. package/src/integrations/serialize.ts +314 -0
  353. package/src/integrations/state.ts +361 -0
  354. package/src/integrations/store.ts +103 -0
  355. package/src/integrations/writer-lock.ts +98 -0
  356. package/src/integrations/writer.ts +691 -0
  357. package/src/lab/artifacts/sanitize.ts +586 -0
  358. package/src/lab/artifacts/secure-fs.ts +475 -0
  359. package/src/lab/artifacts/store.ts +310 -0
  360. package/src/lab/automation/budgets.ts +78 -0
  361. package/src/lab/automation/config-persistence.ts +256 -0
  362. package/src/lab/automation/constants.ts +39 -0
  363. package/src/lab/automation/cooldown.ts +103 -0
  364. package/src/lab/automation/dispatch.ts +211 -0
  365. package/src/lab/automation/index.ts +13 -0
  366. package/src/lab/automation/orchestrator.ts +499 -0
  367. package/src/lab/automation/persistence.ts +512 -0
  368. package/src/lab/automation/planner.ts +371 -0
  369. package/src/lab/automation/policy.ts +136 -0
  370. package/src/lab/automation/queue.ts +191 -0
  371. package/src/lab/automation/recovery.ts +24 -0
  372. package/src/lab/automation/route-context.ts +21 -0
  373. package/src/lab/automation/run-key.ts +44 -0
  374. package/src/lab/automation/runs-query.ts +34 -0
  375. package/src/lab/automation/types.ts +160 -0
  376. package/src/lab/conformance/assertion.ts +325 -0
  377. package/src/lab/conformance/digest.ts +22 -0
  378. package/src/lab/conformance/executor.ts +741 -0
  379. package/src/lab/conformance/fixture-provider.ts +27 -0
  380. package/src/lab/conformance/fixtures/live-v1-cases.json +175 -0
  381. package/src/lab/conformance/fixtures/protocol-v1-cases.json +461 -0
  382. package/src/lab/conformance/harness-budget.ts +47 -0
  383. package/src/lab/conformance/index.ts +5 -0
  384. package/src/lab/conformance/jcs.ts +64 -0
  385. package/src/lab/conformance/json-pointer.ts +39 -0
  386. package/src/lab/conformance/manifest.ts +180 -0
  387. package/src/lab/conformance/mcp-stub.ts +179 -0
  388. package/src/lab/conformance/negative-controls.ts +164 -0
  389. package/src/lab/conformance/observation.ts +355 -0
  390. package/src/lab/conformance/runner.ts +68 -0
  391. package/src/lab/conformance/sse-normalize.ts +59 -0
  392. package/src/lab/conformance/suite-manifest.ts +78 -0
  393. package/src/lab/conformance/types.ts +214 -0
  394. package/src/lab/constants.ts +126 -0
  395. package/src/lab/digest.ts +64 -0
  396. package/src/lab/events/errors.ts +9 -0
  397. package/src/lab/events/limits.ts +117 -0
  398. package/src/lab/events/types.ts +229 -0
  399. package/src/lab/events/validate.ts +781 -0
  400. package/src/lab/fabric/constants.ts +40 -0
  401. package/src/lab/fabric/executor.ts +492 -0
  402. package/src/lab/fabric/index.ts +80 -0
  403. package/src/lab/fabric/manifest.ts +222 -0
  404. package/src/lab/fabric/observe.ts +489 -0
  405. package/src/lab/fabric/patch.ts +79 -0
  406. package/src/lab/fabric/producer-child.ts +139 -0
  407. package/src/lab/fabric/producer-isolate.ts +276 -0
  408. package/src/lab/fabric/producer-protocol.ts +61 -0
  409. package/src/lab/fabric/scratch.ts +439 -0
  410. package/src/lab/fabric/subject.ts +106 -0
  411. package/src/lab/fabric/types.ts +134 -0
  412. package/src/lab/fabric/verifier.ts +98 -0
  413. package/src/lab/index.ts +54 -0
  414. package/src/lab/ledger/artifact-refs.ts +127 -0
  415. package/src/lab/ledger/invalidation.ts +136 -0
  416. package/src/lab/ledger/purge.ts +310 -0
  417. package/src/lab/ledger/store.ts +532 -0
  418. package/src/lab/live/credential-lease.ts +53 -0
  419. package/src/lab/live/destination.ts +155 -0
  420. package/src/lab/live/executor.ts +336 -0
  421. package/src/lab/live/inert-tools.ts +56 -0
  422. package/src/lab/live/manifest.ts +85 -0
  423. package/src/lab/live/mcp-loopback.ts +57 -0
  424. package/src/lab/live/runner.ts +19 -0
  425. package/src/lab/live/sandbox.ts +61 -0
  426. package/src/lab/live/suite-manifest.ts +41 -0
  427. package/src/lab/live/transport.ts +118 -0
  428. package/src/lab/live/types.ts +197 -0
  429. package/src/lab/observe/from-conformance.ts +301 -0
  430. package/src/lab/observe/from-live.ts +117 -0
  431. package/src/lab/paths.ts +153 -0
  432. package/src/lab/projection/rebuild.ts +495 -0
  433. package/src/lab/projection/schema.ts +135 -0
  434. package/src/lab/projection/verdicts.ts +474 -0
  435. package/src/lab/projection/verification.ts +412 -0
  436. package/src/lab/public/bundle.ts +217 -0
  437. package/src/lab/public/community-authority.ts +175 -0
  438. package/src/lab/public/community-files.ts +29 -0
  439. package/src/lab/public/community.ts +479 -0
  440. package/src/lab/public/file-safety.ts +155 -0
  441. package/src/lab/public/ids.ts +26 -0
  442. package/src/lab/public/index.ts +16 -0
  443. package/src/lab/public/mutation-lock.ts +424 -0
  444. package/src/lab/public/operator.ts +353 -0
  445. package/src/lab/public/origin-purge.ts +79 -0
  446. package/src/lab/public/origin.ts +203 -0
  447. package/src/lab/public/privacy.ts +143 -0
  448. package/src/lab/public/private-file.ts +261 -0
  449. package/src/lab/public/project.ts +124 -0
  450. package/src/lab/public/purge-test-fault.ts +21 -0
  451. package/src/lab/public/purge.ts +223 -0
  452. package/src/lab/public/registry.ts +44 -0
  453. package/src/lab/public/revocation.ts +252 -0
  454. package/src/lab/public/signature.ts +243 -0
  455. package/src/lab/public/storage.ts +105 -0
  456. package/src/lab/public/strict-json.ts +206 -0
  457. package/src/lab/public/time.ts +26 -0
  458. package/src/lab/public/types.ts +172 -0
  459. package/src/lab/public/validate.ts +391 -0
  460. package/src/lab/query/catalog.ts +101 -0
  461. package/src/lab/query/connection.ts +107 -0
  462. package/src/lab/query/constants.ts +4 -0
  463. package/src/lab/query/cursor.ts +132 -0
  464. package/src/lab/query/dto-map.ts +277 -0
  465. package/src/lab/query/errors.ts +22 -0
  466. package/src/lab/query/freshness.ts +53 -0
  467. package/src/lab/query/index.ts +45 -0
  468. package/src/lab/query/latest-observation.ts +59 -0
  469. package/src/lab/query/passive-production.ts +159 -0
  470. package/src/lab/query/queries.ts +444 -0
  471. package/src/lab/query/types.ts +266 -0
  472. package/src/lab/subject/behavior-fingerprint.ts +77 -0
  473. package/src/lab/subject/installation-salt.ts +112 -0
  474. package/src/lab/subject/protocol-subject.ts +80 -0
  475. package/src/lab/subject/route-subject.ts +74 -0
  476. package/src/lib/abort.ts +146 -0
  477. package/src/lib/admin-secrets.ts +25 -0
  478. package/src/lib/admission.ts +83 -0
  479. package/src/lib/app-owned-memory-stores.ts +195 -0
  480. package/src/lib/app-owned-memory.ts +265 -0
  481. package/src/lib/bounded-body.ts +346 -0
  482. package/src/lib/bun-binary-validator.d.mts +3 -0
  483. package/src/lib/bun-binary-validator.mjs +18 -0
  484. package/src/lib/bun-runtime.ts +184 -0
  485. package/src/lib/bun-stream-caps.ts +130 -0
  486. package/src/lib/codex-restart-contract.ts +120 -0
  487. package/src/lib/config-ownership.ts +364 -0
  488. package/src/lib/crash-guard.ts +344 -0
  489. package/src/lib/debug-log-buffer.ts +83 -0
  490. package/src/lib/debug-settings.ts +108 -0
  491. package/src/lib/debug.ts +31 -0
  492. package/src/lib/destination-policy.ts +380 -0
  493. package/src/lib/errors.ts +406 -0
  494. package/src/lib/eventstream-decoder.ts +253 -0
  495. package/src/lib/fabric-task-execution-authority.ts +7 -0
  496. package/src/lib/fabric-task-host.ts +29 -0
  497. package/src/lib/gcp-adc.ts +341 -0
  498. package/src/lib/injection-debug-log.ts +58 -0
  499. package/src/lib/lab-activation.ts +223 -0
  500. package/src/lib/lab-live-execution-authority.ts +13 -0
  501. package/src/lib/lab-live-host.ts +30 -0
  502. package/src/lib/lab-live-pinned-sender.ts +56 -0
  503. package/src/lib/lab-live-route-production.ts +130 -0
  504. package/src/lib/lab-passive-linker-registration.ts +26 -0
  505. package/src/lib/local-management-attestation.ts +51 -0
  506. package/src/lib/local-management-capability.ts +100 -0
  507. package/src/lib/local-provider-reload-contract.ts +100 -0
  508. package/src/lib/open-url.ts +25 -0
  509. package/src/lib/optional-shutdown-hooks.ts +57 -0
  510. package/src/lib/pinned-http.ts +270 -0
  511. package/src/lib/privacy.ts +20 -0
  512. package/src/lib/process-control.ts +168 -0
  513. package/src/lib/provider-outbound.ts +210 -0
  514. package/src/lib/provider-url.ts +14 -0
  515. package/src/lib/proxy-env.ts +18 -0
  516. package/src/lib/redact.ts +521 -0
  517. package/src/lib/retry-after.ts +55 -0
  518. package/src/lib/self-launch-argv.ts +15 -0
  519. package/src/lib/server-resource-ownership.ts +71 -0
  520. package/src/lib/service-secrets.ts +25 -0
  521. package/src/lib/shadow-call.ts +61 -0
  522. package/src/lib/sidecar-tracker.ts +52 -0
  523. package/src/lib/sse-decoder.ts +364 -0
  524. package/src/lib/state-store-registrations.ts +119 -0
  525. package/src/lib/state-store-sweeper.ts +184 -0
  526. package/src/lib/system-restart-contract.ts +73 -0
  527. package/src/lib/test-home-guard.ts +90 -0
  528. package/src/lib/token-estimate.ts +86 -0
  529. package/src/lib/tool-argument-integers.ts +202 -0
  530. package/src/lib/translator-budget.ts +400 -0
  531. package/src/lib/upstream-http-version.ts +57 -0
  532. package/src/lib/upstream-reachability.ts +95 -0
  533. package/src/lib/upstream-retry.ts +392 -0
  534. package/src/lib/win-exec.ts +115 -0
  535. package/src/lib/win-paths.ts +68 -0
  536. package/src/lib/windows-atomic-replace.ts +156 -0
  537. package/src/lib/windows-elevation.ts +773 -0
  538. package/src/lib/windows-secret-acl.ts +854 -0
  539. package/src/lib/windows-service-wrappers.ts +72 -0
  540. package/src/lib/windows-text.ts +106 -0
  541. package/src/lib/windows-user-principal.ts +341 -0
  542. package/src/lib/winsw.ts +403 -0
  543. package/src/oauth/account-import/google-antigravity-adapter.ts +74 -0
  544. package/src/oauth/account-import/index.ts +15 -0
  545. package/src/oauth/account-import/parser.ts +83 -0
  546. package/src/oauth/account-import/registry.ts +18 -0
  547. package/src/oauth/account-import/service.ts +75 -0
  548. package/src/oauth/account-import/types.ts +91 -0
  549. package/src/oauth/anthropic-routing.ts +594 -0
  550. package/src/oauth/anthropic.ts +188 -0
  551. package/src/oauth/antigravity-routing.ts +151 -0
  552. package/src/oauth/callback-server.ts +300 -0
  553. package/src/oauth/chatgpt.ts +161 -0
  554. package/src/oauth/command-code.ts +239 -0
  555. package/src/oauth/cursor.ts +252 -0
  556. package/src/oauth/github-copilot.ts +428 -0
  557. package/src/oauth/google-antigravity.ts +262 -0
  558. package/src/oauth/health.ts +407 -0
  559. package/src/oauth/index.ts +1504 -0
  560. package/src/oauth/key-providers.ts +124 -0
  561. package/src/oauth/kimi.ts +227 -0
  562. package/src/oauth/kiro-credentials.ts +726 -0
  563. package/src/oauth/kiro.ts +621 -0
  564. package/src/oauth/local-token-detect.ts +130 -0
  565. package/src/oauth/log.ts +50 -0
  566. package/src/oauth/login-cli.ts +223 -0
  567. package/src/oauth/nous.ts +798 -0
  568. package/src/oauth/pkce.ts +15 -0
  569. package/src/oauth/store.ts +728 -0
  570. package/src/oauth/token-guardian.ts +309 -0
  571. package/src/oauth/types.ts +62 -0
  572. package/src/oauth/xai.ts +241 -0
  573. package/src/providers/alibaba-region-backup.ts +75 -0
  574. package/src/providers/alibaba-region-migration.ts +156 -0
  575. package/src/providers/alibaba-region-startup.ts +36 -0
  576. package/src/providers/antigravity-models.ts +695 -0
  577. package/src/providers/antigravity-quota.ts +216 -0
  578. package/src/providers/api-keys.ts +140 -0
  579. package/src/providers/base-url-choices.ts +74 -0
  580. package/src/providers/codex-capacity.ts +292 -0
  581. package/src/providers/command-code-efforts.ts +144 -0
  582. package/src/providers/context-cap.ts +82 -0
  583. package/src/providers/cursor-pool.ts +72 -0
  584. package/src/providers/derive.ts +586 -0
  585. package/src/providers/fastwire.ts +501 -0
  586. package/src/providers/free-directory.ts +187 -0
  587. package/src/providers/github-copilot-transport.ts +56 -0
  588. package/src/providers/google-vertex-location.ts +14 -0
  589. package/src/providers/key-failover.ts +271 -0
  590. package/src/providers/kiro-models.ts +67 -0
  591. package/src/providers/label.ts +19 -0
  592. package/src/providers/model-discovery-limits.ts +16 -0
  593. package/src/providers/model-discovery.ts +449 -0
  594. package/src/providers/model-rename-migration.ts +255 -0
  595. package/src/providers/model-rename-startup.ts +28 -0
  596. package/src/providers/openai-sidecar.ts +243 -0
  597. package/src/providers/openai-tier-startup.ts +56 -0
  598. package/src/providers/openai-tiers.ts +423 -0
  599. package/src/providers/openai-virtual-models.ts +83 -0
  600. package/src/providers/opencode-zen-rate-limit.ts +102 -0
  601. package/src/providers/openrouter-routing.ts +102 -0
  602. package/src/providers/provider-id-rewrite.ts +185 -0
  603. package/src/providers/quota.ts +2345 -0
  604. package/src/providers/registry.ts +2918 -0
  605. package/src/providers/replit/constants.ts +27 -0
  606. package/src/providers/replit/derive.ts +85 -0
  607. package/src/providers/replit/headers.ts +28 -0
  608. package/src/providers/replit/origin.ts +55 -0
  609. package/src/providers/replit/pair-install-response.ts +72 -0
  610. package/src/providers/replit/probe.ts +199 -0
  611. package/src/providers/replit/setup.ts +350 -0
  612. package/src/providers/request-pacing.ts +310 -0
  613. package/src/providers/service-tier.ts +277 -0
  614. package/src/providers/slug-codec.ts +103 -0
  615. package/src/providers/static-model-discovery.ts +86 -0
  616. package/src/providers/xai-responses-opt-in.ts +15 -0
  617. package/src/providers/xai-transport.ts +148 -0
  618. package/src/reasoning-effort.ts +183 -0
  619. package/src/responses/compaction.ts +142 -0
  620. package/src/responses/custom-tool-compat.ts +266 -0
  621. package/src/responses/hosted-tool-policy.ts +9 -0
  622. package/src/responses/namespace-tool-compat.ts +355 -0
  623. package/src/responses/parser.ts +838 -0
  624. package/src/responses/provider-continuation.ts +98 -0
  625. package/src/responses/provider-opaque-metadata.ts +73 -0
  626. package/src/responses/reasoning-envelope.ts +60 -0
  627. package/src/responses/reasoning-replay-cache.ts +426 -0
  628. package/src/responses/schema.ts +165 -0
  629. package/src/responses/spill-store.ts +459 -0
  630. package/src/responses/state.ts +1433 -0
  631. package/src/responses/thought-signature-replay.ts +347 -0
  632. package/src/responses/tool-groups.ts +19 -0
  633. package/src/responses/tool-search-compat.ts +301 -0
  634. package/src/responses/truncated-stop-reason.ts +60 -0
  635. package/src/router.ts +761 -0
  636. package/src/routing/analytics.ts +378 -0
  637. package/src/routing/capability.ts +244 -0
  638. package/src/routing/compatibility/assemble.ts +73 -0
  639. package/src/routing/compatibility/behavior.ts +278 -0
  640. package/src/routing/compatibility/catalog.ts +99 -0
  641. package/src/routing/compatibility/endpoint.ts +52 -0
  642. package/src/routing/compatibility/lab-evidence-provider.ts +130 -0
  643. package/src/routing/compatibility/policy.ts +181 -0
  644. package/src/routing/compatibility/provider-slot.ts +56 -0
  645. package/src/routing/compatibility/reader.ts +110 -0
  646. package/src/routing/compatibility/subject.ts +191 -0
  647. package/src/routing/compatibility/types.ts +64 -0
  648. package/src/routing/compatibility/version.ts +104 -0
  649. package/src/routing/cost.ts +77 -0
  650. package/src/routing/evaluator.ts +495 -0
  651. package/src/routing/health.ts +412 -0
  652. package/src/routing/history/cursor.ts +43 -0
  653. package/src/routing/history/indexer.ts +605 -0
  654. package/src/routing/history/schema.ts +72 -0
  655. package/src/routing/profile-namespace.ts +15 -0
  656. package/src/routing/profile.ts +547 -0
  657. package/src/routing/quota.ts +145 -0
  658. package/src/routing/request-evidence.ts +45 -0
  659. package/src/routing/trace.ts +776 -0
  660. package/src/server/adapter-resolve.ts +53 -0
  661. package/src/server/auth-cors.ts +751 -0
  662. package/src/server/background-lifecycle.ts +182 -0
  663. package/src/server/chat-completions.ts +442 -0
  664. package/src/server/chat-native-sse.ts +331 -0
  665. package/src/server/chat-native.ts +426 -0
  666. package/src/server/claude-messages.ts +1030 -0
  667. package/src/server/direct-local-http.ts +347 -0
  668. package/src/server/effort-policy.ts +190 -0
  669. package/src/server/github-copilot-responses-repair.ts +338 -0
  670. package/src/server/gui-static.ts +152 -0
  671. package/src/server/image-retry.ts +42 -0
  672. package/src/server/images.ts +568 -0
  673. package/src/server/index.ts +1813 -0
  674. package/src/server/lifecycle.ts +498 -0
  675. package/src/server/live.ts +717 -0
  676. package/src/server/local-management-read-client.ts +90 -0
  677. package/src/server/local-provider-reload-client.ts +137 -0
  678. package/src/server/management/agent-settings-routes.ts +1433 -0
  679. package/src/server/management/api-access.ts +141 -0
  680. package/src/server/management/api-key-usage.ts +193 -0
  681. package/src/server/management/body.ts +41 -0
  682. package/src/server/management/combo-routes.ts +263 -0
  683. package/src/server/management/config-routes.ts +835 -0
  684. package/src/server/management/context.ts +113 -0
  685. package/src/server/management/integration-routes.ts +498 -0
  686. package/src/server/management/lab-automation-routes.ts +206 -0
  687. package/src/server/management/lab-routes.ts +563 -0
  688. package/src/server/management/logs-usage-routes.ts +586 -0
  689. package/src/server/management/model-routes.ts +560 -0
  690. package/src/server/management/model-rows.ts +163 -0
  691. package/src/server/management/native-integration-routes.ts +769 -0
  692. package/src/server/management/oauth-account-routes.ts +637 -0
  693. package/src/server/management/provider-capability-config.ts +48 -0
  694. package/src/server/management/provider-routes.ts +1033 -0
  695. package/src/server/management/replit-provider-routes.ts +86 -0
  696. package/src/server/management/request-history-routes.ts +191 -0
  697. package/src/server/management/routing-analytics-routes.ts +74 -0
  698. package/src/server/management/routing-profile-routes.ts +385 -0
  699. package/src/server/management/shared.ts +286 -0
  700. package/src/server/management/sidebar-routes.ts +106 -0
  701. package/src/server/management/storage-log-guard-routes.ts +186 -0
  702. package/src/server/management/sync-response.ts +69 -0
  703. package/src/server/management/system-restart.ts +435 -0
  704. package/src/server/management/system-routes.ts +194 -0
  705. package/src/server/management/usage-summary-cache.ts +94 -0
  706. package/src/server/management/vision-sidecar-options.ts +167 -0
  707. package/src/server/management/web-search-sidecar-options.ts +120 -0
  708. package/src/server/management-api.ts +314 -0
  709. package/src/server/management-auth.ts +482 -0
  710. package/src/server/memory-watchdog.ts +156 -0
  711. package/src/server/passive-route-linker.ts +66 -0
  712. package/src/server/port-reclaim.ts +307 -0
  713. package/src/server/ports.ts +156 -0
  714. package/src/server/proxy-liveness.ts +328 -0
  715. package/src/server/readiness.ts +99 -0
  716. package/src/server/relay-eager.ts +353 -0
  717. package/src/server/relay.ts +1209 -0
  718. package/src/server/request-decompress.ts +239 -0
  719. package/src/server/request-log-conversation.ts +168 -0
  720. package/src/server/request-log.ts +1259 -0
  721. package/src/server/responses/agent-task-recovery-cache.ts +143 -0
  722. package/src/server/responses/agent-task-recovery.ts +465 -0
  723. package/src/server/responses/collaboration.ts +551 -0
  724. package/src/server/responses/compact.ts +771 -0
  725. package/src/server/responses/core.ts +5389 -0
  726. package/src/server/responses/empty-completion-guard.ts +276 -0
  727. package/src/server/responses/encrypted-payload.ts +331 -0
  728. package/src/server/responses/fetch-helpers.ts +232 -0
  729. package/src/server/responses/input-admission.ts +185 -0
  730. package/src/server/responses/pacing-overload.ts +13 -0
  731. package/src/server/responses/passthrough-error.ts +78 -0
  732. package/src/server/responses/policy-fallback.ts +178 -0
  733. package/src/server/responses/responses-field-backfill.ts +251 -0
  734. package/src/server/responses/terminal-guard.ts +251 -0
  735. package/src/server/responses/upstream-error.ts +53 -0
  736. package/src/server/responses/ws-upstream.ts +308 -0
  737. package/src/server/responses-custom-tool-repair.ts +282 -0
  738. package/src/server/responses-image-gen-repair.ts +132 -0
  739. package/src/server/responses-item-id-repair.ts +272 -0
  740. package/src/server/responses-json-events.ts +90 -0
  741. package/src/server/responses-model-rewrite.ts +29 -0
  742. package/src/server/responses-reasoning-summary-rewrite.ts +178 -0
  743. package/src/server/responses-snapshot-repair.ts +621 -0
  744. package/src/server/responses-terminal-repair.ts +342 -0
  745. package/src/server/responses-tool-search-repair.ts +267 -0
  746. package/src/server/responses-undeclared-tool-guard.ts +153 -0
  747. package/src/server/responses.ts +25 -0
  748. package/src/server/search.ts +201 -0
  749. package/src/server/sse-frame-buffer.ts +292 -0
  750. package/src/server/sse-payload-rewrite.ts +263 -0
  751. package/src/server/startup-action-control.ts +315 -0
  752. package/src/server/startup-health-cache.ts +131 -0
  753. package/src/server/system-env.ts +484 -0
  754. package/src/server/windows-tcp-drop.ts +184 -0
  755. package/src/server/windows-tray-control.ts +41 -0
  756. package/src/server/ws-bridge.ts +472 -0
  757. package/src/service-manager-probe.ts +892 -0
  758. package/src/service.ts +3575 -0
  759. package/src/sidecar/auth.ts +92 -0
  760. package/src/sidecar/candidates.ts +83 -0
  761. package/src/stall-timeout.ts +20 -0
  762. package/src/storage/cleanup-job.ts +57 -0
  763. package/src/storage/cleanup.ts +3085 -0
  764. package/src/storage/policy-job.ts +457 -0
  765. package/src/storage/policy-scheduler.ts +40 -0
  766. package/src/storage/policy-worker.ts +59 -0
  767. package/src/storage/policy.ts +527 -0
  768. package/src/storage/restore-job.ts +299 -0
  769. package/src/storage/restore-worker.ts +58 -0
  770. package/src/storage/scanner.ts +238 -0
  771. package/src/storage/storage-mutation-coordinator.ts +139 -0
  772. package/src/storage/worker-lifecycle.ts +215 -0
  773. package/src/tray/assets/opencodex-tray-offline.ico +0 -0
  774. package/src/tray/assets/opencodex-tray-online.ico +0 -0
  775. package/src/tray/assets/opencodex-tray-warning.ico +0 -0
  776. package/src/tray/assets/opencodex-tray.png +0 -0
  777. package/src/tray/windows-tray.ps1 +364 -0
  778. package/src/tray/windows.ts +757 -0
  779. package/src/types/accounts.ts +37 -0
  780. package/src/types/config.ts +876 -0
  781. package/src/types/provider.ts +545 -0
  782. package/src/types/request.ts +384 -0
  783. package/src/types/tools.ts +131 -0
  784. package/src/types/wire.ts +80 -0
  785. package/src/types.ts +106 -0
  786. package/src/update/badge.ts +72 -0
  787. package/src/update/index.ts +415 -0
  788. package/src/update/job.ts +1887 -0
  789. package/src/update/notify.ts +263 -0
  790. package/src/update/npm-cache-preflight.d.mts +47 -0
  791. package/src/update/npm-cache-preflight.mjs +201 -0
  792. package/src/update/npm-invocation.d.mts +23 -0
  793. package/src/update/npm-invocation.mjs +94 -0
  794. package/src/update/transactional-install.d.mts +22 -0
  795. package/src/update/transactional-install.mjs +259 -0
  796. package/src/update/tray-update-plan.d.mts +18 -0
  797. package/src/update/tray-update-plan.mjs +38 -0
  798. package/src/usage/cost.ts +625 -0
  799. package/src/usage/debug.ts +97 -0
  800. package/src/usage/expected-prices.ts +416 -0
  801. package/src/usage/log.ts +1223 -0
  802. package/src/usage/summary.ts +753 -0
  803. package/src/usage/totals.ts +14 -0
  804. package/src/usage/user-cost-overlay-reconciler.ts +313 -0
  805. package/src/usage/user-cost-overlays.ts +314 -0
  806. package/src/vision/anthropic-describe.ts +189 -0
  807. package/src/vision/backends.ts +97 -0
  808. package/src/vision/describe.ts +131 -0
  809. package/src/vision/eligibility.ts +250 -0
  810. package/src/vision/index.ts +681 -0
  811. package/src/vision/reasoning.ts +55 -0
  812. package/src/vision/routed-describe.ts +175 -0
  813. package/src/vision/timeout-bounds.ts +9 -0
  814. package/src/web-search/anthropic-executor.ts +195 -0
  815. package/src/web-search/backends.ts +108 -0
  816. package/src/web-search/exa-executor.ts +88 -0
  817. package/src/web-search/executor.ts +113 -0
  818. package/src/web-search/format-result.ts +89 -0
  819. package/src/web-search/gemini-executor.ts +141 -0
  820. package/src/web-search/index.ts +331 -0
  821. package/src/web-search/loop.ts +896 -0
  822. package/src/web-search/parse.ts +315 -0
  823. package/src/web-search/progress-stream.ts +342 -0
  824. package/src/web-search/sources.ts +60 -0
  825. package/src/web-search/synthetic-tool.ts +47 -0
  826. package/src/web-search/xai-executor.ts +219 -0
@@ -0,0 +1,1033 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { readFileSync } from "node:fs";
3
+ import type { CatalogModel } from "../../codex/catalog";
4
+ import { catalogModelSlug, invalidateCodexModelsCache, nativeModelRows, uniqueCatalogModelsForPublicList } from "../../codex/catalog";
5
+ import { clearGatherRoutedModelsInflight } from "../../codex/catalog/provider-fetch";
6
+ import {
7
+ DEFAULT_SUBAGENT_MODELS,
8
+ adoptPersistedProviderIntoLiveConfig,
9
+ codexAutoStartEnabled,
10
+ hasOwnProvider,
11
+ isValidProviderName,
12
+ multiAgentGuidanceEnabled,
13
+ nonBlankStringArrayConfigError,
14
+ normalizeNonBlankStringArray,
15
+ providerBaseUrlConfigError,
16
+ providerHeadersConfigError,
17
+ requestPacingConfigError,
18
+ readConfigAdmissionSnapshot,
19
+ saveConfigPreservingClaudeCode,
20
+ upstreamHttpVersionConfigError,
21
+ withConfigMutationLockSync,
22
+ } from "../../config";
23
+ import {
24
+ clearLoginState,
25
+ getLoginStatus,
26
+ isPublicOAuthProvider,
27
+ listOAuthProviders,
28
+ startLoginFlow,
29
+ submitManualLoginCode,
30
+ upsertOAuthProvider,
31
+ } from "../../oauth";
32
+ import { replaceProviderAccountSet } from "../../oauth/store";
33
+ import { providerDestinationResolvedError } from "../../lib/destination-policy";
34
+ import { reconcileLiveStateStores } from "../../lib/state-store-registrations";
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";
38
+ import { enrichProviderFromCatalog, listKeyLoginProviders } from "../../oauth/key-providers";
39
+ import { deriveProviderPresets } from "../../providers/derive";
40
+ import { effectiveGoogleMode, providerCodexAccountMode, providerMatchesRegistryTransport } from "../../providers/registry";
41
+ import {
42
+ extractModelEnvelopeRows,
43
+ extractProviderModelItems,
44
+ readBoundedDiscoveryJson,
45
+ resolveProviderModelDiscovery,
46
+ } from "../../providers/model-discovery";
47
+ import { routedSlug, slugEquals } from "../../providers/slug-codec";
48
+ import { clearAccountQuotaCache, clearProviderQuotaCache, fetchProviderQuotaReports } from "../../providers/quota";
49
+ import { clearKeyCooldowns } from "../../providers/key-failover";
50
+ import { providerRequestPacingStatus } from "../../providers/request-pacing";
51
+ import { CODEX_FORWARD_BASE_URL, isCanonicalOpenAiForwardProvider } from "../../providers/openai-tiers";
52
+ import { codexAccountNamespaceProviderCollisionError } from "../../codex/account-namespace-match";
53
+ import { clearThreadAccountMap } from "../../codex/routing";
54
+ import { primeCodexPoolQuotas } from "../../codex/auth-api";
55
+ import { clearModelCache, getProviderDiscoveryStatus } from "../../codex/model-cache";
56
+ import { DEFAULT_PROVIDER_CONTEXT_CAP, globalContextCapValue, providerContextCap, providerContextCaps, setAllProviderContextCaps, setGlobalContextCapValue, setProviderContextCap } from "../../providers/context-cap";
57
+ import { resolveCodexHomeDir } from "../../codex/home";
58
+ import { readUsageEntries } from "../../usage/log";
59
+ import { getUsageDebugLogEntries } from "../../usage/debug";
60
+ import { parseRange, parseUsageSurface, summarizeUsage } from "../../usage/summary";
61
+ import { stripCodexRuntimeProviderFields } from "../../codex/auth-context";
62
+ import { getProviderRegistryEntry } from "../../providers/registry";
63
+ import { getDebugLogEntries } from "../../lib/debug-log-buffer";
64
+ import { getInjectionDebugLogEntries } from "../../lib/injection-debug-log";
65
+ import {
66
+ clearDebugSettings,
67
+ clearDebugSetting,
68
+ getDebugSettings,
69
+ setDebugSettings,
70
+ type DebugFlag,
71
+ } from "../../lib/debug-settings";
72
+ import type { OcxClaudeCodeConfig, OcxConfig, OcxCustomModel, OcxProviderConfig } from "../../types";
73
+ import { drainAndShutdown } from "../lifecycle";
74
+ import { filterRequestLogs, getRequestLogEntries, type RequestLogEntry } from "../request-log";
75
+ import { estimateComboCost, estimateRequestCost, normalizeCostTokens, tokensPerSecond } from "../../usage/cost";
76
+ import type { PersistedUsageAttempt } from "../../usage/log";
77
+ import { isAllowedRequestOrigin, jsonResponse, providerManagementConfigError, publicProviderBaseUrl, safeConfigDTO } from "../auth-cors";
78
+ import { providerServiceTierConfigError } from "./provider-capability-config";
79
+ import { applySystemEnvToggle } from "../system-env";
80
+ import {
81
+ LOCAL_PROVIDER_RELOAD_NAME_HEADER,
82
+ LOCAL_PROVIDER_RELOAD_PATH,
83
+ } from "../../lib/local-provider-reload-contract";
84
+ import { refreshUserCostOverlays } from "../../usage/user-cost-overlays";
85
+ import {
86
+ XAI_RESPONSES_OPT_IN_MODELS,
87
+ xaiResponsesOptInState,
88
+ } from "../../providers/xai-responses-opt-in";
89
+
90
+ import { isPlainRecord, parseDebugLogQuery, tokPerSecondResult, unavailableCostReason, costResult, requestLogDto, stripRegistryOnlyStaticHeaders, fetchAllModels } from "./shared";
91
+ import type { MetricUnavailableReason, TokPerSecondResult, CostEstimateReason, CostResult, MetricSource } from "./shared";
92
+ import type { ManagementContext } from "./context";
93
+ import { readManagementJsonBody, rethrowManagementBodyTooLarge } from "./body";
94
+
95
+ type ProviderPatchApplication =
96
+ | { error: string }
97
+ | {
98
+ next: OcxProviderConfig;
99
+ touched: boolean;
100
+ editorTouched: boolean;
101
+ enablingOpenAi: boolean;
102
+ headersTouched: boolean;
103
+ };
104
+
105
+ /**
106
+ * Apply the recognized PATCH field mask onto a provider copy. The caller runs this once
107
+ * for validation and again inside the config mutation lock against the newest provider,
108
+ * so a concurrent PATCH cannot be erased by a save of a stale snapshot. Only synchronous
109
+ * checks live here; the async destination probe stays in the route.
110
+ */
111
+ function applyProviderPatchFields(
112
+ name: string,
113
+ provider: OcxProviderConfig,
114
+ rawBody: Record<string, unknown>,
115
+ keys: string[],
116
+ config: OcxConfig,
117
+ ): ProviderPatchApplication {
118
+ const next: OcxProviderConfig = { ...provider };
119
+ let touched = false;
120
+ let headersTouched = false;
121
+
122
+ if (Object.hasOwn(rawBody, "disabled")) {
123
+ if (typeof rawBody.disabled !== "boolean") return { error: "disabled must be a boolean" };
124
+ if (rawBody.disabled && name === config.defaultProvider) {
125
+ return { error: "cannot disable the default provider; set another default first" };
126
+ }
127
+ next.disabled = rawBody.disabled;
128
+ touched = true;
129
+ }
130
+ if (Object.hasOwn(rawBody, "adapter")) {
131
+ if (typeof rawBody.adapter !== "string" || !rawBody.adapter.trim()) return { error: "adapter must be a non-empty string" };
132
+ next.adapter = rawBody.adapter.trim();
133
+ touched = true;
134
+ }
135
+ if (Object.hasOwn(rawBody, "baseUrl")) {
136
+ if (typeof rawBody.baseUrl !== "string" || !rawBody.baseUrl.trim()) return { error: "baseUrl must be a non-empty string" };
137
+ next.baseUrl = rawBody.baseUrl.trim();
138
+ touched = true;
139
+ }
140
+ if (Object.hasOwn(rawBody, "defaultModel")) {
141
+ if (typeof rawBody.defaultModel !== "string") return { error: "defaultModel must be a string" };
142
+ const dm = rawBody.defaultModel.trim();
143
+ if (dm) next.defaultModel = dm;
144
+ else delete next.defaultModel;
145
+ touched = true;
146
+ }
147
+ if (Object.hasOwn(rawBody, "authMode")) {
148
+ if (typeof rawBody.authMode !== "string") return { error: "authMode must be a string" };
149
+ const mode = rawBody.authMode.trim();
150
+ if (mode === "key" || mode === "forward" || mode === "oauth" || mode === "local") {
151
+ next.authMode = mode;
152
+ touched = true;
153
+ } else if (mode === "") {
154
+ delete next.authMode;
155
+ touched = true;
156
+ } else {
157
+ return { error: "authMode must be key, forward, oauth, or local" };
158
+ }
159
+ }
160
+ if (Object.hasOwn(rawBody, "apiKeyTransport")) {
161
+ const transport = rawBody.apiKeyTransport;
162
+ if (transport === "x-api-key" || transport === "bearer") {
163
+ next.apiKeyTransport = transport;
164
+ touched = true;
165
+ } else if (transport === "") {
166
+ delete next.apiKeyTransport;
167
+ touched = true;
168
+ } else {
169
+ return { error: "apiKeyTransport must be x-api-key, bearer, or empty to clear" };
170
+ }
171
+ }
172
+ if (Object.hasOwn(rawBody, "note")) {
173
+ if (typeof rawBody.note !== "string") return { error: "note must be a string" };
174
+ const note = rawBody.note.trim();
175
+ if (note) next.note = note;
176
+ else delete next.note;
177
+ touched = true;
178
+ }
179
+ if (Object.hasOwn(rawBody, "allowPrivateNetwork")) {
180
+ if (typeof rawBody.allowPrivateNetwork !== "boolean") return { error: "allowPrivateNetwork must be a boolean" };
181
+ next.allowPrivateNetwork = rawBody.allowPrivateNetwork;
182
+ touched = true;
183
+ }
184
+ if (Object.hasOwn(rawBody, "liveModels")) {
185
+ if (typeof rawBody.liveModels !== "boolean") return { error: "liveModels must be a boolean" };
186
+ next.liveModels = rawBody.liveModels;
187
+ touched = true;
188
+ }
189
+ if (Object.hasOwn(rawBody, "xaiResponsesOptIn")) {
190
+ if (name !== "xai") return { error: "xaiResponsesOptIn is valid only for provider xai" };
191
+ if (typeof rawBody.xaiResponsesOptIn !== "boolean") {
192
+ return { error: "xaiResponsesOptIn must be a boolean" };
193
+ }
194
+ const modelAdapters = { ...(next.modelAdapters ?? {}) };
195
+ for (const model of XAI_RESPONSES_OPT_IN_MODELS) {
196
+ if (rawBody.xaiResponsesOptIn) modelAdapters[model] = "openai-responses";
197
+ else delete modelAdapters[model];
198
+ }
199
+ if (Object.keys(modelAdapters).length > 0) next.modelAdapters = modelAdapters;
200
+ else delete next.modelAdapters;
201
+ touched = true;
202
+ }
203
+ if (Object.hasOwn(rawBody, "requestPacing")) {
204
+ const value = rawBody.requestPacing;
205
+ if (value === null) {
206
+ delete next.requestPacing;
207
+ } else {
208
+ if (!isPlainRecord(value)) return { error: "requestPacing must be a plain object or null" };
209
+ const pacingError = requestPacingConfigError(value);
210
+ if (pacingError) return { error: pacingError };
211
+ // `requestPacingConfigError` is the runtime narrowing boundary above; keep the
212
+ // assertion explicit because a generic plain record cannot express `enabled`.
213
+ next.requestPacing = structuredClone(value) as unknown as OcxProviderConfig["requestPacing"];
214
+ }
215
+ touched = true;
216
+ }
217
+ if (Object.hasOwn(rawBody, "upstreamHttpVersion")) {
218
+ const value = rawBody.upstreamHttpVersion;
219
+ if (value === null || value === "") {
220
+ delete next.upstreamHttpVersion;
221
+ } else {
222
+ const versionError = upstreamHttpVersionConfigError(value);
223
+ if (versionError) return { error: versionError };
224
+ // `upstreamHttpVersionConfigError` is the shared write boundary; the assertion is
225
+ // explicit because the incoming value is an unknown JSON scalar.
226
+ next.upstreamHttpVersion = value as OcxProviderConfig["upstreamHttpVersion"];
227
+ }
228
+ touched = true;
229
+ }
230
+ // The Models page edits the catalog hints in place; keep them on the existing
231
+ // provider mutation path so validation, cache invalidation, and convergence stay unified (#1073).
232
+ if (Object.hasOwn(rawBody, "contextWindow")) {
233
+ const value = rawBody.contextWindow;
234
+ if (value === null) {
235
+ delete next.contextWindow;
236
+ // `Number.isInteger(1e100)` is true, so an integer check alone admits a value that
237
+ // serializes into the catalog as an enormous number and can make Codex reject the whole
238
+ // file. Safe-integer is the real bound for something that ends up in a JSON int field.
239
+ } else if (typeof value === "number" && Number.isSafeInteger(value) && value > 0) {
240
+ next.contextWindow = value;
241
+ } else {
242
+ return { error: "contextWindow must be a positive safe integer or null" };
243
+ }
244
+ touched = true;
245
+ }
246
+ if (Object.hasOwn(rawBody, "modelContextWindows")) {
247
+ const value = rawBody.modelContextWindows;
248
+ if (value === null) {
249
+ delete next.modelContextWindows;
250
+ } else {
251
+ if (!isPlainRecord(value)) return { error: "modelContextWindows must be a plain object or null" };
252
+ const windows: Record<string, number> = { ...(next.modelContextWindows ?? {}) };
253
+ for (const [model, window] of Object.entries(value)) {
254
+ if (!model.trim()) return { error: "modelContextWindows keys must be nonblank model ids" };
255
+ if (window === null) {
256
+ delete windows[model];
257
+ continue;
258
+ }
259
+ if (typeof window !== "number" || !Number.isSafeInteger(window) || window <= 0) {
260
+ return { error: "modelContextWindows values must be positive safe integers or null" };
261
+ }
262
+ windows[model] = window;
263
+ }
264
+ if (Object.keys(windows).length > 0) next.modelContextWindows = windows;
265
+ else delete next.modelContextWindows;
266
+ }
267
+ touched = true;
268
+ }
269
+ if (Object.hasOwn(rawBody, "modelSupportsServiceTier")) {
270
+ const value = rawBody.modelSupportsServiceTier;
271
+ if (value === null) {
272
+ delete next.modelSupportsServiceTier;
273
+ } else {
274
+ if (!isPlainRecord(value)) return { error: "modelSupportsServiceTier must be a plain object or null" };
275
+ const capabilities: Record<string, boolean> = { ...(next.modelSupportsServiceTier ?? {}) };
276
+ for (const [model, supported] of Object.entries(value)) {
277
+ if (!model.trim()) return { error: "modelSupportsServiceTier keys must be nonblank model ids" };
278
+ if (supported === null) {
279
+ delete capabilities[model];
280
+ continue;
281
+ }
282
+ if (typeof supported !== "boolean") {
283
+ return { error: "modelSupportsServiceTier values must be booleans or null" };
284
+ }
285
+ capabilities[model] = supported;
286
+ }
287
+ if (Object.keys(capabilities).length > 0) next.modelSupportsServiceTier = capabilities;
288
+ else delete next.modelSupportsServiceTier;
289
+ }
290
+ touched = true;
291
+ }
292
+ if (Object.hasOwn(rawBody, "noStructuredOutputModels")) {
293
+ const value = rawBody.noStructuredOutputModels;
294
+ if (value === null) {
295
+ delete next.noStructuredOutputModels;
296
+ } else {
297
+ const error = nonBlankStringArrayConfigError(value, "noStructuredOutputModels");
298
+ if (error) return { error };
299
+ const models = normalizeNonBlankStringArray(value as string[]);
300
+ if (models.length > 0) next.noStructuredOutputModels = models;
301
+ else delete next.noStructuredOutputModels;
302
+ }
303
+ touched = true;
304
+ }
305
+
306
+ // headers is the one object-valued field in the mask. PATCH semantics merge it
307
+ // shallowly into the existing block so a single fingerprint header can be added
308
+ // without wiping the rest; null or an empty object clears user-managed headers.
309
+ if (Object.hasOwn(rawBody, "headers")) {
310
+ const headersValue = rawBody.headers;
311
+ if (headersValue === null || (isPlainRecord(headersValue) && Object.keys(headersValue).length === 0)) {
312
+ // Registry-owned static metadata (e.g. opencode-free's x-opencode-client marker)
313
+ // is not user-managed: restoring it keeps the upstream transport intact after a
314
+ // clear instead of deleting the whole block.
315
+ const entry = getProviderRegistryEntry(name);
316
+ if (entry?.staticHeaders && providerMatchesRegistryTransport(name, next)) {
317
+ next.headers = { ...entry.staticHeaders };
318
+ } else {
319
+ delete next.headers;
320
+ }
321
+ } else {
322
+ if (!isPlainRecord(headersValue)) return { error: "headers must be an object" };
323
+ const headersError = providerHeadersConfigError(headersValue);
324
+ if (headersError) return { error: headersError };
325
+ // Header names are case-insensitive on the wire. Drop any existing key whose
326
+ // lowercase name collides with an incoming one, or Headers normalization would
327
+ // send a combined "x-custom: v1, v2" value upstream.
328
+ const incoming = new Map(
329
+ Object.entries(headersValue as Record<string, string>).map(([key, value]) => [key.toLowerCase(), [key, value] as const]),
330
+ );
331
+ const merged: Record<string, string> = {};
332
+ for (const [key, value] of Object.entries(next.headers ?? {})) {
333
+ if (!incoming.has(key.toLowerCase())) merged[key] = value;
334
+ }
335
+ for (const [key, value] of incoming.values()) merged[key] = value;
336
+ next.headers = merged;
337
+ }
338
+ touched = true;
339
+ headersTouched = true;
340
+ }
341
+
342
+ if (!touched) return { error: "no recognized fields to update" };
343
+
344
+ // A disabled-only toggle preserves the v2 fast lane for non-openai providers: it changes
345
+ // routing eligibility, not the provider shape. Re-enabling `openai` is different — a
346
+ // malformed disabled row must not come back online unchanged, so canonicalize/reject
347
+ // against the same built-in gate used by mode PATCH and POST.
348
+ const editorTouched = keys.some(key => key !== "disabled");
349
+ const enablingOpenAi = name === "openai"
350
+ && Object.hasOwn(rawBody, "disabled")
351
+ && rawBody.disabled === false
352
+ && provider.disabled === true;
353
+ if (!editorTouched && enablingOpenAi) {
354
+ if (!isCanonicalOpenAiForwardProvider(next)) {
355
+ return { error: "provider openai must be the canonical built-in provider" };
356
+ }
357
+ // Persist the byte-identical canonical URL so config.ts startup checks (case-sensitive)
358
+ // accept the row after we fill mode. Equivalent hosts like CHATGPT.com/:443 normalize here.
359
+ next.baseUrl = CODEX_FORWARD_BASE_URL;
360
+ // Fill missing mode so a disabled canonical row becomes a complete live openai entry.
361
+ if (next.codexAccountMode !== "pool" && next.codexAccountMode !== "direct") {
362
+ next.codexAccountMode = "pool";
363
+ }
364
+ if (next.disabled === false) delete next.disabled;
365
+ // Canonical openai never uses private-network opt-in; drop a stale flag that
366
+ // was ignored for the DNS probe so it cannot linger on the live row.
367
+ delete next.allowPrivateNetwork;
368
+ }
369
+ return { next, touched, editorTouched, enablingOpenAi, headersTouched };
370
+ }
371
+
372
+ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Response | null> {
373
+ const { req, url, config, deps, principal, convergeCodexCatalog, syncClaudeAgentDefsBestEffort } = ctx;
374
+
375
+ if (url.pathname === "/api/provider-quotas" && req.method === "GET") {
376
+ const forceRefresh = url.searchParams.get("refresh") === "1" || url.searchParams.get("refresh") === "true";
377
+ return jsonResponse(await fetchProviderQuotaReports(config, forceRefresh));
378
+ }
379
+
380
+ if (url.pathname === "/api/provider-request-pacing" && req.method === "GET") {
381
+ const name = url.searchParams.get("name")?.trim();
382
+ if (name) {
383
+ if (!isValidProviderName(name) || !hasOwnProvider(config.providers, name)) {
384
+ return jsonResponse({ error: "unknown provider" }, 404);
385
+ }
386
+ return jsonResponse(providerRequestPacingStatus(name, config.providers[name]!));
387
+ }
388
+ return jsonResponse(Object.fromEntries(
389
+ Object.entries(config.providers).map(([providerName, provider]) => [
390
+ providerName,
391
+ providerRequestPacingStatus(providerName, provider),
392
+ ]),
393
+ ));
394
+ }
395
+
396
+ if (url.pathname === "/api/providers" && req.method === "GET") {
397
+ return jsonResponse(Object.entries(config.providers).map(([name, p]) => ({
398
+ name, adapter: p.adapter, baseUrl: publicProviderBaseUrl(p.baseUrl), defaultModel: p.defaultModel,
399
+ hasApiKey: !!p.apiKey,
400
+ // Presence only (#959 review): header names and values never leave the process.
401
+ hasHeaders: !!p.headers && Object.keys(p.headers).length > 0,
402
+ allowPrivateNetwork: p.allowPrivateNetwork === true,
403
+ liveModels: p.liveModels !== false,
404
+ requestPacing: p.requestPacing,
405
+ models: p.models ?? [],
406
+ contextWindow: p.contextWindow,
407
+ modelContextWindows: p.modelContextWindows,
408
+ modelSupportsServiceTier: p.modelSupportsServiceTier,
409
+ noStructuredOutputModels: p.noStructuredOutputModels,
410
+ upstreamHttpVersion: p.upstreamHttpVersion,
411
+ authMode: p.authMode,
412
+ apiKeyTransport: p.apiKeyTransport,
413
+ disabled: p.disabled === true,
414
+ codexAccountMode: providerCodexAccountMode(name, p),
415
+ ...(name === "xai" ? { xaiResponsesOptInState: xaiResponsesOptInState(p) } : {}),
416
+ discovery: p.liveModels === false ? undefined : getProviderDiscoveryStatus(name),
417
+ })));
418
+ }
419
+
420
+ if (url.pathname === LOCAL_PROVIDER_RELOAD_PATH && req.method === "POST") {
421
+ if (principal !== "local-provider-reload-capability") {
422
+ return jsonResponse({ error: "provider reload capability required" }, 403);
423
+ }
424
+ const name = req.headers.get(LOCAL_PROVIDER_RELOAD_NAME_HEADER) ?? "";
425
+ if (!isValidProviderName(name)) return jsonResponse({ error: "invalid provider reload target" }, 400);
426
+
427
+ const admitted = readConfigAdmissionSnapshot();
428
+ if (
429
+ admitted.kind !== "read"
430
+ || admitted.diagnostics.source !== "file"
431
+ || admitted.diagnostics.error !== null
432
+ ) {
433
+ return jsonResponse({ error: "provider reload source unavailable" }, 409);
434
+ }
435
+ const diskConfig = admitted.diagnostics.config;
436
+ if (!hasOwnProvider(diskConfig.providers, name)) {
437
+ return jsonResponse({ error: "provider reload target unavailable" }, 404);
438
+ }
439
+ const provider = diskConfig.providers[name]!;
440
+ const providerError = providerManagementConfigError(name, provider);
441
+ if (providerError) return jsonResponse({ error: "provider reload target invalid" }, 409);
442
+ const namespaceCollision = codexAccountNamespaceProviderCollisionError(
443
+ diskConfig.codexAccountNamespaces,
444
+ name,
445
+ );
446
+ if (namespaceCollision) return jsonResponse({ error: "provider reload target conflicts with routing" }, 409);
447
+ const allowBenchmarkAddresses = name === "openai" && isCanonicalOpenAiForwardProvider(provider);
448
+ const resolvedError = await providerDestinationResolvedError(name, provider, { allowBenchmarkAddresses });
449
+ if (resolvedError) return jsonResponse({ error: "provider reload target rejected" }, 409);
450
+
451
+ // Destination validation awaits DNS. A cooperating writer holds the same SQLite
452
+ // mutation lock, so the final exact-byte check and live adoption happen as one
453
+ // synchronous authority decision. The route does not save or reserialize disk.
454
+ let currentDiskConfig: OcxConfig | null = null;
455
+ let sourceChanged = false;
456
+ withConfigMutationLockSync(() => {
457
+ const current = readConfigAdmissionSnapshot();
458
+ if (
459
+ current.kind !== "read"
460
+ || current.diagnostics.source !== "file"
461
+ || current.diagnostics.error !== null
462
+ || current.contentSha256 !== admitted.contentSha256
463
+ ) {
464
+ sourceChanged = true;
465
+ return;
466
+ }
467
+ currentDiskConfig = current.diagnostics.config;
468
+ adoptPersistedProviderIntoLiveConfig(
469
+ config,
470
+ name,
471
+ current.diagnostics.config.providers[name]!,
472
+ current.diagnostics.config,
473
+ );
474
+ });
475
+ if (sourceChanged || currentDiskConfig === null) {
476
+ return jsonResponse({ error: "provider reload source changed" }, 409);
477
+ }
478
+ reconcileLiveStateStores();
479
+ // The complete disk snapshot owns display overlays, including providers that this
480
+ // live routing instance deliberately does not adopt.
481
+ refreshUserCostOverlays(currentDiskConfig);
482
+ clearGatherRoutedModelsInflight();
483
+ (deps.clearProviderQuotaCache ?? clearProviderQuotaCache)();
484
+ clearAccountQuotaCache(name);
485
+ clearKeyCooldowns(name);
486
+ clearModelCache(name);
487
+ if (name === "openai") (deps.clearThreadAccountMap ?? clearThreadAccountMap)();
488
+ const catalogRefresh = await convergeCodexCatalog();
489
+ return jsonResponse({ success: true, name, catalogRefresh });
490
+ }
491
+
492
+ // Add (or overwrite) a single provider. Merges into the live in-memory config and
493
+ // persists — existing providers' real keys are never round-tripped (unlike PUT /api/config,
494
+ // which would re-save the masked keys from GET). Live routing picks it up immediately.
495
+ if (url.pathname === "/api/providers" && req.method === "POST") {
496
+ let body: { name?: unknown; provider?: unknown; setDefault?: boolean };
497
+ try { body = await readManagementJsonBody(req); } catch (error) { rethrowManagementBodyTooLarge(error); return jsonResponse({ error: "invalid JSON body" }, 400); }
498
+ const name = typeof body.name === "string" ? body.name.trim() : "";
499
+ const providerError = providerManagementConfigError(name, body.provider);
500
+ if (providerError) return jsonResponse({ error: providerError }, 400);
501
+ const serviceTierError = providerServiceTierConfigError(name, body.provider);
502
+ if (serviceTierError) return jsonResponse({ error: serviceTierError }, 400);
503
+ const prov = body.provider ? stripCodexRuntimeProviderFields(body.provider as OcxProviderConfig) : undefined;
504
+ // PATCH already clears on null; POST persisted the body as submitted, so a `null` here
505
+ // reached disk and the next loadConfig() refused it. Canonicalize to absent, which is what
506
+ // "clear" means everywhere else.
507
+ if (prov && prov.upstreamHttpVersion === null) delete prov.upstreamHttpVersion;
508
+ if (!name || !prov?.adapter || !prov?.baseUrl) {
509
+ return jsonResponse({ error: "name, provider.adapter and provider.baseUrl are required" }, 400);
510
+ }
511
+ if (!isValidProviderName(name)) {
512
+ return jsonResponse({ error: "provider name must use letters, numbers, dot, underscore, or hyphen and cannot be a reserved object key" }, 400);
513
+ }
514
+ const namespaceCollision = codexAccountNamespaceProviderCollisionError(config.codexAccountNamespaces, name);
515
+ if (namespaceCollision) {
516
+ return jsonResponse({ error: namespaceCollision }, 409);
517
+ }
518
+ // Hostname destinations additionally get a DNS-resolved SSRF check at write time —
519
+ // the sync check above only classifies literal IPs (review finding, PR #96).
520
+ // Canonical openai still runs the resolver: only Clash fake-IP (198.18.0.0/15)
521
+ // answers are ignored; loopback/RFC1918/metadata/mixed sets still fail.
522
+ const allowBenchmarkAddresses = name === "openai" && isCanonicalOpenAiForwardProvider(prov);
523
+ const resolvedError = await providerDestinationResolvedError(name, prov, { allowBenchmarkAddresses });
524
+ if (resolvedError) return jsonResponse({ error: resolvedError }, 400);
525
+ if (body.setDefault !== undefined && typeof body.setDefault !== "boolean") {
526
+ return jsonResponse({ error: "setDefault must be a boolean" }, 400);
527
+ }
528
+ if (body.setDefault === true && prov.disabled) {
529
+ return jsonResponse({ error: "cannot set a disabled provider as default", code: "default_provider_disabled" }, 400);
530
+ }
531
+ // Catalog providers (e.g. ollama-cloud) carry a models + vision/reasoning classification the GUI
532
+ // doesn't send — merge it in so the sidecars are gated correctly.
533
+ // Sample request ownership BEFORE enrichment. Enrichment fills absent fields from the
534
+ // registry seed, after which "the client omitted this" and "the registry supplied it" are
535
+ // indistinguishable — so a carry-over guard written as `prov.x === undefined` after this
536
+ // call can never fire.
537
+ const submittedContextWindow = Object.hasOwn(prov, "contextWindow");
538
+ const submittedModelContextWindows = Object.hasOwn(prov, "modelContextWindows");
539
+ const submittedRequestPacing = Object.hasOwn(prov, "requestPacing");
540
+ enrichProviderFromCatalog(name, prov);
541
+ const { saveConfigPreservingClaudeCode: save } = await import("../../config");
542
+ // Overwriting an existing provider must not drop its multi-key pool: carry it over, then
543
+ // let the (possibly new) apiKey join the pool as the active entry.
544
+ const existingPool = config.providers[name]?.apiKeyPool;
545
+ if (existingPool && !prov.apiKeyPool) prov.apiKeyPool = existingPool;
546
+ // The same rule applies to user-configured price overlays: the dashboard's
547
+ // add/edit form does not send modelCosts, so an overwrite must not silently
548
+ // erase hand-edited per-model prices from Logs/Usage estimates.
549
+ const existingCosts = config.providers[name]?.modelCosts;
550
+ if (existingCosts && !prov.modelCosts) prov.modelCosts = existingCosts;
551
+ // ...and to hand-edited context windows. `ProviderPayload` (gui/src/provider-payload.ts)
552
+ // has no member for either field, so the add/edit form structurally cannot send them:
553
+ // absence in the request means "not carried", never "the user deleted it". Deletion goes
554
+ // through PATCH with an explicit null (#1409).
555
+ const existing = config.providers[name];
556
+ if (!submittedRequestPacing && existing?.requestPacing) {
557
+ prov.requestPacing = structuredClone(existing.requestPacing);
558
+ }
559
+ if (!submittedContextWindow && existing?.contextWindow !== undefined) {
560
+ prov.contextWindow = existing.contextWindow;
561
+ }
562
+ if (existing?.modelContextWindows) {
563
+ // When the client did send a map, its keys win and the user's other keys survive. When
564
+ // it did not, the stored value is the user's map alone: merging the registry seed in
565
+ // would persist seed keys into user config as a side effect of an unrelated save, and
566
+ // router.ts already fills registry values beneath user entries at resolve time.
567
+ prov.modelContextWindows = submittedModelContextWindows
568
+ ? { ...existing.modelContextWindows, ...(prov.modelContextWindows ?? {}) }
569
+ : { ...existing.modelContextWindows };
570
+ }
571
+ config.providers[name] = stripRegistryOnlyStaticHeaders(name, prov);
572
+ if (body.setDefault === true) config.defaultProvider = name;
573
+ save(config);
574
+ reconcileLiveStateStores();
575
+ if (prov.apiKey && prov.apiKeyPool) {
576
+ const { addProviderApiKey } = await import("../../providers/api-keys");
577
+ addProviderApiKey(config, name, prov.apiKey);
578
+ }
579
+ const { clearModelCache } = await import("../../codex/model-cache");
580
+ clearModelCache(name);
581
+ const catalogRefresh = await convergeCodexCatalog();
582
+ return jsonResponse({ success: true, name, catalogRefresh });
583
+ }
584
+
585
+ if (url.pathname === "/api/providers" && req.method === "PATCH") {
586
+ const name = url.searchParams.get("name")?.trim();
587
+ if (!name || !isValidProviderName(name) || !hasOwnProvider(config.providers, name)) return jsonResponse({ error: "unknown provider" }, 404);
588
+ let rawBody: unknown;
589
+ try { rawBody = await readManagementJsonBody(req); } catch (error) { rethrowManagementBodyTooLarge(error); return jsonResponse({ error: "invalid JSON body" }, 400); }
590
+ if (!isPlainRecord(rawBody)) return jsonResponse({ error: "provider patch body must be a plain object" }, 400);
591
+ const keys = Object.keys(rawBody);
592
+ const hasMode = Object.hasOwn(rawBody, "codexAccountMode");
593
+ const hasSetDefault = Object.hasOwn(rawBody, "setDefault");
594
+
595
+ // codexAccountMode keeps its dedicated side-effect path (quota cache clear, thread map
596
+ // clear, pool prime) and is mutually exclusive with every other patch field.
597
+ if (hasMode) {
598
+ if (keys.length !== 1) {
599
+ return jsonResponse({ error: "codexAccountMode cannot be combined with other patch fields" }, 400);
600
+ }
601
+ if (name !== "openai") return jsonResponse({ error: "codexAccountMode is valid only for provider openai" }, 400);
602
+ const mode = rawBody.codexAccountMode;
603
+ if (mode !== "pool" && mode !== "direct") {
604
+ return jsonResponse({ error: "codexAccountMode must be pool or direct" }, 400);
605
+ }
606
+ const provider = config.providers.openai;
607
+ if (!provider || !isCanonicalOpenAiForwardProvider(provider)) {
608
+ return jsonResponse({ error: "provider openai must be the canonical built-in provider" }, 400);
609
+ }
610
+ const { saveConfigPreservingClaudeCode: save } = await import("../../config");
611
+ config.providers.openai = { ...provider, codexAccountMode: mode };
612
+ save(config);
613
+ reconcileLiveStateStores();
614
+ (deps.clearProviderQuotaCache ?? clearProviderQuotaCache)();
615
+ (deps.clearThreadAccountMap ?? clearThreadAccountMap)();
616
+ if (mode === "pool") {
617
+ try {
618
+ const prime = deps.primeCodexPoolQuotas ?? primeCodexPoolQuotas;
619
+ void Promise.resolve(prime(config, "mode-change")).catch(() => undefined);
620
+ } catch {
621
+ // Quota priming is best-effort; the persisted live mode is already authoritative.
622
+ }
623
+ }
624
+ return jsonResponse({ success: true, name: "openai", codexAccountMode: mode });
625
+ }
626
+
627
+ // Default-provider changes must be a deliberate, standalone action. This keeps
628
+ // routing changes out of ordinary provider edits and lets the dashboard expose a
629
+ // simple "Set as default" control without round-tripping the full config.
630
+ if (hasSetDefault) {
631
+ if (keys.length !== 1 || rawBody.setDefault !== true) {
632
+ return jsonResponse({ error: "setDefault must be true and cannot be combined with other patch fields" }, 400);
633
+ }
634
+ if (config.providers[name]!.disabled) {
635
+ return jsonResponse({ error: "cannot set a disabled provider as default", code: "default_provider_disabled" }, 400);
636
+ }
637
+ const { saveConfigPreservingClaudeCode: save } = await import("../../config");
638
+ config.defaultProvider = name;
639
+ save(config);
640
+ reconcileLiveStateStores();
641
+ return jsonResponse({ success: true, name, defaultProvider: name });
642
+ }
643
+
644
+ // Field-mask editor: apply recognized fields onto a copy, then validate the MERGED
645
+ // provider (canonical-seed guard covers openai; local-guard covers registry key providers).
646
+ // API keys are never writable here — the api-keys endpoints own pool-integrated key writes.
647
+ if (Object.hasOwn(rawBody, "apiKey")) {
648
+ return jsonResponse({ error: "apiKey cannot be patched here; use the provider API-key endpoints" }, 400);
649
+ }
650
+ const applied = applyProviderPatchFields(name, config.providers[name]!, rawBody, keys, config);
651
+ if ("error" in applied) return jsonResponse({ error: applied.error }, 400);
652
+ const next = applied.next;
653
+
654
+ const pacingOnly = keys.every(key => key === "requestPacing");
655
+ if (applied.editorTouched && !pacingOnly) {
656
+ const providerError = providerManagementConfigError(name, next);
657
+ if (providerError) return jsonResponse({ error: providerError }, 400);
658
+ const serviceTierError = providerServiceTierConfigError(name, next);
659
+ if (serviceTierError) return jsonResponse({ error: serviceTierError }, 400);
660
+ const resolvedError = await providerDestinationResolvedError(name, next);
661
+ if (resolvedError) return jsonResponse({ error: resolvedError }, 400);
662
+ } else if (applied.enablingOpenAi) {
663
+ // Same DNS gate as POST: Clash fake-IP only. Never honor a persisted
664
+ // allowPrivateNetwork on this path — it must not bypass the built-in guard.
665
+ const resolvedError = await providerDestinationResolvedError(
666
+ "openai",
667
+ { baseUrl: CODEX_FORWARD_BASE_URL },
668
+ { allowBenchmarkAddresses: true },
669
+ );
670
+ if (resolvedError) return jsonResponse({ error: resolvedError }, 400);
671
+ }
672
+
673
+ // The live config is shared and the destination probe above is awaited. Re-apply the
674
+ // mask onto the newest provider under the mutation lock right before saving, so two
675
+ // concurrent PATCHes updating different fields/headers both survive instead of the
676
+ // later save clobbering the earlier snapshot.
677
+ let replayError: string | undefined;
678
+ withConfigMutationLockSync(() => {
679
+ const replay = applyProviderPatchFields(name, config.providers[name]!, rawBody, keys, config);
680
+ if ("error" in replay) {
681
+ replayError = replay.error;
682
+ return;
683
+ }
684
+ if (replay.editorTouched && !pacingOnly) {
685
+ const syncError = providerManagementConfigError(name, replay.next);
686
+ if (syncError) {
687
+ replayError = syncError;
688
+ return;
689
+ }
690
+ const serviceTierError = providerServiceTierConfigError(name, replay.next);
691
+ if (serviceTierError) {
692
+ replayError = serviceTierError;
693
+ return;
694
+ }
695
+ } else if (replay.enablingOpenAi && !isCanonicalOpenAiForwardProvider(replay.next)) {
696
+ replayError = "provider openai must be the canonical built-in provider";
697
+ return;
698
+ }
699
+ // A PATCH that managed headers owns the resulting block: the clear path restores
700
+ // registry static headers, so exact-match stripping must not erase them again.
701
+ config.providers[name] = replay.headersTouched ? replay.next : stripRegistryOnlyStaticHeaders(name, replay.next);
702
+ saveConfigPreservingClaudeCode(config);
703
+ });
704
+ if (replayError !== undefined) return jsonResponse({ error: replayError }, 409);
705
+ reconcileLiveStateStores();
706
+ if (applied.editorTouched && !pacingOnly) {
707
+ const { clearModelCache } = await import("../../codex/model-cache");
708
+ clearModelCache(name);
709
+ }
710
+ const catalogRefresh = pacingOnly ? null : await convergeCodexCatalog();
711
+ return jsonResponse({
712
+ success: true,
713
+ name,
714
+ disabled: config.providers[name]!.disabled === true,
715
+ hasApiKey: !!config.providers[name]!.apiKey,
716
+ ...(name === "xai"
717
+ ? { xaiResponsesOptInState: xaiResponsesOptInState(config.providers[name]!) }
718
+ : {}),
719
+ catalogRefresh,
720
+ });
721
+ }
722
+
723
+ // Lightweight connectivity probe: perform the provider's live /models fetch DIRECTLY and
724
+ // report only real upstream evidence. The catalog aggregate (fetchAllModels) deliberately
725
+ // hides fetch failures behind stale/static fallbacks, so a catalog-presence check would
726
+ // let a static-catalog provider with a fake key "pass" — this endpoint never uses it.
727
+ if (url.pathname === "/api/providers/test" && req.method === "POST") {
728
+ const name = url.searchParams.get("name")?.trim();
729
+ if (!name || !isValidProviderName(name) || !hasOwnProvider(config.providers, name)) {
730
+ return jsonResponse({ error: "unknown provider" }, 404);
731
+ }
732
+ const prov = config.providers[name]!;
733
+ if (prov.disabled) {
734
+ return jsonResponse({ ok: false, error: "Provider is disabled", latencyMs: 0 });
735
+ }
736
+ if (prov.authMode === "forward") {
737
+ return jsonResponse({
738
+ ok: true,
739
+ latencyMs: 0,
740
+ message: "Passthrough provider is configured (forwards your Codex login; no upstream /models).",
741
+ });
742
+ }
743
+ if (prov.liveModels === false) {
744
+ // A static catalog has no live discovery endpoint to test. This is neither
745
+ // positive connectivity evidence nor an outage, and it must stay before
746
+ // credential resolution/network access for providers such as Antigravity.
747
+ return jsonResponse({ applicable: false, reason: "static_catalog", latencyMs: 0 });
748
+ }
749
+ const { buildModelsRequest, getValidAccessTokenSnapshot, resolveModelsAuthToken } = await import("../../oauth");
750
+ const antigravity = effectiveGoogleMode(name, prov) === "cloud-code-assist";
751
+ const snapshot = antigravity
752
+ ? await getValidAccessTokenSnapshot(name).catch(() => undefined)
753
+ : undefined;
754
+ const apiKey = snapshot?.accessToken ?? await resolveModelsAuthToken(name, prov);
755
+ if (prov.authMode === "oauth" && !apiKey) {
756
+ return jsonResponse({ ok: false, latencyMs: 0, error: "static catalog only — upstream not verified (not logged in)" });
757
+ }
758
+ if (prov.adapter === "cursor") {
759
+ const started = Date.now();
760
+ const live = await fetchCursorUsableModels({
761
+ apiKey: apiKey ?? "",
762
+ baseUrl: prov.baseUrl,
763
+ });
764
+ const latencyMs = Date.now() - started;
765
+ if (!live.ok) {
766
+ return jsonResponse({
767
+ ok: false,
768
+ latencyMs,
769
+ error: `cursor discovery ${live.error}${live.detail ? `: ${live.detail}` : ""}`,
770
+ });
771
+ }
772
+ return jsonResponse({
773
+ ok: true,
774
+ latencyMs,
775
+ models: live.models.length,
776
+ message: `Connected. ${live.models.length} models.`,
777
+ });
778
+ }
779
+ const project = prov.project ?? snapshot?.projectId;
780
+ if (antigravity && !project) {
781
+ return jsonResponse({ ok: false, latencyMs: 0, error: "Antigravity project unavailable — re-run `ocx login google-antigravity`" });
782
+ }
783
+ const { method, url: modelsUrl, headers } = buildModelsRequest(prov, apiKey, name);
784
+ const discovery = resolveProviderModelDiscovery(name, prov);
785
+ const started = Date.now();
786
+ try {
787
+ const res = method === "POST"
788
+ ? await providerOutboundPost(name, prov, modelsUrl, {
789
+ headers,
790
+ body: JSON.stringify({ project }),
791
+ signal: AbortSignal.timeout(8000),
792
+ })
793
+ : await providerOutboundGet(name, prov, modelsUrl, {
794
+ headers,
795
+ signal: AbortSignal.timeout(8000),
796
+ });
797
+ const latencyMs = Date.now() - started;
798
+ const redirectError = await providerRedirectError(res, modelsUrl);
799
+ if (redirectError) {
800
+ return jsonResponse({
801
+ ok: false,
802
+ latencyMs,
803
+ error: redirectError,
804
+ });
805
+ }
806
+ if (!res.ok) {
807
+ try {
808
+ void res.body?.cancel().catch(() => undefined);
809
+ } catch {
810
+ // Best-effort release for non-conforming response streams.
811
+ }
812
+ return jsonResponse({ ok: false, latencyMs, error: `upstream model discovery returned ${res.status}` });
813
+ }
814
+ const bounded = await readBoundedDiscoveryJson(res, discovery.maxResponseBytes);
815
+ if (!bounded.ok) {
816
+ return jsonResponse({
817
+ ok: false,
818
+ latencyMs,
819
+ error: bounded.reason === "response_too_large"
820
+ ? `upstream model discovery exceeded the ${discovery.maxResponseBytes}-byte response limit`
821
+ : "upstream model discovery returned invalid JSON",
822
+ });
823
+ }
824
+ const ccaModels = antigravity ? parseAntigravityAvailableModels(bounded.value, discovery.maxModels) : undefined;
825
+ if (antigravity && !ccaModels) {
826
+ return jsonResponse({ ok: false, latencyMs, error: "upstream CCA model discovery returned an unexpected shape" });
827
+ }
828
+ // OpenAI-style lists (and Together top-level arrays) use the same validation/dedupe/filter
829
+ // as catalog discovery. Google's /v1beta/models uses `models[].name` and remains a
830
+ // connectivity-only count because it is not an authoritative catalog source.
831
+ const record = bounded.value !== null && typeof bounded.value === "object" && !Array.isArray(bounded.value)
832
+ ? bounded.value as Record<string, unknown>
833
+ : undefined;
834
+ const extracted = ccaModels
835
+ ? undefined
836
+ : Array.isArray(bounded.value) || Array.isArray(record?.data)
837
+ ? extractProviderModelItems(bounded.value, discovery)
838
+ : extractModelEnvelopeRows(bounded.value, discovery.maxModels, ["models"]);
839
+ if (extracted && !extracted.ok) {
840
+ return jsonResponse({
841
+ ok: false,
842
+ latencyMs,
843
+ error: extracted.reason === "too_many_models"
844
+ ? `upstream /models exceeded the ${discovery.maxModels}-row model limit`
845
+ : "upstream /models returned an unexpected shape",
846
+ });
847
+ }
848
+ const models = ccaModels?.length ?? ("items" in extracted! ? extracted!.items.length : extracted!.rows.length);
849
+ return jsonResponse({
850
+ ok: true,
851
+ latencyMs,
852
+ models,
853
+ message: `Connected — ${models} model${models === 1 ? "" : "s"} available.`,
854
+ });
855
+ } catch (err) {
856
+ return jsonResponse({
857
+ ok: false,
858
+ latencyMs: Date.now() - started,
859
+ error: err instanceof ProviderOutboundPolicyError
860
+ ? `upstream /models blocked by destination policy: ${err.message}`
861
+ : err instanceof Error ? err.message : "Connection test failed",
862
+ });
863
+ }
864
+ }
865
+
866
+ if (url.pathname === "/api/providers" && req.method === "DELETE") {
867
+ const name = url.searchParams.get("name")?.trim();
868
+ if (!name || !isValidProviderName(name) || !hasOwnProvider(config.providers, name)) return jsonResponse({ error: "unknown provider" }, 404);
869
+ // Config validation requires a default provider. Reassigning before deletion keeps
870
+ // the persisted config valid and makes removal of the current default a one-step UI
871
+ // operation. Prefer the first remaining *enabled* provider so DELETE cannot leave a
872
+ // disabled default that setDefault / disable already refuse. Object-key order is the
873
+ // documented configuration order and is stable through JSON persistence.
874
+ const fallbackDefault = name === config.defaultProvider
875
+ ? Object.entries(config.providers)
876
+ .find(([provider, providerConfig]) => provider !== name && providerConfig.disabled !== true)
877
+ ?.[0]
878
+ : undefined;
879
+ if (name === config.defaultProvider && !fallbackDefault) {
880
+ return jsonResponse({
881
+ error: "cannot delete the default provider when no enabled replacement remains",
882
+ code: "last_provider",
883
+ }, 409);
884
+ }
885
+ const dependentCombos = Object.entries(config.combos ?? {})
886
+ .filter(([, combo]) => combo.targets.some(target => target.provider === name))
887
+ .map(([id]) => id)
888
+ .sort((a, b) => a.localeCompare(b));
889
+ if (dependentCombos.length > 0) {
890
+ return jsonResponse({
891
+ error: `cannot delete provider "${name}" while combos depend on it`,
892
+ code: "provider_has_dependent_combos",
893
+ combos: dependentCombos,
894
+ }, 409);
895
+ }
896
+ const { saveConfigPreservingClaudeCode: save } = await import("../../config");
897
+ if (fallbackDefault) config.defaultProvider = fallbackDefault;
898
+ delete config.providers[name];
899
+ const { dropProviderCustomModels } = await import("../../providers/provider-id-rewrite");
900
+ const droppedCustomModels = dropProviderCustomModels(config, name);
901
+ setProviderContextCap(config, name, false);
902
+ save(config);
903
+ await replaceProviderAccountSet(name, null);
904
+ reconcileLiveStateStores();
905
+ const { clearModelCache: clearCache } = await import("../../codex/model-cache");
906
+ clearCache(name);
907
+ const catalogRefresh = await convergeCodexCatalog();
908
+ return jsonResponse({
909
+ success: true,
910
+ ...(fallbackDefault ? { defaultProvider: fallbackDefault } : {}),
911
+ ...(droppedCustomModels > 0 ? { droppedCustomModels } : {}),
912
+ catalogRefresh,
913
+ });
914
+ }
915
+
916
+ if (url.pathname === "/api/provider-context-caps" && req.method === "GET") {
917
+ return jsonResponse({ cap: DEFAULT_PROVIDER_CONTEXT_CAP, value: globalContextCapValue(config), caps: providerContextCaps(config) });
918
+ }
919
+
920
+ if (url.pathname === "/api/provider-context-caps" && req.method === "PUT") {
921
+ let rawBody: unknown;
922
+ try { rawBody = await readManagementJsonBody(req); } catch (error) { rethrowManagementBodyTooLarge(error); return jsonResponse({ error: "invalid JSON body" }, 400); }
923
+ // Reject non-object payloads (e.g. `{"provider": true}` or a bare array) before any
924
+ // property access, with the route's consistent 400 response.
925
+ if (!isPlainRecord(rawBody)) return jsonResponse({ error: "provider-context-caps body must be a plain object" }, 400);
926
+ const body = rawBody as { provider?: unknown; enabled?: unknown; value?: unknown; setAll?: unknown };
927
+ const { saveConfigPreservingClaudeCode: save } = await import("../../config");
928
+ const { clearModelCache } = await import("../../codex/model-cache");
929
+ const respond = (catalogRefresh: Awaited<ReturnType<typeof convergeCodexCatalog>>) => jsonResponse({
930
+ ok: true,
931
+ cap: DEFAULT_PROVIDER_CONTEXT_CAP,
932
+ value: globalContextCapValue(config),
933
+ caps: providerContextCaps(config),
934
+ catalogRefresh,
935
+ });
936
+
937
+ // Reject malformed and mixed payloads before branch selection: `provider` and `enabled`
938
+ // must appear together with their expected types, and provider updates cannot be
939
+ // combined with `setAll` (which would otherwise be silently ignored).
940
+ const hasProviderFields = Object.hasOwn(body, "provider") || Object.hasOwn(body, "enabled");
941
+ if (hasProviderFields) {
942
+ if (typeof body.provider !== "string" || typeof body.enabled !== "boolean") {
943
+ return jsonResponse({ error: "provider and enabled are required together" }, 400);
944
+ }
945
+ if (Object.hasOwn(body, "setAll")) {
946
+ return jsonResponse({ error: "setAll cannot be combined with provider updates" }, 400);
947
+ }
948
+ }
949
+
950
+ // Branch 1: per-provider toggle (checked first: a per-provider request may carry an
951
+ // explicit `value`, which must never fall through to the global-value branch). Enable
952
+ // writes the current global default unless an explicit per-provider value is supplied;
953
+ // that value is never copied to other providers.
954
+ if (typeof body.provider === "string" && typeof body.enabled === "boolean") {
955
+ const provider = body.provider.trim();
956
+ if (!isValidProviderName(provider)) {
957
+ return jsonResponse({ error: "provider name must use letters, numbers, dot, underscore, or hyphen and cannot be a reserved object key" }, 400);
958
+ }
959
+ if (!hasOwnProvider(config.providers, provider)) {
960
+ return jsonResponse({ error: "unknown provider" }, 404);
961
+ }
962
+ // Validate a supplied per-provider value before mutating anything: it must be a
963
+ // finite number, and after flooring it must still be >= 1 — a value like 0.5 would
964
+ // otherwise floor to 0 and silently fall back to the global default.
965
+ if (body.value !== undefined && (typeof body.value !== "number" || !Number.isFinite(body.value))) {
966
+ return jsonResponse({ error: "value must be a positive number" }, 400);
967
+ }
968
+ const perProviderValue = typeof body.value === "number" ? Math.floor(body.value) : undefined;
969
+ if (perProviderValue !== undefined && perProviderValue < 1) {
970
+ return jsonResponse({ error: "value must be a positive number" }, 400);
971
+ }
972
+ setProviderContextCap(config, provider, body.enabled, perProviderValue);
973
+ save(config);
974
+ reconcileLiveStateStores();
975
+ clearModelCache(provider);
976
+ const catalogRefresh = await convergeCodexCatalog();
977
+ return respond(catalogRefresh);
978
+ }
979
+
980
+ // Branch 2: set the global cap value. When `setAll` accompanies it (dashboard "apply to
981
+ // every routed provider" toggle), re-point every enabled provider; otherwise keep each
982
+ // provider's own cap value and only change the default for future toggles.
983
+ if (body.value !== undefined) {
984
+ // Same normalization as the per-provider branch: floor before validating so a value
985
+ // that floors to zero is rejected rather than silently stored.
986
+ if (typeof body.value !== "number" || !Number.isFinite(body.value)) {
987
+ return jsonResponse({ error: "value must be a positive number" }, 400);
988
+ }
989
+ const normalizedValue = Math.floor(body.value);
990
+ if (normalizedValue < 1) {
991
+ return jsonResponse({ error: "value must be a positive number" }, 400);
992
+ }
993
+ if (body.setAll !== undefined && typeof body.setAll !== "boolean") {
994
+ return jsonResponse({ error: "setAll must be a boolean" }, 400);
995
+ }
996
+ const affected = Object.keys(providerContextCaps(config));
997
+ const applyToAll = body.setAll === true;
998
+ setGlobalContextCapValue(config, normalizedValue, applyToAll);
999
+ save(config);
1000
+ reconcileLiveStateStores();
1001
+ if (applyToAll) {
1002
+ for (const provider of affected) clearModelCache(provider);
1003
+ }
1004
+ const catalogRefresh = await convergeCodexCatalog();
1005
+ return respond(catalogRefresh);
1006
+ }
1007
+
1008
+ // Branch 3: enable/clear the cap for every provider at once.
1009
+ if (body.setAll !== undefined) {
1010
+ if (typeof body.setAll !== "boolean") {
1011
+ return jsonResponse({ error: "setAll must be a boolean" }, 400);
1012
+ }
1013
+ const before = Object.keys(providerContextCaps(config));
1014
+ const names = Object.keys(config.providers);
1015
+ setAllProviderContextCaps(config, names, body.setAll);
1016
+ save(config);
1017
+ reconcileLiveStateStores();
1018
+ for (const provider of new Set([...before, ...names])) clearModelCache(provider);
1019
+ const catalogRefresh = await convergeCodexCatalog();
1020
+ return respond(catalogRefresh);
1021
+ }
1022
+
1023
+ // Unrecognized payload: reject rather than silently succeeding.
1024
+ return jsonResponse({ error: "provider string and enabled boolean are required" }, 400);
1025
+ }
1026
+
1027
+ // Complete GUI picker presets, derived from the canonical provider registry. The GUI is a
1028
+ // standalone Vite package, so it consumes this runtime view instead of importing repo-root src.
1029
+ if (url.pathname === "/api/provider-presets" && req.method === "GET") {
1030
+ return jsonResponse({ providers: deriveProviderPresets() });
1031
+ }
1032
+ return null;
1033
+ }