@iislee/opencodex 2.11.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 (476) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +225 -0
  3. package/assets/architecture.png +0 -0
  4. package/assets/banner.png +0 -0
  5. package/assets/claude-code-models.gif +0 -0
  6. package/assets/codex-app-picker.png +0 -0
  7. package/bin/ocx.mjs +451 -0
  8. package/bin/package-main.mjs +9 -0
  9. package/gui/dist/assets/index-DTpMHS4F.js +67 -0
  10. package/gui/dist/assets/index-ZNVDE3C7.css +1 -0
  11. package/gui/dist/favicon.png +0 -0
  12. package/gui/dist/icons.svg +24 -0
  13. package/gui/dist/index.html +25 -0
  14. package/gui/dist/logo.png +0 -0
  15. package/gui/dist/provider-icons/alibaba-color.svg +1 -0
  16. package/gui/dist/provider-icons/antigravity-color.svg +1 -0
  17. package/gui/dist/provider-icons/antigravity.svg +1 -0
  18. package/gui/dist/provider-icons/claude-color.svg +1 -0
  19. package/gui/dist/provider-icons/claude.svg +1 -0
  20. package/gui/dist/provider-icons/cloudflare-ai-gateway-color.svg +1 -0
  21. package/gui/dist/provider-icons/copilot-color.svg +1 -0
  22. package/gui/dist/provider-icons/copilot.svg +1 -0
  23. package/gui/dist/provider-icons/cursor-color.svg +2 -0
  24. package/gui/dist/provider-icons/cursor.svg +2 -0
  25. package/gui/dist/provider-icons/deepseek-color.svg +1 -0
  26. package/gui/dist/provider-icons/discord.svg +1 -0
  27. package/gui/dist/provider-icons/firepass-color.svg +1 -0
  28. package/gui/dist/provider-icons/fireworks-color.svg +1 -0
  29. package/gui/dist/provider-icons/gemini-color.svg +1 -0
  30. package/gui/dist/provider-icons/gemini.svg +1 -0
  31. package/gui/dist/provider-icons/github-copilot-color.svg +1 -0
  32. package/gui/dist/provider-icons/gitlab-duo-color.svg +1 -0
  33. package/gui/dist/provider-icons/grok-color.svg +1 -0
  34. package/gui/dist/provider-icons/grok.svg +1 -0
  35. package/gui/dist/provider-icons/groq-color.svg +1 -0
  36. package/gui/dist/provider-icons/huggingface-color.svg +1 -0
  37. package/gui/dist/provider-icons/kimi-color.svg +1 -0
  38. package/gui/dist/provider-icons/kiro-color.svg +15 -0
  39. package/gui/dist/provider-icons/kiro.svg +14 -0
  40. package/gui/dist/provider-icons/lm-studio-color.svg +1 -0
  41. package/gui/dist/provider-icons/mistral-color.svg +1 -0
  42. package/gui/dist/provider-icons/moonshot-color.svg +1 -0
  43. package/gui/dist/provider-icons/nvidia-color.svg +1 -0
  44. package/gui/dist/provider-icons/ollama-color.svg +1 -0
  45. package/gui/dist/provider-icons/openai.svg +1 -0
  46. package/gui/dist/provider-icons/opencode.svg +1 -0
  47. package/gui/dist/provider-icons/openrouter-color.svg +1 -0
  48. package/gui/dist/provider-icons/pi.svg +21 -0
  49. package/gui/dist/provider-icons/qianfan-color.svg +1 -0
  50. package/gui/dist/provider-icons/qwen-portal-color.svg +1 -0
  51. package/gui/dist/provider-icons/telegram.svg +1 -0
  52. package/gui/dist/provider-icons/vercel-ai-gateway-color.svg +1 -0
  53. package/gui/dist/provider-icons/vllm-color.svg +1 -0
  54. package/gui/dist/provider-icons/xiaomi-color.svg +1 -0
  55. package/package.json +102 -0
  56. package/src/AGENTS.md +28 -0
  57. package/src/adapters/anthropic-image-guard.ts +251 -0
  58. package/src/adapters/anthropic-image-normalize.ts +518 -0
  59. package/src/adapters/anthropic.ts +1003 -0
  60. package/src/adapters/azure.ts +36 -0
  61. package/src/adapters/base.ts +72 -0
  62. package/src/adapters/client-fingerprint.ts +59 -0
  63. package/src/adapters/cursor/arg-codec.ts +38 -0
  64. package/src/adapters/cursor/arg-normalize.ts +104 -0
  65. package/src/adapters/cursor/cursor-errors.ts +165 -0
  66. package/src/adapters/cursor/discovery.ts +276 -0
  67. package/src/adapters/cursor/effort-map.ts +127 -0
  68. package/src/adapters/cursor/exec-policy.ts +88 -0
  69. package/src/adapters/cursor/framing.ts +211 -0
  70. package/src/adapters/cursor/gen/agent_pb.ts +15274 -0
  71. package/src/adapters/cursor/kv-store.ts +52 -0
  72. package/src/adapters/cursor/live-models.ts +153 -0
  73. package/src/adapters/cursor/live-smoke-gate.ts +41 -0
  74. package/src/adapters/cursor/live-transport.ts +1214 -0
  75. package/src/adapters/cursor/mcp-config.ts +42 -0
  76. package/src/adapters/cursor/mcp-manager.ts +333 -0
  77. package/src/adapters/cursor/message-mapper.ts +49 -0
  78. package/src/adapters/cursor/native-exec-common.ts +55 -0
  79. package/src/adapters/cursor/native-exec-desktop.ts +184 -0
  80. package/src/adapters/cursor/native-exec-fs.ts +329 -0
  81. package/src/adapters/cursor/native-exec-mcp.ts +153 -0
  82. package/src/adapters/cursor/native-exec-network.ts +43 -0
  83. package/src/adapters/cursor/native-exec-shell.ts +548 -0
  84. package/src/adapters/cursor/native-exec-tools.ts +118 -0
  85. package/src/adapters/cursor/native-exec.ts +576 -0
  86. package/src/adapters/cursor/protobuf-events.ts +563 -0
  87. package/src/adapters/cursor/protobuf-request.ts +714 -0
  88. package/src/adapters/cursor/request-builder.ts +255 -0
  89. package/src/adapters/cursor/thread-continuity.ts +67 -0
  90. package/src/adapters/cursor/tool-definitions.ts +505 -0
  91. package/src/adapters/cursor/transport-retry.ts +132 -0
  92. package/src/adapters/cursor/transport.ts +57 -0
  93. package/src/adapters/cursor/types.ts +52 -0
  94. package/src/adapters/cursor.ts +196 -0
  95. package/src/adapters/google-antigravity-replay.ts +303 -0
  96. package/src/adapters/google-antigravity-wire.ts +108 -0
  97. package/src/adapters/google-errors.ts +85 -0
  98. package/src/adapters/google-http.ts +100 -0
  99. package/src/adapters/google-tool-schema.ts +173 -0
  100. package/src/adapters/google-truncation.ts +13 -0
  101. package/src/adapters/google-wire-compiler.ts +232 -0
  102. package/src/adapters/google.ts +758 -0
  103. package/src/adapters/identity.ts +44 -0
  104. package/src/adapters/image.ts +23 -0
  105. package/src/adapters/kiro-constants.ts +16 -0
  106. package/src/adapters/kiro-errors.ts +197 -0
  107. package/src/adapters/kiro-events.ts +179 -0
  108. package/src/adapters/kiro-images.ts +129 -0
  109. package/src/adapters/kiro-retry.ts +312 -0
  110. package/src/adapters/kiro-thinking.ts +96 -0
  111. package/src/adapters/kiro-tool-fallback.ts +36 -0
  112. package/src/adapters/kiro-tools.ts +215 -0
  113. package/src/adapters/kiro-truncation.ts +33 -0
  114. package/src/adapters/kiro-wire.ts +129 -0
  115. package/src/adapters/kiro.ts +1898 -0
  116. package/src/adapters/mimo-free.ts +263 -0
  117. package/src/adapters/openai-chat.ts +1005 -0
  118. package/src/adapters/openai-responses.ts +1137 -0
  119. package/src/adapters/run-turn-queue.ts +114 -0
  120. package/src/adapters/tool-catalog-nudge.ts +71 -0
  121. package/src/adapters/upstream-http-error.ts +48 -0
  122. package/src/bridge.ts +1619 -0
  123. package/src/chat/inbound.ts +295 -0
  124. package/src/chat/outbound.ts +765 -0
  125. package/src/claude/agents-inject.ts +243 -0
  126. package/src/claude/alias.ts +149 -0
  127. package/src/claude/auth-detect.ts +229 -0
  128. package/src/claude/auth-mode-migration.ts +32 -0
  129. package/src/claude/auth-mode.ts +62 -0
  130. package/src/claude/context-windows.ts +189 -0
  131. package/src/claude/desktop-3p-guard.ts +35 -0
  132. package/src/claude/desktop-3p-paths.ts +84 -0
  133. package/src/claude/desktop-3p.ts +381 -0
  134. package/src/claude/desktop-health.ts +26 -0
  135. package/src/claude/desktop-profile.ts +263 -0
  136. package/src/claude/gateway-cache.ts +70 -0
  137. package/src/claude/inbound-debug.ts +163 -0
  138. package/src/claude/inbound.ts +509 -0
  139. package/src/claude/model-info.ts +151 -0
  140. package/src/claude/outbound.ts +872 -0
  141. package/src/cli/access.ts +108 -0
  142. package/src/cli/account-api.ts +268 -0
  143. package/src/cli/account-auth.ts +223 -0
  144. package/src/cli/account-extended.ts +350 -0
  145. package/src/cli/account.ts +275 -0
  146. package/src/cli/agent-driven.ts +70 -0
  147. package/src/cli/agent.ts +184 -0
  148. package/src/cli/catalog-prewarm.ts +27 -0
  149. package/src/cli/claude-desktop.ts +188 -0
  150. package/src/cli/claude.ts +286 -0
  151. package/src/cli/codex-shim-autorestore.ts +45 -0
  152. package/src/cli/combo.ts +119 -0
  153. package/src/cli/config-command.ts +145 -0
  154. package/src/cli/debug.ts +228 -0
  155. package/src/cli/doctor.ts +930 -0
  156. package/src/cli/export-command.ts +187 -0
  157. package/src/cli/help.ts +354 -0
  158. package/src/cli/index.ts +1113 -0
  159. package/src/cli/init.ts +224 -0
  160. package/src/cli/integrations.ts +142 -0
  161. package/src/cli/interactive-confirm.ts +133 -0
  162. package/src/cli/internal-dispatch.ts +20 -0
  163. package/src/cli/models-runtime.ts +212 -0
  164. package/src/cli/models.ts +336 -0
  165. package/src/cli/observe.ts +117 -0
  166. package/src/cli/opencode.ts +586 -0
  167. package/src/cli/pi.ts +188 -0
  168. package/src/cli/provider-runtime.ts +162 -0
  169. package/src/cli/provider.ts +463 -0
  170. package/src/cli/runtime-api.ts +325 -0
  171. package/src/cli/star-prompt.ts +155 -0
  172. package/src/cli/status-oauth.ts +78 -0
  173. package/src/cli/status.ts +321 -0
  174. package/src/cli/sync-cloud.ts +283 -0
  175. package/src/cli/system-command.ts +112 -0
  176. package/src/cli/tray-proxy.ts +52 -0
  177. package/src/cli/v2.ts +173 -0
  178. package/src/cli.ts +10 -0
  179. package/src/clients/config-export.ts +377 -0
  180. package/src/clients/effective-status.ts +385 -0
  181. package/src/clients/probes/agy.ts +55 -0
  182. package/src/clients/probes/cc-switch.ts +110 -0
  183. package/src/clients/probes/claude.ts +90 -0
  184. package/src/clients/probes/codex.ts +125 -0
  185. package/src/clients/probes/grok.ts +29 -0
  186. package/src/clients/probes/opencode.ts +109 -0
  187. package/src/clients/probes/paseo.ts +55 -0
  188. package/src/clients/probes/pi.ts +55 -0
  189. package/src/cloud/onedrive-auth.ts +666 -0
  190. package/src/cloud/onedrive-graph.ts +108 -0
  191. package/src/cloud/settings.ts +75 -0
  192. package/src/cloud/sync.ts +212 -0
  193. package/src/cloud/types.ts +56 -0
  194. package/src/cloud/vault.ts +89 -0
  195. package/src/codex/account-id.ts +34 -0
  196. package/src/codex/account-label.ts +34 -0
  197. package/src/codex/account-lifecycle.ts +55 -0
  198. package/src/codex/account-namespace-match.ts +63 -0
  199. package/src/codex/account-namespaces.ts +149 -0
  200. package/src/codex/account-pause.ts +20 -0
  201. package/src/codex/account-runtime-state.ts +31 -0
  202. package/src/codex/account-store.ts +517 -0
  203. package/src/codex/account-usability.ts +20 -0
  204. package/src/codex/app-server-processes.ts +756 -0
  205. package/src/codex/auth-api.ts +1540 -0
  206. package/src/codex/auth-collision.ts +107 -0
  207. package/src/codex/auth-context.ts +352 -0
  208. package/src/codex/autostart-health.ts +149 -0
  209. package/src/codex/catalog/aggregation.ts +378 -0
  210. package/src/codex/catalog/bundled.ts +251 -0
  211. package/src/codex/catalog/effort.ts +355 -0
  212. package/src/codex/catalog/metadata.ts +180 -0
  213. package/src/codex/catalog/parsing.ts +456 -0
  214. package/src/codex/catalog/provider-fetch.ts +902 -0
  215. package/src/codex/catalog/sync.ts +620 -0
  216. package/src/codex/catalog.ts +12 -0
  217. package/src/codex/data/upstream-models.json +830 -0
  218. package/src/codex/exec-invocation.ts +22 -0
  219. package/src/codex/features.ts +969 -0
  220. package/src/codex/history-migration-guardian.ts +102 -0
  221. package/src/codex/history-provider.ts +776 -0
  222. package/src/codex/home.ts +206 -0
  223. package/src/codex/inject.ts +799 -0
  224. package/src/codex/injected-marker.ts +72 -0
  225. package/src/codex/journal.ts +163 -0
  226. package/src/codex/main-account-cache.ts +32 -0
  227. package/src/codex/main-account.ts +40 -0
  228. package/src/codex/model-cache.ts +227 -0
  229. package/src/codex/paths.ts +65 -0
  230. package/src/codex/plugins-doctor.ts +242 -0
  231. package/src/codex/pool-rotation.ts +225 -0
  232. package/src/codex/project-config-warnings.ts +411 -0
  233. package/src/codex/quota.ts +411 -0
  234. package/src/codex/refresh.ts +53 -0
  235. package/src/codex/routing.ts +1477 -0
  236. package/src/codex/runtime.ts +538 -0
  237. package/src/codex/shim.ts +1189 -0
  238. package/src/codex/subagent-defaults.ts +550 -0
  239. package/src/codex/subagent-model-fallback.ts +469 -0
  240. package/src/codex/sync.ts +130 -0
  241. package/src/codex/warmup.ts +192 -0
  242. package/src/codex/websocket-registry.ts +100 -0
  243. package/src/combos/failover.ts +140 -0
  244. package/src/combos/index.ts +41 -0
  245. package/src/combos/request.ts +62 -0
  246. package/src/combos/resolve.ts +232 -0
  247. package/src/combos/types.ts +326 -0
  248. package/src/config.ts +2356 -0
  249. package/src/generated/jawcode-model-metadata.ts +104 -0
  250. package/src/github/star-state.ts +203 -0
  251. package/src/grok/inject.ts +545 -0
  252. package/src/grok/status.ts +121 -0
  253. package/src/grok/sync.ts +103 -0
  254. package/src/grok/usage-hook/report.mjs +348 -0
  255. package/src/grok/usage-hook.ts +278 -0
  256. package/src/images/artifacts.ts +516 -0
  257. package/src/images/fulfill-video.ts +163 -0
  258. package/src/images/fulfill.ts +149 -0
  259. package/src/images/index.ts +4 -0
  260. package/src/images/loop.ts +829 -0
  261. package/src/images/plan.ts +133 -0
  262. package/src/images/synthetic-tool.ts +133 -0
  263. package/src/images/types.ts +41 -0
  264. package/src/images/xai-client.ts +141 -0
  265. package/src/images/xai-video-client.ts +163 -0
  266. package/src/index.ts +22 -0
  267. package/src/lib/abort.ts +146 -0
  268. package/src/lib/admin-secrets.ts +25 -0
  269. package/src/lib/admission.ts +83 -0
  270. package/src/lib/app-owned-memory-stores.ts +173 -0
  271. package/src/lib/app-owned-memory.ts +265 -0
  272. package/src/lib/bounded-body.ts +202 -0
  273. package/src/lib/bun-binary-validator.d.mts +3 -0
  274. package/src/lib/bun-binary-validator.mjs +18 -0
  275. package/src/lib/bun-runtime.ts +71 -0
  276. package/src/lib/bun-stream-caps.ts +126 -0
  277. package/src/lib/config-ownership.ts +360 -0
  278. package/src/lib/crash-guard.ts +344 -0
  279. package/src/lib/debug-log-buffer.ts +83 -0
  280. package/src/lib/debug-settings.ts +108 -0
  281. package/src/lib/debug.ts +31 -0
  282. package/src/lib/destination-policy.ts +316 -0
  283. package/src/lib/errors.ts +364 -0
  284. package/src/lib/eventstream-decoder.ts +253 -0
  285. package/src/lib/gcp-adc.ts +341 -0
  286. package/src/lib/injection-debug-log.ts +58 -0
  287. package/src/lib/open-url.ts +25 -0
  288. package/src/lib/pinned-http.ts +151 -0
  289. package/src/lib/privacy.ts +20 -0
  290. package/src/lib/process-control.ts +165 -0
  291. package/src/lib/provider-outbound.ts +170 -0
  292. package/src/lib/provider-url.ts +14 -0
  293. package/src/lib/proxy-env.ts +18 -0
  294. package/src/lib/redact.ts +105 -0
  295. package/src/lib/retry-after.ts +55 -0
  296. package/src/lib/service-secrets.ts +25 -0
  297. package/src/lib/shadow-call.ts +30 -0
  298. package/src/lib/sidecar-tracker.ts +52 -0
  299. package/src/lib/sse-decoder.ts +323 -0
  300. package/src/lib/state-store-registrations.ts +109 -0
  301. package/src/lib/state-store-sweeper.ts +184 -0
  302. package/src/lib/test-home-guard.ts +90 -0
  303. package/src/lib/token-estimate.ts +69 -0
  304. package/src/lib/translator-budget.ts +356 -0
  305. package/src/lib/upstream-retry.ts +239 -0
  306. package/src/lib/win-exec.ts +115 -0
  307. package/src/lib/win-paths.ts +68 -0
  308. package/src/lib/windows-elevation.ts +705 -0
  309. package/src/lib/windows-secret-acl.ts +514 -0
  310. package/src/lib/winsw.ts +375 -0
  311. package/src/oauth/anthropic-routing.ts +594 -0
  312. package/src/oauth/anthropic.ts +177 -0
  313. package/src/oauth/callback-server.ts +294 -0
  314. package/src/oauth/chatgpt.ts +150 -0
  315. package/src/oauth/cursor.ts +211 -0
  316. package/src/oauth/github-copilot.ts +428 -0
  317. package/src/oauth/google-antigravity.ts +230 -0
  318. package/src/oauth/health.ts +399 -0
  319. package/src/oauth/index.ts +1174 -0
  320. package/src/oauth/key-providers.ts +108 -0
  321. package/src/oauth/kimi.ts +213 -0
  322. package/src/oauth/kiro-credentials.ts +726 -0
  323. package/src/oauth/kiro.ts +577 -0
  324. package/src/oauth/local-token-detect.ts +121 -0
  325. package/src/oauth/log.ts +48 -0
  326. package/src/oauth/login-cli.ts +163 -0
  327. package/src/oauth/pkce.ts +15 -0
  328. package/src/oauth/store.ts +630 -0
  329. package/src/oauth/token-guardian.ts +303 -0
  330. package/src/oauth/types.ts +62 -0
  331. package/src/oauth/xai.ts +241 -0
  332. package/src/pi/extensions.ts +72 -0
  333. package/src/pi/home.ts +42 -0
  334. package/src/pi/index.ts +40 -0
  335. package/src/pi/models.ts +278 -0
  336. package/src/pi/packages.ts +219 -0
  337. package/src/pi/settings.ts +365 -0
  338. package/src/pi/status.ts +68 -0
  339. package/src/pi/sync.ts +75 -0
  340. package/src/providers/alibaba-region-backup.ts +75 -0
  341. package/src/providers/alibaba-region-migration.ts +156 -0
  342. package/src/providers/alibaba-region-startup.ts +36 -0
  343. package/src/providers/antigravity-models.ts +205 -0
  344. package/src/providers/api-keys.ts +140 -0
  345. package/src/providers/base-url-choices.ts +64 -0
  346. package/src/providers/context-cap.ts +65 -0
  347. package/src/providers/derive.ts +339 -0
  348. package/src/providers/free-directory.ts +184 -0
  349. package/src/providers/github-copilot-transport.ts +56 -0
  350. package/src/providers/key-failover.ts +203 -0
  351. package/src/providers/kiro-models.ts +67 -0
  352. package/src/providers/label.ts +19 -0
  353. package/src/providers/model-discovery.ts +356 -0
  354. package/src/providers/openai-sidecar.ts +175 -0
  355. package/src/providers/openai-tier-startup.ts +27 -0
  356. package/src/providers/openai-tiers.ts +301 -0
  357. package/src/providers/openai-virtual-models.ts +82 -0
  358. package/src/providers/openrouter-routing.ts +102 -0
  359. package/src/providers/provider-id-rewrite.ts +150 -0
  360. package/src/providers/quota.ts +1260 -0
  361. package/src/providers/registry.ts +1600 -0
  362. package/src/providers/slug-codec.ts +67 -0
  363. package/src/providers/xai-transport.ts +141 -0
  364. package/src/reasoning-effort.ts +135 -0
  365. package/src/responses/compaction.ts +117 -0
  366. package/src/responses/parser.ts +656 -0
  367. package/src/responses/reasoning-envelope.ts +52 -0
  368. package/src/responses/schema.ts +159 -0
  369. package/src/responses/spill-store.ts +394 -0
  370. package/src/responses/state.ts +895 -0
  371. package/src/responses/tool-groups.ts +19 -0
  372. package/src/router.ts +425 -0
  373. package/src/server/adapter-resolve.ts +80 -0
  374. package/src/server/auth-cors.ts +530 -0
  375. package/src/server/chat-completions.ts +368 -0
  376. package/src/server/claude-messages.ts +914 -0
  377. package/src/server/effort-policy.ts +172 -0
  378. package/src/server/gui-static.ts +123 -0
  379. package/src/server/image-retry.ts +42 -0
  380. package/src/server/images.ts +476 -0
  381. package/src/server/index.ts +1126 -0
  382. package/src/server/lifecycle.ts +227 -0
  383. package/src/server/live.ts +598 -0
  384. package/src/server/management/agent-settings-routes.ts +1169 -0
  385. package/src/server/management/api-access.ts +141 -0
  386. package/src/server/management/api-key-usage.ts +167 -0
  387. package/src/server/management/body.ts +35 -0
  388. package/src/server/management/clients-routes.ts +63 -0
  389. package/src/server/management/cloud-sync-routes.ts +266 -0
  390. package/src/server/management/combo-routes.ts +220 -0
  391. package/src/server/management/config-routes.ts +422 -0
  392. package/src/server/management/context.ts +31 -0
  393. package/src/server/management/logs-usage-routes.ts +707 -0
  394. package/src/server/management/model-routes.ts +525 -0
  395. package/src/server/management/oauth-account-routes.ts +563 -0
  396. package/src/server/management/provider-routes.ts +556 -0
  397. package/src/server/management/shared.ts +277 -0
  398. package/src/server/management/sidebar-routes.ts +90 -0
  399. package/src/server/management/system-restart.ts +179 -0
  400. package/src/server/management/system-routes.ts +117 -0
  401. package/src/server/management/usage-summary-cache.ts +86 -0
  402. package/src/server/management-api.ts +215 -0
  403. package/src/server/management-auth.ts +267 -0
  404. package/src/server/memory-watchdog.ts +156 -0
  405. package/src/server/port-reclaim.ts +307 -0
  406. package/src/server/ports.ts +116 -0
  407. package/src/server/proxy-liveness.ts +201 -0
  408. package/src/server/relay-eager.ts +313 -0
  409. package/src/server/relay.ts +1049 -0
  410. package/src/server/request-decompress.ts +132 -0
  411. package/src/server/request-log-conversation.ts +168 -0
  412. package/src/server/request-log.ts +1046 -0
  413. package/src/server/responses/collaboration.ts +354 -0
  414. package/src/server/responses/compact.ts +384 -0
  415. package/src/server/responses/core.ts +2758 -0
  416. package/src/server/responses/encrypted-payload.ts +308 -0
  417. package/src/server/responses/fetch-helpers.ts +157 -0
  418. package/src/server/responses/passthrough-error.ts +78 -0
  419. package/src/server/responses/terminal-guard.ts +230 -0
  420. package/src/server/responses/upstream-error.ts +48 -0
  421. package/src/server/responses-image-gen-repair.ts +132 -0
  422. package/src/server/responses-item-id-repair.ts +224 -0
  423. package/src/server/responses.ts +9 -0
  424. package/src/server/search.ts +136 -0
  425. package/src/server/sse-payload-rewrite.ts +175 -0
  426. package/src/server/startup-action-control.ts +308 -0
  427. package/src/server/startup-health-cache.ts +113 -0
  428. package/src/server/system-env.ts +413 -0
  429. package/src/server/windows-tcp-drop.ts +184 -0
  430. package/src/server/windows-tray-control.ts +41 -0
  431. package/src/server/ws-bridge.ts +471 -0
  432. package/src/service.ts +2554 -0
  433. package/src/stall-timeout.ts +20 -0
  434. package/src/storage/cleanup-job.ts +57 -0
  435. package/src/storage/cleanup.ts +3085 -0
  436. package/src/storage/policy-job.ts +457 -0
  437. package/src/storage/policy-scheduler.ts +40 -0
  438. package/src/storage/policy-worker.ts +59 -0
  439. package/src/storage/policy.ts +527 -0
  440. package/src/storage/restore-job.ts +299 -0
  441. package/src/storage/restore-worker.ts +58 -0
  442. package/src/storage/scanner.ts +238 -0
  443. package/src/storage/storage-mutation-coordinator.ts +139 -0
  444. package/src/storage/worker-lifecycle.ts +215 -0
  445. package/src/tray/assets/opencodex-tray-offline.ico +0 -0
  446. package/src/tray/assets/opencodex-tray-online.ico +0 -0
  447. package/src/tray/assets/opencodex-tray-warning.ico +0 -0
  448. package/src/tray/assets/opencodex-tray.png +0 -0
  449. package/src/tray/windows-tray.ps1 +290 -0
  450. package/src/tray/windows.ts +730 -0
  451. package/src/types.ts +1237 -0
  452. package/src/update/badge.ts +72 -0
  453. package/src/update/index.ts +407 -0
  454. package/src/update/job.ts +1520 -0
  455. package/src/update/notify.ts +257 -0
  456. package/src/update/npm-invocation.d.mts +23 -0
  457. package/src/update/npm-invocation.mjs +94 -0
  458. package/src/update/tray-update-plan.d.mts +18 -0
  459. package/src/update/tray-update-plan.mjs +38 -0
  460. package/src/usage/cost.ts +0 -0
  461. package/src/usage/debug.ts +97 -0
  462. package/src/usage/expected-prices.ts +164 -0
  463. package/src/usage/log.ts +658 -0
  464. package/src/usage/summary.ts +585 -0
  465. package/src/usage/totals.ts +14 -0
  466. package/src/vision/anthropic-describe.ts +185 -0
  467. package/src/vision/describe.ts +125 -0
  468. package/src/vision/index.ts +467 -0
  469. package/src/web-search/anthropic-executor.ts +189 -0
  470. package/src/web-search/executor.ts +105 -0
  471. package/src/web-search/format-result.ts +89 -0
  472. package/src/web-search/index.ts +196 -0
  473. package/src/web-search/loop.ts +664 -0
  474. package/src/web-search/parse.ts +220 -0
  475. package/src/web-search/progress-stream.ts +342 -0
  476. package/src/web-search/synthetic-tool.ts +47 -0
@@ -0,0 +1,525 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { readFileSync } from "node:fs";
3
+
4
+ /**
5
+ * Codex parses a catalog entry's `input_modalities` as a closed enum, and one out-of-enum
6
+ * value makes it reject the ENTIRE catalog file — plugins, apps and MCP servers all stop
7
+ * loading over one model's metadata (#759).
8
+ *
9
+ * The catalog writer normalizes on the way out, but a rejected value stored here would still
10
+ * be handed back to the GUI and CLI as if it were real, and the offline `ocx models add` path
11
+ * already refuses it. Validate at ingress so all three paths agree.
12
+ */
13
+ const ALLOWED_INPUT_MODALITIES = new Set(["text", "image", "audio"]);
14
+
15
+ function readInputModalities(raw: unknown): { values?: string[]; error?: string } {
16
+ if (raw === undefined) return {};
17
+ if (!Array.isArray(raw)) return { error: "inputModalities must be an array" };
18
+ // Reject non-strings rather than filtering them out. Dropping them silently accepted a
19
+ // malformed POST and, worse, let a PUT of `[42]` clear the stored modalities while
20
+ // answering 200 — the opposite of the contract this validator exists to state. An empty
21
+ // array stays valid: that is how `ocx models edit --modalities -` clears the field.
22
+ const rejected: string[] = [];
23
+ for (const value of raw) {
24
+ if (typeof value !== "string") return { error: "inputModalities must contain only strings" };
25
+ if (!ALLOWED_INPUT_MODALITIES.has(value)) rejected.push(value);
26
+ }
27
+ if (rejected.length > 0) {
28
+ return { error: `unsupported input modality: ${rejected.join(", ")} (allowed: text, image, audio)` };
29
+ }
30
+ return { values: raw as string[] };
31
+ }
32
+ import type { CatalogModel } from "../../codex/catalog";
33
+ import { catalogModelSlug, disabledNativeSlugs, invalidateCodexModelsCache, nativeModelRows, uniqueCatalogModelsForPublicList } from "../../codex/catalog";
34
+ import { CatalogGatherBusyError } from "../../codex/catalog/provider-fetch";
35
+ import { getProviderLiveModelCount } from "../../codex/model-cache";
36
+ import {
37
+ DEFAULT_SUBAGENT_MODELS,
38
+ codexAutoStartEnabled,
39
+ hasOwnProvider,
40
+ isValidProviderName,
41
+ multiAgentGuidanceEnabled,
42
+ providerBaseUrlConfigError,
43
+ providerHeadersConfigError,
44
+ saveConfigPreservingClaudeCode,
45
+ } from "../../config";
46
+ import {
47
+ clearLoginState,
48
+ getLoginStatus,
49
+ isPublicOAuthProvider,
50
+ listOAuthProviders,
51
+ startLoginFlow,
52
+ submitManualLoginCode,
53
+ upsertOAuthProvider,
54
+ } from "../../oauth";
55
+ import { removeCredential } from "../../oauth/store";
56
+ import { providerDestinationResolvedError } from "../../lib/destination-policy";
57
+ import { enrichProviderFromCatalog, listKeyLoginProviders } from "../../oauth/key-providers";
58
+ import { deriveProviderPresets } from "../../providers/derive";
59
+ import { providerCodexAccountMode } from "../../providers/registry";
60
+ import { routedSlug, slugEquals } from "../../providers/slug-codec";
61
+ import { COMBO_NAMESPACE, comboModelId, comboPublicModelId, preservesPhysicalComboProvider } from "../../combos";
62
+ import { clearProviderQuotaCache, fetchProviderQuotaReports } from "../../providers/quota";
63
+ import { isCanonicalOpenAiForwardProvider } from "../../providers/openai-tiers";
64
+ import { clearThreadAccountMap } from "../../codex/routing";
65
+ import { primeCodexPoolQuotas } from "../../codex/auth-api";
66
+ import { DEFAULT_PROVIDER_CONTEXT_CAP, globalContextCapValue, providerContextCap, providerContextCaps, setAllProviderContextCaps, setGlobalContextCapValue, setProviderContextCap } from "../../providers/context-cap";
67
+ import { resolveCodexHomeDir } from "../../codex/home";
68
+ import { readUsageEntries } from "../../usage/log";
69
+ import { getUsageDebugLogEntries } from "../../usage/debug";
70
+ import { parseRange, parseUsageSurface, summarizeUsage } from "../../usage/summary";
71
+ import { stripCodexRuntimeProviderFields } from "../../codex/auth-context";
72
+ import { getProviderRegistryEntry } from "../../providers/registry";
73
+ import { getDebugLogEntries } from "../../lib/debug-log-buffer";
74
+ import { getInjectionDebugLogEntries } from "../../lib/injection-debug-log";
75
+ import {
76
+ clearDebugSettings,
77
+ clearDebugSetting,
78
+ getDebugSettings,
79
+ setDebugSettings,
80
+ type DebugFlag,
81
+ } from "../../lib/debug-settings";
82
+ import type { OcxClaudeCodeConfig, OcxConfig, OcxCustomModel, OcxProviderConfig } from "../../types";
83
+ import { drainAndShutdown } from "../lifecycle";
84
+ import { filterRequestLogs, getRequestLogEntries, type RequestLogEntry } from "../request-log";
85
+ import { estimateComboCost, estimateRequestCost, normalizeCostTokens, tokensPerSecond } from "../../usage/cost";
86
+ import type { PersistedUsageAttempt } from "../../usage/log";
87
+ import { isAllowedRequestOrigin, jsonResponse, providerManagementConfigError, publicProviderBaseUrl, safeConfigDTO, corsHeaders } from "../auth-cors";
88
+ import { applySystemEnvToggle } from "../system-env";
89
+ import {
90
+ EXPORT_CLIENTS,
91
+ EXPORT_CLIENT_IDS,
92
+ OPENCODE_PROVIDER_ID,
93
+ buildClientConfig,
94
+ isExportClientId,
95
+ opencodeProxyBaseUrl,
96
+ } from "../../clients/config-export";
97
+ import type {
98
+ ExportClientId,
99
+ ExportModel,
100
+ OpencodeGeneratedConfig,
101
+ PiGeneratedConfig,
102
+ } from "../../clients/config-export";
103
+
104
+ import { isPlainRecord, parseDebugLogQuery, tokPerSecondResult, unavailableCostReason, costResult, requestLogDto, stripRegistryOnlyStaticHeaders, fetchAllModels } from "./shared";
105
+ import type { MetricUnavailableReason, TokPerSecondResult, CostEstimateReason, CostResult, MetricSource } from "./shared";
106
+ import type { ManagementContext } from "./context";
107
+ import { readManagementJsonBody, rethrowManagementBodyTooLarge } from "./body";
108
+
109
+ /**
110
+ * One row of the `/api/models` list. Routed rows spread a `CatalogModel`, so the shape is
111
+ * that model plus the identity/visibility fields this boundary computes for every row
112
+ * regardless of source. `disabled` is always present; the rest vary by row origin.
113
+ */
114
+ type ManagementModelRow = Partial<CatalogModel> & {
115
+ provider: string;
116
+ id: string;
117
+ namespaced: string;
118
+ disabled: boolean;
119
+ native?: boolean;
120
+ custom?: boolean;
121
+ customId?: string;
122
+ };
123
+
124
+ /**
125
+ * The exact row list `/api/models` returns. Extracted so `/api/client-config` exports the
126
+ * models the GUI's Models tab shows — including this function's `disabled` computation,
127
+ * which the export core (src/clients/config-export.ts) deliberately does not perform.
128
+ */
129
+ async function listManagementModelRows(config: OcxConfig): Promise<ManagementModelRow[]> {
130
+ const models = await fetchAllModels(config);
131
+ const disabled = new Set(config.disabledModels ?? []);
132
+ // Native GPT passthrough rows lead (provider "openai", bare-slug namespaced ids): sourced
133
+ // from the static supported set so a disabled model stays listed and re-enableable.
134
+ const native: ManagementModelRow[] = nativeModelRows(config).map(row => ({
135
+ provider: "openai",
136
+ id: row.slug,
137
+ namespaced: row.slug,
138
+ disabled: row.disabled,
139
+ native: true,
140
+ ...(row.contextWindow !== undefined ? { contextWindow: row.contextWindow } : {}),
141
+ }));
142
+ const customModels: ManagementModelRow[] = (config.customModels ?? []).map(cm => {
143
+ const namespaced = routedSlug(cm.provider, cm.modelId);
144
+ return {
145
+ provider: cm.provider,
146
+ id: cm.modelId,
147
+ namespaced,
148
+ disabled: [...disabled].some(stored => slugEquals(stored, cm.provider, cm.modelId)),
149
+ custom: true,
150
+ customId: cm.id,
151
+ displayName: cm.displayName,
152
+ ...(cm.contextWindow ? { contextWindow: cm.contextWindow } : {}),
153
+ ...(cm.inputModalities ? { inputModalities: cm.inputModalities } : {}),
154
+ };
155
+ });
156
+ const publicModels = uniqueCatalogModelsForPublicList(models);
157
+ const comboNamespaced = new Set(
158
+ publicModels.filter(model => model.provider === "combo").map(catalogModelSlug),
159
+ );
160
+ const visibleCustomModels = customModels.filter(model => !comboNamespaced.has(model.namespaced));
161
+ // Custom metadata wins when a physical live/static row resolves to the same Codex-facing
162
+ // slug, while a combo keeps the same precedence it has in routing and /v1/models.
163
+ const customNamespaced = new Set(visibleCustomModels.map(c => c.namespaced));
164
+ const dedupedRouted = publicModels.map((m): ManagementModelRow | null => {
165
+ // Codex-facing slug (one "/", slug-codec); disabledModels compares tolerate both forms.
166
+ const namespaced = catalogModelSlug(m);
167
+ if (m.provider !== "combo" && customNamespaced.has(namespaced)) return null;
168
+ const contextCap = providerContextCap(config, m.provider);
169
+ return {
170
+ ...m,
171
+ namespaced,
172
+ disabled: [...disabled].some(stored => (
173
+ stored === namespaced || slugEquals(stored, m.provider, m.id)
174
+ )),
175
+ ...(contextCap !== undefined ? { contextCap, contextCapped: m.contextCapped === true } : {}),
176
+ };
177
+ }).filter((row): row is ManagementModelRow => row !== null);
178
+ return [...native, ...dedupedRouted, ...visibleCustomModels];
179
+ }
180
+
181
+ /** `/api/models` row → the narrower input the client-config serializers accept. */
182
+ function toExportModel(row: ManagementModelRow): ExportModel {
183
+ return {
184
+ namespaced: row.namespaced,
185
+ provider: row.provider,
186
+ id: row.id,
187
+ ...(row.native ? { native: true } : {}),
188
+ ...(row.displayName ? { displayName: row.displayName } : {}),
189
+ ...(row.contextWindow !== undefined ? { contextWindow: row.contextWindow } : {}),
190
+ ...(row.inputModalities ? { inputModalities: row.inputModalities } : {}),
191
+ };
192
+ }
193
+
194
+ /**
195
+ * Counts read back off the SERIALIZED document rather than recomputed from the input rows.
196
+ * `modelsWithoutLimits` drives a GUI line claiming "these models ship without limits", so it
197
+ * has to describe the bytes the user actually receives — a parallel reimplementation of the
198
+ * core's "authoritative context window" rule would be free to drift from it silently.
199
+ */
200
+ function summarizeExportedModels(client: ExportClientId, document: unknown): { modelCount: number; modelsWithoutLimits: number } {
201
+ if (client === "opencode") {
202
+ const models = (document as OpencodeGeneratedConfig).provider[OPENCODE_PROVIDER_ID].models;
203
+ const entries = Object.values(models);
204
+ return { modelCount: entries.length, modelsWithoutLimits: entries.filter(entry => entry.limit === undefined).length };
205
+ }
206
+ const models = (document as PiGeneratedConfig).providers[OPENCODE_PROVIDER_ID].models;
207
+ return { modelCount: models.length, modelsWithoutLimits: models.filter(entry => entry.contextWindow === undefined).length };
208
+ }
209
+
210
+ export async function handleModelRoutes(ctx: ManagementContext): Promise<Response | null> {
211
+ const { req, url, config, deps, refreshCodexCatalogBestEffort, syncClaudeAgentDefsBestEffort } = ctx;
212
+ // A handler persists the exact config object passed in. Production defaults to
213
+ // the real store; tests that pass an in-memory fixture inject a no-op/spy. Do not
214
+ // bypass this seam with a dynamic config import — doing so replaced a user's
215
+ // ~/.opencodex/config.json with the `existing-uuid` test fixture.
216
+ const persistConfig = deps.saveConfigPreservingClaudeCode ?? saveConfigPreservingClaudeCode;
217
+
218
+ if (url.pathname === "/api/catalog" && req.method === "GET") {
219
+ const { readCatalog, readCodexCatalogPath } = await import("../../codex/catalog");
220
+ const catalog = readCatalog(readCodexCatalogPath());
221
+ if (!catalog) return jsonResponse({ error: "catalog not found" }, 404, req, config);
222
+ const headers: Record<string, string> = {
223
+ "Content-Type": "application/json",
224
+ ...corsHeaders(req, config),
225
+ };
226
+ const { loadPersistedCodexRuntime } = await import("../../codex/runtime");
227
+ const version = loadPersistedCodexRuntime()?.selectedVersion;
228
+ if (version) headers["x-opencodex-codex-version"] = version;
229
+ return new Response(JSON.stringify(catalog), { status: 200, headers });
230
+ }
231
+
232
+ if (url.pathname === "/api/models" && req.method === "GET") {
233
+ return jsonResponse(await listManagementModelRows(config));
234
+ }
235
+
236
+ /**
237
+ * Client config document for OpenCode / Pi, built from the SAME function `ocx export`
238
+ * calls, so the bytes a user downloads here and the bytes they pipe from the CLI cannot
239
+ * disagree. Read-only: this route never writes the user's client config.
240
+ */
241
+ if (url.pathname === "/api/client-config" && req.method === "GET") {
242
+ const requested = url.searchParams.get("client")?.trim() ?? "";
243
+ if (!isExportClientId(requested)) {
244
+ return jsonResponse(
245
+ { error: `client must be one of: ${EXPORT_CLIENT_IDS.join(", ")}` },
246
+ 400,
247
+ req,
248
+ config,
249
+ );
250
+ }
251
+ const spec = EXPORT_CLIENTS[requested];
252
+ let rows: ManagementModelRow[];
253
+ try {
254
+ rows = await listManagementModelRows(config);
255
+ } catch (error) {
256
+ // A partial or empty `models` block reads as a valid config while offering nothing,
257
+ // so a catalog failure is surfaced as unavailable rather than serialized. The
258
+ // catalog-busy error keeps its own 503 from handleManagementAPI.
259
+ if (error instanceof CatalogGatherBusyError) throw error;
260
+ return jsonResponse(
261
+ { error: `model catalog unavailable: ${error instanceof Error ? error.message : String(error)}` },
262
+ 503,
263
+ req,
264
+ config,
265
+ );
266
+ }
267
+ // The export core does not filter visibility — it serializes what it is given. A model the
268
+ // user disabled in the Models tab is absent from /v1/models, so exporting it would hand the
269
+ // client a selector the proxy refuses to route.
270
+ const models = rows.filter(row => !row.disabled).map(toExportModel);
271
+ const document = buildClientConfig(requested, {
272
+ baseUrl: opencodeProxyBaseUrl(Number(url.port) || config.port, config.hostname),
273
+ models,
274
+ config,
275
+ });
276
+ return jsonResponse({
277
+ client: spec.id,
278
+ filename: spec.filename,
279
+ destination: spec.destination(process.env),
280
+ apiKeyEnv: spec.apiKeyEnv,
281
+ exportHint: spec.exportHint,
282
+ ...summarizeExportedModels(requested, document),
283
+ config: document,
284
+ }, 200, req, config);
285
+ }
286
+
287
+ // Enable/disable models: which routed models Codex sees. PUT hides them from the catalog +
288
+ // /v1/models and invalidates Codex's 5-min models cache so it applies on the next turn.
289
+ if (url.pathname === "/api/disabled-models" && req.method === "PUT") {
290
+ let body: { models?: unknown };
291
+ try { body = await readManagementJsonBody(req); } catch (error) { rethrowManagementBodyTooLarge(error); return jsonResponse({ error: "invalid JSON body" }, 400); }
292
+ const disabled = Array.isArray(body.models) ? body.models.filter((m): m is string => typeof m === "string") : [];
293
+ config.disabledModels = disabled;
294
+ persistConfig(config);
295
+ await refreshCodexCatalogBestEffort();
296
+ return jsonResponse({ ok: true, disabled });
297
+ }
298
+
299
+ // One user-facing visibility switch spans two persisted filters: a provider allowlist and the
300
+ // shared blocklist. Keep the update atomic so an interrupted request cannot expose a half-applied
301
+ // state. Native rows only use the blocklist; routed/custom rows also join a non-empty allowlist.
302
+ if (url.pathname === "/api/model-visibility" && req.method === "PUT") {
303
+ let parsedBody: unknown;
304
+ try { parsedBody = await readManagementJsonBody(req); } catch (error) { rethrowManagementBodyTooLarge(error); return jsonResponse({ error: "invalid JSON body" }, 400); }
305
+ if (!isPlainRecord(parsedBody)) return jsonResponse({ error: "invalid model visibility request" }, 400);
306
+ const body = parsedBody;
307
+ const scope = body.scope === "models" || body.scope === "provider" ? body.scope : null;
308
+ const provider = typeof body.provider === "string" ? body.provider.trim() : "";
309
+ if (!scope || !provider || !isValidProviderName(provider) || typeof body.enabled !== "boolean" || !Array.isArray(body.targets)) {
310
+ return jsonResponse({ error: "invalid model visibility request" }, 400);
311
+ }
312
+
313
+ const providerConfig = hasOwnProvider(config.providers, provider) ? config.providers[provider] : undefined;
314
+ const isVirtualComboNamespace = provider === COMBO_NAMESPACE && !preservesPhysicalComboProvider(config);
315
+ if (!providerConfig && provider !== "openai" && !isVirtualComboNamespace) {
316
+ return jsonResponse({ error: "unknown model visibility provider" }, 400);
317
+ }
318
+ const supportedNative = new Set(nativeModelRows(config).map(row => row.slug));
319
+ const targets: Array<{ id: string; native: boolean }> = [];
320
+ const seen = new Set<string>();
321
+ for (const value of body.targets) {
322
+ if (!isPlainRecord(value) || typeof value.id !== "string" || (value.native !== undefined && typeof value.native !== "boolean")) {
323
+ return jsonResponse({ error: "invalid model visibility target" }, 400);
324
+ }
325
+ const id = value.id.trim();
326
+ const native = value.native === true;
327
+ if (!id || (provider === "openai") !== native || (native && !supportedNative.has(id))) {
328
+ return jsonResponse({ error: "invalid model visibility target" }, 400);
329
+ }
330
+ const key = `${native ? "native" : "routed"}:${id}`;
331
+ if (!seen.has(key)) {
332
+ seen.add(key);
333
+ targets.push({ id, native });
334
+ }
335
+ }
336
+ if (targets.length === 0) return jsonResponse({ error: "model visibility targets required" }, 400);
337
+
338
+ const knownComboSelectors = new Set(
339
+ Object.entries(config.combos ?? {}).flatMap(([id, combo]) => [
340
+ comboModelId(id),
341
+ comboPublicModelId(id, combo),
342
+ ]),
343
+ );
344
+ const targetComboSelectors = new Map<string, Set<string>>();
345
+ if (isVirtualComboNamespace) {
346
+ for (const target of targets) {
347
+ const combo = config.combos && Object.hasOwn(config.combos, target.id) ? config.combos[target.id] : undefined;
348
+ if (!combo) return jsonResponse({ error: "invalid model visibility target" }, 400);
349
+ targetComboSelectors.set(target.id, new Set([comboModelId(target.id), comboPublicModelId(target.id, combo)]));
350
+ }
351
+ }
352
+ const matchesTarget = (stored: string, target: { id: string; native: boolean }) => target.native
353
+ ? stored === target.id
354
+ : isVirtualComboNamespace
355
+ ? targetComboSelectors.get(target.id)!.has(stored)
356
+ : slugEquals(stored, provider, target.id);
357
+
358
+ let disabled = [...new Set(config.disabledModels ?? [])];
359
+ if (body.enabled) {
360
+ if (scope === "provider") {
361
+ if (providerConfig && !isVirtualComboNamespace) delete providerConfig.selectedModels;
362
+ if (isVirtualComboNamespace) {
363
+ disabled = disabled.filter(stored => !knownComboSelectors.has(stored));
364
+ } else {
365
+ const nativeIds = provider === "openai"
366
+ ? disabledNativeSlugs({ disabledModels: disabled })
367
+ : new Set<string>();
368
+ disabled = disabled.filter(stored => (
369
+ knownComboSelectors.has(stored)
370
+ || (!stored.startsWith(`${provider}/`) && !nativeIds.has(stored))
371
+ ));
372
+ }
373
+ } else {
374
+ if (!isVirtualComboNamespace && providerConfig?.selectedModels && providerConfig.selectedModels.length > 0) {
375
+ const additions = targets.filter(target => !target.native).map(target => target.id);
376
+ providerConfig.selectedModels = [...new Set([...providerConfig.selectedModels, ...additions])];
377
+ }
378
+ disabled = disabled.filter(stored => !targets.some(target => matchesTarget(stored, target)));
379
+ }
380
+ } else {
381
+ for (const target of targets) {
382
+ const canonical = target.native
383
+ ? target.id
384
+ : isVirtualComboNamespace
385
+ ? comboModelId(target.id)
386
+ : routedSlug(provider, target.id);
387
+ const alreadyDisabled = disabled.some(stored => matchesTarget(stored, target));
388
+ if (!alreadyDisabled) disabled.push(canonical);
389
+ }
390
+ }
391
+
392
+ config.disabledModels = disabled;
393
+ persistConfig(config);
394
+ await refreshCodexCatalogBestEffort();
395
+ return jsonResponse({ ok: true, scope, provider, enabled: body.enabled, disabled });
396
+ }
397
+
398
+ if (url.pathname === "/api/custom-models" && req.method === "GET") {
399
+ return jsonResponse(config.customModels ?? []);
400
+ }
401
+
402
+ if (url.pathname === "/api/custom-models" && req.method === "POST") {
403
+ let body: { provider?: unknown; modelId?: unknown; displayName?: unknown; contextWindow?: unknown; inputModalities?: unknown };
404
+ try { body = await readManagementJsonBody(req); } catch (error) { rethrowManagementBodyTooLarge(error); return jsonResponse({ error: "invalid JSON body" }, 400); }
405
+ const provider = typeof body.provider === "string" ? body.provider.trim() : "";
406
+ const modelId = typeof body.modelId === "string" ? body.modelId.trim() : "";
407
+ if (!provider || !modelId) return jsonResponse({ error: "provider and modelId are required" }, 400);
408
+ if (modelId.includes("/")) return jsonResponse({ error: "modelId must not contain /" }, 400);
409
+ if (!isValidProviderName(provider)) return jsonResponse({ error: "invalid provider name" }, 400);
410
+ if (!hasOwnProvider(config.providers, provider)) return jsonResponse({ error: "provider not configured" }, 404);
411
+ const displayName = typeof body.displayName === "string" && body.displayName.trim() ? body.displayName.trim() : undefined;
412
+ if (displayName?.includes("/")) return jsonResponse({ error: "displayName must not contain /" }, 400);
413
+ const contextWindow = typeof body.contextWindow === "number" && body.contextWindow > 0 ? Math.floor(body.contextWindow) : undefined;
414
+ const modalities = readInputModalities(body.inputModalities);
415
+ if (modalities.error) return jsonResponse({ error: modalities.error }, 400);
416
+ const inputModalities = modalities.values;
417
+ const existing = config.customModels ?? [];
418
+ const newSlug = routedSlug(provider, modelId);
419
+ if (existing.some(cm => routedSlug(cm.provider, cm.modelId) === newSlug)) {
420
+ return jsonResponse({ error: "duplicate model" }, 409);
421
+ }
422
+ const entry: OcxCustomModel = {
423
+ id: randomUUID(),
424
+ provider,
425
+ modelId,
426
+ ...(displayName ? { displayName } : {}),
427
+ ...(contextWindow ? { contextWindow } : {}),
428
+ ...(inputModalities && inputModalities.length > 0 ? { inputModalities } : {}),
429
+ addedAt: new Date().toISOString(),
430
+ };
431
+ config.customModels = [...existing, entry];
432
+ persistConfig(config);
433
+ await refreshCodexCatalogBestEffort();
434
+ return jsonResponse(entry, 201);
435
+ }
436
+
437
+ const customPutMatch = url.pathname.match(/^\/api\/custom-models\/([^/]+)$/);
438
+ if (customPutMatch && req.method === "PUT") {
439
+ let id: string;
440
+ try { id = decodeURIComponent(customPutMatch[1]); } catch { return jsonResponse({ error: "invalid id encoding" }, 400); }
441
+ let body: { displayName?: unknown; contextWindow?: unknown; inputModalities?: unknown; modelId?: unknown };
442
+ try { body = await readManagementJsonBody(req); } catch (error) { rethrowManagementBodyTooLarge(error); return jsonResponse({ error: "invalid JSON body" }, 400); }
443
+ const list = config.customModels ?? [];
444
+ const idx = list.findIndex(cm => cm.id === id);
445
+ if (idx === -1) return jsonResponse({ error: "not found" }, 404);
446
+ const cm = { ...list[idx] };
447
+ if (typeof body.modelId === "string" && body.modelId.trim()) {
448
+ if (body.modelId.includes("/")) return jsonResponse({ error: "modelId must not contain /" }, 400);
449
+ cm.modelId = body.modelId.trim();
450
+ }
451
+ if (body.displayName !== undefined) {
452
+ const dn = typeof body.displayName === "string" ? body.displayName.trim() : "";
453
+ if (dn.includes("/")) return jsonResponse({ error: "displayName must not contain /" }, 400);
454
+ cm.displayName = dn || undefined;
455
+ }
456
+ if (body.contextWindow !== undefined) {
457
+ cm.contextWindow = typeof body.contextWindow === "number" && body.contextWindow > 0 ? Math.floor(body.contextWindow) : undefined;
458
+ }
459
+ if (body.inputModalities !== undefined) {
460
+ const edited = readInputModalities(body.inputModalities);
461
+ if (edited.error) return jsonResponse({ error: edited.error }, 400);
462
+ cm.inputModalities = edited.values && edited.values.length > 0 ? edited.values : undefined;
463
+ }
464
+ const updatedSlug = routedSlug(cm.provider, cm.modelId);
465
+ if (list.some((other, i) => i !== idx && routedSlug(other.provider, other.modelId) === updatedSlug)) {
466
+ return jsonResponse({ error: "duplicate model" }, 409);
467
+ }
468
+ list[idx] = cm;
469
+ config.customModels = list;
470
+ persistConfig(config);
471
+ await refreshCodexCatalogBestEffort();
472
+ return jsonResponse(cm);
473
+ }
474
+
475
+ const customDelMatch = url.pathname.match(/^\/api\/custom-models\/([^/]+)$/);
476
+ if (customDelMatch && req.method === "DELETE") {
477
+ let id: string;
478
+ try { id = decodeURIComponent(customDelMatch[1]); } catch { return jsonResponse({ error: "invalid id encoding" }, 400); }
479
+ const list = config.customModels ?? [];
480
+ const idx = list.findIndex(cm => cm.id === id);
481
+ if (idx === -1) return jsonResponse({ error: "not found" }, 404);
482
+ list.splice(idx, 1);
483
+ config.customModels = list.length > 0 ? list : undefined;
484
+ persistConfig(config);
485
+ await refreshCodexCatalogBestEffort();
486
+ return jsonResponse({ ok: true });
487
+ }
488
+
489
+ // Per-provider catalog allowlist (issue #52): when a provider has a non-empty selectedModels list,
490
+ // only those ids ship to Codex's catalog / /v1/models. GET returns the CURRENT selection plus the
491
+ // FULL available set per provider (unfiltered — the picker needs everything to choose from).
492
+ if (url.pathname === "/api/selected-models" && req.method === "GET") {
493
+ const models = await fetchAllModels(config);
494
+ const available: Record<string, string[]> = {};
495
+ for (const m of models) (available[m.provider] ??= []).push(m.id);
496
+ const selected: Record<string, string[]> = {};
497
+ // Live-catalog provenance. The GUI cannot infer this by subtracting known custom ids: an id
498
+ // that is both custom and discovered would make a real live catalog look custom-only.
499
+ const liveModelCounts: Record<string, number> = {};
500
+ for (const [name, prov] of Object.entries(config.providers)) {
501
+ if (Array.isArray(prov.selectedModels) && prov.selectedModels.length > 0) selected[name] = [...prov.selectedModels];
502
+ const liveCount = getProviderLiveModelCount(name);
503
+ if (liveCount !== undefined) liveModelCounts[name] = liveCount;
504
+ }
505
+ return jsonResponse({ selected, available, liveModelCounts });
506
+ }
507
+ if (url.pathname === "/api/selected-models" && req.method === "PUT") {
508
+ let body: { provider?: unknown; models?: unknown };
509
+ try { body = await readManagementJsonBody(req); } catch (error) { rethrowManagementBodyTooLarge(error); return jsonResponse({ error: "invalid JSON body" }, 400); }
510
+ const provider = typeof body.provider === "string" ? body.provider : "";
511
+ if (!provider || !hasOwnProvider(config.providers, provider)) {
512
+ return jsonResponse({ error: "unknown provider" }, provider ? 404 : 400);
513
+ }
514
+ const models = Array.isArray(body.models)
515
+ ? [...new Set(body.models.filter((m): m is string => typeof m === "string"))]
516
+ : [];
517
+ // Empty list clears the allowlist (provider reverts to exposing all models).
518
+ if (models.length > 0) config.providers[provider].selectedModels = models;
519
+ else delete config.providers[provider].selectedModels;
520
+ persistConfig(config);
521
+ await refreshCodexCatalogBestEffort();
522
+ return jsonResponse({ ok: true, provider, selected: models });
523
+ }
524
+ return null;
525
+ }