@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,530 @@
1
+ import { timingSafeEqual } from "node:crypto";
2
+ import { formatErrorResponse } from "../bridge";
3
+ import {
4
+ apiKeyTransportConfigError,
5
+ booleanRecordConfigError,
6
+ modelAdapterRecordConfigError,
7
+ codexAutoStartEnabled,
8
+ positiveIntegerConfigError,
9
+ positiveIntegerRecordConfigError,
10
+ providerBaseUrlConfigError,
11
+ providerHeadersConfigError,
12
+ reasoningSummaryDeliveryRecordConfigError,
13
+ } from "../config";
14
+ import { providerDestinationConfigError } from "../lib/destination-policy";
15
+ import { getProviderRegistryEntry, providerCodexAccountMode, providerMatchesRegistryTransport, registryEntryForProviderDestination } from "../providers/registry";
16
+ import { providerConfigSeed } from "../providers/derive";
17
+ import type { OcxConfig, OcxProviderConfig } from "../types";
18
+ import { openRouterRoutingConfigError } from "../providers/openrouter-routing";
19
+
20
+ let _corsOrigin = "http://localhost:10100";
21
+ export function setCorsOrigin(port: number): void { _corsOrigin = `http://localhost:${port}`; }
22
+ /** The proxy's own listening port. No admission check uses it: both loopback predicates key on hostname alone. */
23
+ export function configuredPort(): string {
24
+ try { return new URL(_corsOrigin).port; } catch { return "10100"; }
25
+ }
26
+
27
+ export function parseHttpHost(value: string | null): { hostname: string; port: string } | null {
28
+ if (!value) return null;
29
+ try {
30
+ const parsed = new URL(`http://${value}`);
31
+ return { hostname: parsed.hostname.toLowerCase(), port: parsed.port };
32
+ } catch {
33
+ return null;
34
+ }
35
+ }
36
+
37
+ export function isLoopbackRequestHost(value: string | null): boolean {
38
+ const parsed = parseHttpHost(value);
39
+ if (!parsed) return true;
40
+ // Loopback is a trust boundary by hostname, not by port. `ssh -L 20100:localhost:10100`
41
+ // legitimately arrives as `Host: localhost:20100`, and refusing it took the whole /v1/*
42
+ // data plane down with it, not just CORS. The sibling isLoopbackOriginValue() dropped its
43
+ // own port check for the same reason in e4e06125b ("same-trust-boundary"). Port equality
44
+ // was never the rebinding defense: a rebinding browser connects to the real port and sends
45
+ // it verbatim, so the hostname check below is what rejected it then and now.
46
+ //
47
+ // Scope of that guarantee: it holds for Hosts `parseHttpHost` can parse. An unparseable
48
+ // Host still returns true above — pre-existing behavior, not browser-reachable (a browser
49
+ // composes Host from its own connection), and pinned by a characterization test in
50
+ // tests/server-loopback-host-gate.test.ts. Tightening it is separate work.
51
+ return isLoopbackHostname(parsed.hostname);
52
+ }
53
+
54
+ export function isLoopbackOriginValue(value: string): boolean {
55
+ try {
56
+ const parsed = new URL(value);
57
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return false;
58
+ return isLoopbackHostname(parsed.hostname);
59
+ } catch {
60
+ return false;
61
+ }
62
+ }
63
+
64
+ export function isSameOriginAsRequest(req: Request, origin: string): boolean {
65
+ try {
66
+ return origin === new URL(req.url).origin;
67
+ } catch {
68
+ return false;
69
+ }
70
+ }
71
+
72
+ export function isAllowedRequestOrigin(req: Request, config: OcxConfig): boolean {
73
+ const origin = req.headers.get("Origin");
74
+ if (!isApiAuthRequired(config)) {
75
+ if (!isLoopbackRequestHost(req.headers.get("Host"))) return false;
76
+ return !origin || isLoopbackOriginValue(origin) || isExtraAllowedOrigin(origin, config);
77
+ }
78
+ return !origin || isLoopbackOriginValue(origin) || isSameOriginAsRequest(req, origin) || isExtraAllowedOrigin(origin, config);
79
+ }
80
+
81
+ function isExtraAllowedOrigin(origin: string, cfg: OcxConfig): boolean {
82
+ if (!cfg.corsAllowOrigins?.length) return false;
83
+ return cfg.corsAllowOrigins.some(allowed => {
84
+ try {
85
+ return new URL(allowed).origin === new URL(origin).origin;
86
+ } catch {
87
+ return allowed === origin;
88
+ }
89
+ });
90
+ }
91
+
92
+ export function managementRequestOrigin(req: Request, config: OcxConfig): string | null {
93
+ const host = req.headers.get("Host");
94
+ const parsedHost = parseHttpHost(host);
95
+ if (!host || !parsedHost) return null;
96
+ if (!isApiAuthRequired(config) && !isLoopbackHostname(parsedHost.hostname)) return null;
97
+ try {
98
+ const protocol = new URL(req.url).protocol;
99
+ if (protocol !== "http:" && protocol !== "https:") return null;
100
+ return new URL(`${protocol}//${host}`).origin;
101
+ } catch {
102
+ return null;
103
+ }
104
+ }
105
+
106
+ export function isAllowedManagementOrigin(req: Request, config: OcxConfig): boolean {
107
+ const requestOrigin = managementRequestOrigin(req, config);
108
+ if (!requestOrigin) return false;
109
+ const origin = req.headers.get("Origin");
110
+ // Exact match against the process-derived origin, or an operator-listed corsAllowOrigins
111
+ // entry (covers TLS-terminator https://… when the process observes http://…).
112
+ return !origin || origin === requestOrigin || isExtraAllowedOrigin(origin, config);
113
+ }
114
+
115
+ export function browserSecurityHeaders(): Record<string, string> {
116
+ return {
117
+ "X-Frame-Options": "DENY",
118
+ "Content-Security-Policy": "frame-ancestors 'none'",
119
+ };
120
+ }
121
+
122
+ export function corsHeaders(req?: Request, config?: OcxConfig): Record<string, string> {
123
+ const origin = req?.headers.get("Origin");
124
+ const allowOrigin = origin && req && config && isAllowedRequestOrigin(req, config) ? origin : _corsOrigin;
125
+ return {
126
+ "Access-Control-Allow-Origin": allowOrigin,
127
+ "Access-Control-Allow-Methods": "GET, POST, PUT, PATCH, DELETE, OPTIONS",
128
+ // ChatGPT-Account-Id is required for browser/Electron ChatGPT & Codex App voice preflights
129
+ // (direct forward auth matches the bearer to this account id). The OpenAI-Alpha .. X-OAI-Attestation
130
+ // block covers GPT-Live voice protocol headers relayed by the /v1/live call-create path.
131
+ "Access-Control-Allow-Headers": "Content-Type, Authorization, X-OpenCodex-API-Key, X-Api-Key, Anthropic-Version, Anthropic-Beta, ChatGPT-Account-Id, OpenAI-Alpha, X-Session-Id, Session-Id, Thread-Id, Originator, X-OAI-Attestation",
132
+ "Vary": "Origin",
133
+ ...browserSecurityHeaders(),
134
+ };
135
+ }
136
+
137
+ export function managementCorsHeaders(req?: Request, config?: OcxConfig): Record<string, string> {
138
+ const headers = corsHeaders();
139
+ const origin = req?.headers.get("Origin");
140
+ if (origin && req && config && isAllowedManagementOrigin(req, config)) {
141
+ headers["Access-Control-Allow-Origin"] = origin;
142
+ }
143
+ return headers;
144
+ }
145
+
146
+ export function withCors(response: Response, req: Request, config: OcxConfig): Response {
147
+ const headers = new Headers(response.headers);
148
+ for (const [name, value] of Object.entries(corsHeaders(req, config))) {
149
+ headers.set(name, value);
150
+ }
151
+ return new Response(response.body, {
152
+ status: response.status,
153
+ statusText: response.statusText,
154
+ headers,
155
+ });
156
+ }
157
+
158
+ export function withManagementCors(response: Response, req: Request, config: OcxConfig): Response {
159
+ const headers = new Headers(response.headers);
160
+ for (const [name, value] of Object.entries(managementCorsHeaders(req, config))) {
161
+ headers.set(name, value);
162
+ }
163
+ return new Response(response.body, {
164
+ status: response.status,
165
+ statusText: response.statusText,
166
+ headers,
167
+ });
168
+ }
169
+
170
+ export function jsonResponse(data: unknown, status = 200, req?: Request, config?: OcxConfig): Response {
171
+ return new Response(JSON.stringify(data), {
172
+ status,
173
+ headers: { "Content-Type": "application/json", ...corsHeaders(req, config) },
174
+ });
175
+ }
176
+
177
+ export function configuredApiAuthToken(_config: OcxConfig): string | undefined {
178
+ const token = process.env.OPENCODEX_API_AUTH_TOKEN?.trim();
179
+ return token || undefined;
180
+ }
181
+
182
+ export function configuredAdminAuthToken(): string | undefined {
183
+ const token = process.env.OPENCODEX_ADMIN_AUTH_TOKEN?.trim();
184
+ return token || undefined;
185
+ }
186
+
187
+ export function isLoopbackHostname(hostname: string | undefined): boolean {
188
+ // A fully-qualified "localhost." is the same host as "localhost": curl and some clients
189
+ // send the trailing dot verbatim, and refusing it 403s a legitimate loopback caller.
190
+ const normalized = (hostname ?? "127.0.0.1").trim().toLowerCase().replace(/\.$/, "");
191
+ return normalized === "" || normalized === "localhost" || normalized === "127.0.0.1" || normalized === "::1" || normalized === "[::1]";
192
+ }
193
+
194
+ export function isApiAuthRequired(config: OcxConfig): boolean {
195
+ return !isLoopbackHostname(config.hostname);
196
+ }
197
+
198
+ export function assertServerAuthConfig(config: OcxConfig): void {
199
+ const hasConfiguredDataCredential = !!configuredApiAuthToken(config)
200
+ || (config.apiKeys ?? []).some(entry => !!entry.key.trim());
201
+ if (isApiAuthRequired(config) && !hasConfiguredDataCredential) {
202
+ throw new Error(
203
+ "A data-plane credential (OPENCODEX_API_AUTH_TOKEN or config.apiKeys) is required when binding opencodex to a non-loopback hostname",
204
+ );
205
+ }
206
+ }
207
+
208
+ function secretEquals(actual: string, expected: string | undefined): boolean {
209
+ if (!expected) return false;
210
+ const enc = new TextEncoder();
211
+ const actualBytes = enc.encode(actual);
212
+ const expectedBytes = enc.encode(expected);
213
+ return expectedBytes.length === actualBytes.length && timingSafeEqual(actualBytes, expectedBytes);
214
+ }
215
+
216
+ /**
217
+ * Which admission a data-plane request used.
218
+ *
219
+ * `configured` carries the matched key's id so a request can be attributed to
220
+ * the key that opened it. The other two exist so an unattributed request is a
221
+ * stated fact rather than a missing field: neither has a configured entry to
222
+ * point at, and a sentinel string in the id would collide with a hand-edited
223
+ * entry that happens to be named `loopback`.
224
+ */
225
+ export type DataPlaneAdmission =
226
+ | { kind: "configured"; keyId: string }
227
+ | { kind: "environment" }
228
+ | { kind: "loopback" };
229
+
230
+ /**
231
+ * Which admission secret `token` is, or null when it is none of them.
232
+ *
233
+ * Identical comparisons in an identical order to the boolean form this replaces —
234
+ * `secretEquals` still length-guards before `timingSafeEqual`. The only
235
+ * difference is that the matched entry's id survives the loop instead of being
236
+ * discarded, which is what makes per-key attribution possible without touching
237
+ * the admission decision itself.
238
+ */
239
+ export function resolveDataPlaneAdmissionSecret(token: string, config: OcxConfig): DataPlaneAdmission | null {
240
+ const actual = token.trim();
241
+ if (!actual) return null;
242
+ if (secretEquals(actual, configuredApiAuthToken(config))) return { kind: "environment" };
243
+ for (const k of config.apiKeys ?? []) {
244
+ if (secretEquals(actual, k.key)) return { kind: "configured", keyId: k.id };
245
+ }
246
+ return null;
247
+ }
248
+
249
+ /** Whether `token` is a data-plane admission secret. */
250
+ export function isDataPlaneAdmissionSecret(token: string, config: OcxConfig): boolean {
251
+ return resolveDataPlaneAdmissionSecret(token, config) !== null;
252
+ }
253
+
254
+ /**
255
+ * Split an admission into the fields a log row records.
256
+ *
257
+ * `apiKeyId` is set only for a configured key. The other two kinds have no
258
+ * configured entry to name, and folding them into the id as sentinel strings
259
+ * would collide with a hand-edited entry that happens to be called `loopback` —
260
+ * ids are only validated as non-empty strings.
261
+ */
262
+ export function admissionFields(admission: DataPlaneAdmission): {
263
+ admissionKind: DataPlaneAdmission["kind"];
264
+ apiKeyId?: string;
265
+ } {
266
+ return admission.kind === "configured"
267
+ ? { admissionKind: "configured", apiKeyId: admission.keyId }
268
+ : { admissionKind: admission.kind };
269
+ }
270
+
271
+ export type ApiAuthDisposition = "required" | "accepted" | "rejected";
272
+
273
+ export interface ApiAuthMatrixRow {
274
+ endpoint: string;
275
+ bearer: ApiAuthDisposition;
276
+ dedicated: ApiAuthDisposition;
277
+ xApiKey: ApiAuthDisposition;
278
+ }
279
+
280
+ /**
281
+ * Which headers each data-plane endpoint actually accepts, shipped to the GUI so
282
+ * it stops describing the rule from memory. The dashboard has been telling users
283
+ * that Chat Completions takes `Authorization: Bearer`, which this file has never
284
+ * allowed — that route uses the dedicated-header-only wrapper because
285
+ * `Authorization` there may belong to Codex Direct passthrough.
286
+ *
287
+ * It lives next to the wrappers it describes, and a test drives real requests
288
+ * against every cell rather than reading the table back to itself.
289
+ */
290
+ export const AUTH_MATRIX: readonly ApiAuthMatrixRow[] = [
291
+ { endpoint: "/v1/responses", bearer: "rejected", dedicated: "required", xApiKey: "rejected" },
292
+ { endpoint: "/v1/chat/completions", bearer: "rejected", dedicated: "required", xApiKey: "rejected" },
293
+ { endpoint: "/v1/messages", bearer: "accepted", dedicated: "accepted", xApiKey: "accepted" },
294
+ { endpoint: "/v1/models", bearer: "accepted", dedicated: "accepted", xApiKey: "accepted" },
295
+ ];
296
+
297
+ /** Whether `token` is the environment-provided management secret. */
298
+ export function isManagementAdmissionSecret(token: string): boolean {
299
+ const actual = token.trim();
300
+ return !!actual && secretEquals(actual, configuredAdminAuthToken());
301
+ }
302
+
303
+ /** Whether `token` is one of the proxy's own admission secrets and must never reach an upstream. */
304
+ export function isProxyAdmissionSecret(token: string, config: OcxConfig): boolean {
305
+ const actual = token.trim();
306
+ if (!actual) return false;
307
+ if (/^ocx_(?:data|admin|session)_/.test(actual) || /^ocx_[0-9a-f]{40}$/.test(actual)) return true;
308
+ return isDataPlaneAdmissionSecret(actual, config) || isManagementAdmissionSecret(actual);
309
+ }
310
+
311
+ export class ForwardAdmissionCredentialError extends Error {
312
+ constructor() {
313
+ super("OpenCodex admission credentials cannot be forwarded upstream");
314
+ this.name = "ForwardAdmissionCredentialError";
315
+ }
316
+ }
317
+
318
+ export function validateForwardAdmissionCredential(headers: Headers, config: OcxConfig): void {
319
+ const bearer = headers.get("authorization")?.replace(/^Bearer\s+/i, "").trim();
320
+ if (bearer && isProxyAdmissionSecret(bearer, config)) throw new ForwardAdmissionCredentialError();
321
+ }
322
+
323
+ /**
324
+ * Resolving form of `hasValidApiAuth`: identical header precedence, identical
325
+ * decision, but it names the admission instead of collapsing it to a boolean.
326
+ */
327
+ export function resolveApiAuth(req: Request, config: OcxConfig): DataPlaneAdmission | null {
328
+ // A loopback bind never reads a token at all, so there is no key to name.
329
+ if (!isApiAuthRequired(config)) return { kind: "loopback" };
330
+ const actual = req.headers.get("x-opencodex-api-key")?.trim()
331
+ || req.headers.get("authorization")?.replace(/^Bearer\s+/i, "").trim()
332
+ // Anthropic-SDK clients (Claude Code with ANTHROPIC_API_KEY) authenticate via x-api-key.
333
+ || req.headers.get("x-api-key")?.trim();
334
+ if (!actual) return null;
335
+ return resolveDataPlaneAdmissionSecret(actual, config);
336
+ }
337
+
338
+ export function hasValidApiAuth(req: Request, config: OcxConfig): boolean {
339
+ return resolveApiAuth(req, config) !== null;
340
+ }
341
+
342
+ export function requireApiAuth(req: Request, config: OcxConfig, _kind: "data-plane"): Response | null {
343
+ if (hasValidApiAuth(req, config)) return null;
344
+ return formatErrorResponse(401, "authentication_error", "opencodex API key required");
345
+ }
346
+
347
+ /**
348
+ * Admission for OpenAI Responses transports whose Authorization header belongs to
349
+ * Codex Direct. Remote binds must use the dedicated proxy header so the two bearer
350
+ * domains can never be confused.
351
+ */
352
+ export function resolveResponsesApiAuth(req: Request, config: OcxConfig): DataPlaneAdmission | null {
353
+ if (!isApiAuthRequired(config)) return { kind: "loopback" };
354
+ // Dedicated header ONLY. `Authorization` on these transports may belong to
355
+ // Codex Direct passthrough, and the two bearer domains must stay unconfusable.
356
+ const actual = req.headers.get("x-opencodex-api-key")?.trim();
357
+ if (!actual) return null;
358
+ return resolveDataPlaneAdmissionSecret(actual, config);
359
+ }
360
+
361
+ export function requireResponsesApiAuth(req: Request, config: OcxConfig): Response | null {
362
+ if (resolveResponsesApiAuth(req, config)) return null;
363
+ return formatErrorResponse(401, "authentication_error", "opencodex API key required");
364
+ }
365
+
366
+ const FORBIDDEN_PROVIDER_RUNTIME_FIELDS = [
367
+ "virtualModels", "codexAuthContext", "selectedForwardHeaders",
368
+ "sidecarOutcomeRecorder", "_codexAccountOverride", "_codexAccountRequired",
369
+ ] as const;
370
+
371
+ function sameCanonicalProviderSeed(actual: Record<string, unknown>, expected: OcxProviderConfig): boolean {
372
+ const actualKeys = Object.keys(actual).sort();
373
+ const expectedKeys = Object.keys(expected).sort();
374
+ if (actualKeys.length !== expectedKeys.length || actualKeys.some((key, i) => key !== expectedKeys[i])) return false;
375
+ return actualKeys.every(key => JSON.stringify(actual[key]) === JSON.stringify((expected as unknown as Record<string, unknown>)[key]));
376
+ }
377
+
378
+ export function providerManagementConfigError(name: unknown, provider: unknown): string | null {
379
+ if (typeof name !== "string" || !provider || typeof provider !== "object" || Array.isArray(provider)) {
380
+ return "provider must be a plain object";
381
+ }
382
+ const raw = provider as Record<string, unknown>;
383
+ for (const field of FORBIDDEN_PROVIDER_RUNTIME_FIELDS) {
384
+ if (Object.hasOwn(raw, field)) return `provider ${name} must not include runtime field "${field}"`;
385
+ }
386
+ if (name === "chatgpt") return "provider chatgpt is reserved for internal credential compatibility";
387
+ if (name === "openai-multi") return "provider openai-multi is reserved for legacy config migration";
388
+ if (name === "openai") {
389
+ const entry = getProviderRegistryEntry(name);
390
+ const seed = entry ? providerConfigSeed(entry) : undefined;
391
+ if (!Object.hasOwn(raw, "codexAccountMode") || (raw.codexAccountMode !== "pool" && raw.codexAccountMode !== "direct")) {
392
+ return "provider openai codexAccountMode must be pool or direct";
393
+ }
394
+ if (seed) seed.codexAccountMode = raw.codexAccountMode;
395
+ const canonical = seed && sameCanonicalProviderSeed(raw, seed);
396
+ if (!canonical) {
397
+ return `provider ${name} must equal the canonical built-in provider seed`;
398
+ }
399
+ } else if (Object.hasOwn(raw, "codexAccountMode")) {
400
+ return `provider ${name} must not include codexAccountMode`;
401
+ }
402
+ const typed = provider as unknown as OcxProviderConfig;
403
+ const baseUrlError = providerBaseUrlConfigError(typed.baseUrl);
404
+ if (baseUrlError) return `provider ${name} ${baseUrlError}`;
405
+ const destinationError = providerDestinationConfigError(name, typed);
406
+ if (destinationError) return `provider ${name} ${destinationError}`;
407
+ const headersError = providerHeadersConfigError(typed.headers);
408
+ if (headersError) return `provider ${name} ${headersError}`;
409
+ const apiKeyTransportError = apiKeyTransportConfigError(typed);
410
+ if (apiKeyTransportError) return `provider ${name} ${apiKeyTransportError}`;
411
+ const maxInputError = positiveIntegerRecordConfigError(raw.modelMaxInputTokens, "modelMaxInputTokens");
412
+ if (maxInputError) return `provider ${name} ${maxInputError}`;
413
+ const reasoningSummariesError = booleanRecordConfigError(raw.modelSupportsReasoningSummaries, "modelSupportsReasoningSummaries");
414
+ if (reasoningSummariesError) return `provider ${name} ${reasoningSummariesError}`;
415
+ const reasoningSummaryDeliveryError = reasoningSummaryDeliveryRecordConfigError(
416
+ raw.modelReasoningSummaryDelivery,
417
+ raw.modelSupportsReasoningSummaries,
418
+ );
419
+ if (reasoningSummaryDeliveryError) return `provider ${name} ${reasoningSummaryDeliveryError}`;
420
+ const modelAdaptersError = modelAdapterRecordConfigError(raw.modelAdapters, "modelAdapters", name, typed);
421
+ if (modelAdaptersError) return `provider ${name} ${modelAdaptersError}`;
422
+ const defaultMaxOutputError = positiveIntegerConfigError(raw.defaultMaxOutputTokens, "defaultMaxOutputTokens");
423
+ if (defaultMaxOutputError) return `provider ${name} ${defaultMaxOutputError}`;
424
+ const maxOutputError = positiveIntegerRecordConfigError(raw.modelMaxOutputTokens, "modelMaxOutputTokens");
425
+ if (maxOutputError) return `provider ${name} ${maxOutputError}`;
426
+ const openRouterError = openRouterRoutingConfigError(typed);
427
+ if (openRouterError) return `provider ${name} ${openRouterError}`;
428
+ if (typed.authMode === "local") {
429
+ // "local" bypasses key-requirement enforcement (api-keys/key-failover treat non-oauth/
430
+ // forward as key auth; openai-chat skips credential checks for local). Only providers
431
+ // whose registry entry is genuinely local (Ollama/vLLM/LM Studio) may claim it.
432
+ const entry = getProviderRegistryEntry(name);
433
+ if (entry && entry.authKind !== "local") {
434
+ return `provider ${name} cannot use authMode "local" — its registry entry requires ${entry.authKind} auth`;
435
+ }
436
+ }
437
+ if (typed.authMode === "forward") {
438
+ const normalizedName = name.trim().toLowerCase();
439
+ const base = typed.baseUrl.replace(/\/+$/, "");
440
+ const isBuiltInChatGptForward = normalizedName === "openai"
441
+ && typed.adapter === "openai-responses"
442
+ && base === "https://chatgpt.com/backend-api/codex";
443
+ if (isBuiltInChatGptForward) return null;
444
+ return `provider ${name} uses reserved authMode "forward"; configure ChatGPT passthrough via the built-in provider`;
445
+ }
446
+ return null;
447
+ }
448
+
449
+ export function publicProviderBaseUrl(baseUrl: string): string {
450
+ try {
451
+ const parsed = new URL(baseUrl.trim());
452
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return "(invalid URL)";
453
+ parsed.username = "";
454
+ parsed.password = "";
455
+ parsed.search = "";
456
+ parsed.hash = "";
457
+ return parsed.toString().replace(/\/$/, baseUrl.endsWith("/") ? "/" : "");
458
+ } catch {
459
+ return "(invalid URL)";
460
+ }
461
+ }
462
+
463
+ export function copyIfDefined<K extends keyof OcxProviderConfig>(
464
+ out: Record<string, unknown>,
465
+ provider: OcxProviderConfig,
466
+ key: K,
467
+ ): void {
468
+ const value = provider[key];
469
+ if (value !== undefined) out[key as string] = value as unknown;
470
+ }
471
+
472
+ export function safeConfigDTO(config: OcxConfig): unknown {
473
+ const providers: Record<string, Record<string, unknown>> = {};
474
+ for (const [name, provider] of Object.entries(config.providers)) {
475
+ const dto: Record<string, unknown> = {
476
+ adapter: provider.adapter,
477
+ baseUrl: publicProviderBaseUrl(provider.baseUrl),
478
+ hasApiKey: !!provider.apiKey,
479
+ hasHeaders: !!provider.headers && Object.keys(provider.headers).length > 0,
480
+ };
481
+ for (const key of [
482
+ "defaultModel",
483
+ "disabled",
484
+ "allowPrivateNetwork",
485
+ "authMode",
486
+ "apiKeyTransport",
487
+ "keyOptional",
488
+ "freeTier",
489
+ "liveModels",
490
+ "models",
491
+ "contextWindow",
492
+ "modelContextWindows",
493
+ "defaultMaxOutputTokens",
494
+ "modelMaxOutputTokens",
495
+ "openRouterRouting",
496
+ "modelOpenRouterRouting",
497
+ "reasoningEfforts",
498
+ "modelReasoningEfforts",
499
+ "noVisionModels",
500
+ "noReasoningModels",
501
+ "noTemperatureModels",
502
+ "noTopPModels",
503
+ "noPenaltyModels",
504
+ "autoToolChoiceOnlyModels",
505
+ "preserveReasoningContentModels",
506
+ "escapeBuiltinToolNames",
507
+ ] as const) {
508
+ copyIfDefined(dto, provider, key);
509
+ }
510
+ // Resolve the note by DESTINATION, not by name. A preset saved under a custom name is
511
+ // still pointed at the same vendor route, and a usage restriction the user needs to see
512
+ // must not disappear because the row was renamed. Prefer the same-name entry so an
513
+ // unrenamed provider keeps its exact registry note.
514
+ const registryNote = (providerMatchesRegistryTransport(name, provider)
515
+ ? getProviderRegistryEntry(name)
516
+ : registryEntryForProviderDestination(provider))?.note;
517
+ if (typeof registryNote === "string" && registryNote.trim()) dto.note = registryNote;
518
+ const codexAccountMode = providerCodexAccountMode(name, provider);
519
+ if (codexAccountMode) dto.codexAccountMode = codexAccountMode;
520
+ providers[name] = dto;
521
+ }
522
+ return {
523
+ port: config.port,
524
+ hostname: config.hostname ?? "127.0.0.1",
525
+ defaultProvider: config.defaultProvider,
526
+ codexAutoStart: codexAutoStartEnabled(config),
527
+ websockets: config.websockets,
528
+ providers,
529
+ };
530
+ }