@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,563 @@
1
+ import { randomBytes, randomUUID } from "node:crypto";
2
+ import { readFileSync } from "node:fs";
3
+ import type { CatalogModel } from "../../codex/catalog";
4
+ import { catalogModelSlug, invalidateCodexModelsCache, nativeModelRows, uniqueCatalogModelsForPublicList } from "../../codex/catalog";
5
+ import {
6
+ DEFAULT_SUBAGENT_MODELS,
7
+ codexAutoStartEnabled,
8
+ hasOwnProvider,
9
+ isValidProviderName,
10
+ multiAgentGuidanceEnabled,
11
+ providerBaseUrlConfigError,
12
+ providerHeadersConfigError,
13
+ readConfigDiagnostics,
14
+ reconcileLiveConfigFromDisk,
15
+ saveConfigPreservingClaudeCode,
16
+ } from "../../config";
17
+ import {
18
+ clearLoginState,
19
+ getLoginStatus,
20
+ isPublicOAuthProvider,
21
+ listOAuthProviders,
22
+ startLoginFlow,
23
+ submitManualLoginCode,
24
+ } from "../../oauth";
25
+ import { OAuthMutationBusyError, removeCredential } from "../../oauth/store";
26
+ import { providerDestinationResolvedError } from "../../lib/destination-policy";
27
+ import { reconcileLiveStateStores } from "../../lib/state-store-registrations";
28
+ import { enrichProviderFromCatalog, listKeyLoginProviders } from "../../oauth/key-providers";
29
+ import { deriveProviderPresets } from "../../providers/derive";
30
+ import { providerCodexAccountMode } from "../../providers/registry";
31
+ import { routedSlug, slugEquals } from "../../providers/slug-codec";
32
+ import { clearProviderQuotaCache, fetchProviderAccountQuotas, fetchProviderQuotaReports, supportsPerAccountQuota } from "../../providers/quota";
33
+ import { isCanonicalOpenAiForwardProvider } from "../../providers/openai-tiers";
34
+ import { clearThreadAccountMap } from "../../codex/routing";
35
+ import {
36
+ normalizeAccountPoolStickyLimit,
37
+ normalizeAccountPoolStrategy,
38
+ parseAccountPoolStickyLimit,
39
+ parseAccountPoolStrategy,
40
+ } from "../../codex/pool-rotation";
41
+ import { primeCodexPoolQuotas } from "../../codex/auth-api";
42
+ import { DEFAULT_PROVIDER_CONTEXT_CAP, globalContextCapValue, providerContextCap, providerContextCaps, setAllProviderContextCaps, setGlobalContextCapValue, setProviderContextCap } from "../../providers/context-cap";
43
+ import { resolveCodexHomeDir } from "../../codex/home";
44
+ import { readUsageEntries } from "../../usage/log";
45
+ import { getUsageDebugLogEntries } from "../../usage/debug";
46
+ import { parseRange, parseUsageSurface, summarizeUsage } from "../../usage/summary";
47
+ import { stripCodexRuntimeProviderFields } from "../../codex/auth-context";
48
+ import { getProviderRegistryEntry } from "../../providers/registry";
49
+ import { getDebugLogEntries } from "../../lib/debug-log-buffer";
50
+ import { getInjectionDebugLogEntries } from "../../lib/injection-debug-log";
51
+ import {
52
+ clearDebugSettings,
53
+ clearDebugSetting,
54
+ getDebugSettings,
55
+ setDebugSettings,
56
+ type DebugFlag,
57
+ } from "../../lib/debug-settings";
58
+ import type { OcxClaudeCodeConfig, OcxConfig, OcxCustomModel, OcxProviderConfig } from "../../types";
59
+ import { drainAndShutdown } from "../lifecycle";
60
+ import { filterRequestLogs, getRequestLogEntries, type RequestLogEntry } from "../request-log";
61
+ import { estimateComboCost, estimateRequestCost, normalizeCostTokens, tokensPerSecond } from "../../usage/cost";
62
+ import type { PersistedUsageAttempt } from "../../usage/log";
63
+ import { AUTH_MATRIX, isAllowedRequestOrigin, jsonResponse, providerManagementConfigError, publicProviderBaseUrl, safeConfigDTO } from "../auth-cors";
64
+ import { applySystemEnvToggle } from "../system-env";
65
+ import { buildApiAccessEndpoints } from "./api-access";
66
+
67
+ import { isPlainRecord, parseDebugLogQuery, tokPerSecondResult, unavailableCostReason, costResult, requestLogDto, stripRegistryOnlyStaticHeaders, fetchAllModels } from "./shared";
68
+ import type { MetricUnavailableReason, TokPerSecondResult, CostEstimateReason, CostResult, MetricSource } from "./shared";
69
+ import type { ManagementContext } from "./context";
70
+ import { readManagementJsonBody, readManagementJsonBodyOr, rethrowManagementBodyTooLarge } from "./body";
71
+ import { codexAccountNamespaceProviderCollisionError } from "../../codex/account-namespace-match";
72
+
73
+ /**
74
+ * Parses a bounded JSON object body, or null. Malformed JSON is swallowed; an
75
+ * oversized body still throws so the management dispatcher can return 413.
76
+ * a malformed body, which used to surface as a 500 from the key routes.
77
+ */
78
+ async function readJsonBody(req: Request): Promise<Record<string, unknown> | null> {
79
+ try {
80
+ const parsed: unknown = await readManagementJsonBody(req);
81
+ return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)
82
+ ? parsed as Record<string, unknown>
83
+ : null;
84
+ } catch (error) {
85
+ rethrowManagementBodyTooLarge(error);
86
+ return null;
87
+ }
88
+ }
89
+
90
+ /**
91
+ * The single place key-name rules live. The config read schema is deliberately
92
+ * permissive so an existing config can never become unloadable; this is the write
93
+ * boundary that keeps new junk out. A non-string name used to reach `.trim()` and
94
+ * throw.
95
+ */
96
+ function validateKeyName(
97
+ raw: unknown,
98
+ opts: { required: boolean },
99
+ ): { value: string } | { error: string } {
100
+ if (raw === undefined || raw === null) {
101
+ return opts.required ? { error: "name required" } : { value: "" };
102
+ }
103
+ if (typeof raw !== "string") return { error: "name must be a string" };
104
+ // Check the RAW string: trimming first would silently accept "deploy\n" by
105
+ // deleting the very character being rejected.
106
+ // eslint-disable-next-line no-control-regex
107
+ if (/[\u0000-\u001f\u007f]/.test(raw)) return { error: "invalid name" };
108
+ const value = raw.trim();
109
+ if (opts.required && !value) return { error: "name required" };
110
+ if (value.length > 64) return { error: "name too long" };
111
+ return { value };
112
+ }
113
+
114
+ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<Response | null> {
115
+ const { req, url, config, deps, refreshCodexCatalogBestEffort, syncClaudeAgentDefsBestEffort } = ctx;
116
+
117
+ // Which providers support real OAuth login (drives the GUI's "Log in with …" buttons).
118
+ if (url.pathname === "/api/oauth/providers" && req.method === "GET") {
119
+ return jsonResponse({ providers: listOAuthProviders() });
120
+ }
121
+
122
+ // API-key "login" providers (open dashboard → paste key). Drives the GUI's key-provider picker.
123
+ if (url.pathname === "/api/key-providers" && req.method === "GET") {
124
+ return jsonResponse({ providers: listKeyLoginProviders() });
125
+ }
126
+
127
+ // OAuth login (xai now; anthropic/kimi in cycle 2). Starts the flow and returns the auth URL;
128
+ // the provider's loopback callback server (inside this process) captures the redirect in the
129
+ // background, then the credential is persisted. The GUI opens the URL and polls /api/oauth/status.
130
+ if (url.pathname === "/api/oauth/login" && req.method === "POST") {
131
+ const body = await readManagementJsonBodyOr(req, {}) as { provider?: string; addAccount?: boolean; accountId?: string; reauth?: boolean };
132
+ const provider = (body.provider ?? "").trim().toLowerCase();
133
+ if (!isPublicOAuthProvider(provider)) return jsonResponse({ error: "unknown oauth provider" }, 400);
134
+ const namespaceCollision = codexAccountNamespaceProviderCollisionError(config.codexAccountNamespaces, provider);
135
+ if (namespaceCollision) return jsonResponse({ error: namespaceCollision }, 409);
136
+ const accountId = body.accountId?.trim();
137
+ const reauth = body.reauth === true || Boolean(accountId);
138
+ try {
139
+ if (accountId) {
140
+ const { getAccountSet } = await import("../../oauth/store");
141
+ const set = getAccountSet(provider);
142
+ if (!set?.accounts.some(a => a.id === accountId)) {
143
+ return jsonResponse({ error: "Unknown account for reauth" }, 404);
144
+ }
145
+ }
146
+ // Use persisted state, not the live object, as the merge base: another management
147
+ // request may already have mutated live config and yielded before its save.
148
+ const persistedBaseline = readConfigDiagnostics().config;
149
+ // addAccount / reauth forces a fresh browser identity (skips local-CLI token import).
150
+ const { url: authUrl, instructions, deviceCode } = await startLoginFlow(provider, {
151
+ forceLogin: body.addAccount === true || reauth,
152
+ ...(accountId ? { reauthAccountId: accountId } : {}),
153
+ }, {
154
+ // startLoginFlow returns the authorization URL before background persistence completes.
155
+ // Three-way reconcile settled disk changes so a failed login cannot leave a provider
156
+ // live-only and an in-flight management mutation cannot be erased before it saves.
157
+ onSettled: () => {
158
+ reconcileLiveConfigFromDisk(config, persistedBaseline);
159
+ reconcileLiveStateStores();
160
+ },
161
+ });
162
+ if (authUrl && !deviceCode) {
163
+ // Open the browser server-side (the proxy runs on the user's machine) — the GUI's
164
+ // window.open is popup-blocked because it runs after an await, not a direct click.
165
+ const { openUrl } = await import("../../lib/open-url");
166
+ openUrl(authUrl);
167
+ }
168
+ return jsonResponse({ url: authUrl, instructions, deviceCode });
169
+ } catch (err) {
170
+ if (err instanceof OAuthMutationBusyError) throw err;
171
+ return jsonResponse({ error: err instanceof Error ? err.message : String(err) }, 409);
172
+ }
173
+ }
174
+
175
+ // Cancel an in-progress browser/device OAuth login (GUI "Cancel" / modal close). Guarded by
176
+ // the same public predicate as /api/oauth/login — only publicly startable flows are cancellable.
177
+ if (url.pathname === "/api/oauth/login/cancel" && req.method === "POST") {
178
+ const body = await readManagementJsonBodyOr(req, {}) as { provider?: string };
179
+ const provider = (body.provider ?? "").trim().toLowerCase();
180
+ if (!isPublicOAuthProvider(provider)) return jsonResponse({ error: "unknown oauth provider" }, 400);
181
+ const { cancelLoginFlow } = await import("../../oauth");
182
+ const cancelled = cancelLoginFlow(provider);
183
+ return jsonResponse({ ok: true, cancelled });
184
+ }
185
+
186
+ // Manual fallback for browser OAuth: paste the final redirect URL (or authorization code)
187
+ // when the browser cannot reach the loopback callback (remote/SSH/blocked localhost).
188
+ if (url.pathname === "/api/oauth/login/code" && req.method === "POST") {
189
+ const body = await readManagementJsonBodyOr(req, {}) as { provider?: string; input?: string; code?: string };
190
+ const provider = (body.provider ?? "").trim().toLowerCase();
191
+ if (!isPublicOAuthProvider(provider)) return jsonResponse({ error: "unknown oauth provider" }, 400);
192
+ const input = typeof body.input === "string" ? body.input : typeof body.code === "string" ? body.code : "";
193
+ // Authorization responses are measured in hundreds of bytes; never accept the
194
+ // generic management-body allowance here.
195
+ if (input.length > 4096) return jsonResponse({ error: "input too long" }, 400);
196
+ const result = submitManualLoginCode(provider, input);
197
+ if (!result.ok) return jsonResponse({ error: result.error }, 409);
198
+ return jsonResponse({ ok: true });
199
+ }
200
+
201
+ if (url.pathname === "/api/oauth/status" && req.method === "GET") {
202
+ const provider = (url.searchParams.get("provider") ?? "").trim().toLowerCase();
203
+ if (!isPublicOAuthProvider(provider)) return jsonResponse({ error: "unknown oauth provider" }, 400);
204
+ return jsonResponse(getLoginStatus(provider));
205
+ }
206
+
207
+ if (url.pathname === "/api/oauth/logout" && req.method === "POST") {
208
+ const provider = (url.searchParams.get("provider") ?? "").trim().toLowerCase();
209
+ if (!isPublicOAuthProvider(provider)) return jsonResponse({ error: "unknown oauth provider" }, 400);
210
+ await removeCredential(provider);
211
+ reconcileLiveStateStores();
212
+ clearLoginState(provider);
213
+ // Drop cached/last-good quota rows tied to the removed credential.
214
+ const { clearProviderQuotaCache, clearAccountQuotaCache } = await import("../../providers/quota");
215
+ clearProviderQuotaCache();
216
+ clearAccountQuotaCache(provider);
217
+ return jsonResponse({ success: true });
218
+ }
219
+
220
+ // Multiauth account management: list a provider's logged-in accounts, switch the active
221
+ // one, or remove one. Emails are masked; tokens never leave the store.
222
+ if (url.pathname === "/api/oauth/accounts" && req.method === "GET") {
223
+ const provider = (url.searchParams.get("provider") ?? "").trim().toLowerCase();
224
+ if (!isPublicOAuthProvider(provider)) return jsonResponse({ error: "unknown oauth provider" }, 400);
225
+ const status = getLoginStatus(provider);
226
+ const { getAccountSet } = await import("../../oauth/store");
227
+ const {
228
+ oauthAccountHealthFields,
229
+ projectOAuthAccountHealth,
230
+ projectStoredOAuthAccountHealth,
231
+ } = await import("../../oauth/health");
232
+ const projectAccounts = () => {
233
+ const set = getAccountSet(provider);
234
+ const current = getLoginStatus(provider);
235
+ return {
236
+ activeAccountId: current.activeAccountId ?? null,
237
+ accounts: (current.accounts ?? []).map(summary => {
238
+ const full = set?.accounts.find(account => account.id === summary.id);
239
+ const health = full
240
+ ? projectStoredOAuthAccountHealth(provider, full)
241
+ : projectOAuthAccountHealth({
242
+ needsReauth: summary.needsReauth === true,
243
+ reauthReason: summary.needsReauth === true ? "refresh_failed" : undefined,
244
+ });
245
+ return { ...summary, ...oauthAccountHealthFields(provider, summary.id, health) };
246
+ }),
247
+ };
248
+ };
249
+ // Per-account rate limits: Anthropic (5h/weekly) and xAI (monthly credits + plan badge)
250
+ // report usage per credential, so every logged-in account can show its own bars — not
251
+ // just the active one. Opt-in via ?quota=1 so the plain account list stays a cheap local
252
+ // read; ?refresh=1 bypasses the TTL.
253
+ const wantQuota = url.searchParams.get("quota") === "1" && supportsPerAccountQuota(provider);
254
+ if (!wantQuota) return jsonResponse(projectAccounts());
255
+ const forceRefresh = url.searchParams.get("refresh") === "1";
256
+ // Probing may refresh the active credential and mark needsReauth — project health
257
+ // from the post-probe store so the response is not stale.
258
+ const rows = await fetchProviderAccountQuotas(provider, forceRefresh);
259
+ const byId = new Map(rows.map(row => [row.accountId, row]));
260
+ const projected = projectAccounts();
261
+ return jsonResponse({
262
+ activeAccountId: projected.activeAccountId,
263
+ accounts: projected.accounts.map(account => {
264
+ const row = byId.get(account.id);
265
+ if (!row) return account;
266
+ return {
267
+ ...account,
268
+ quota: row.quota,
269
+ ...(row.plan ? { plan: row.plan } : {}),
270
+ ...(row.unavailable ? { quotaUnavailable: true } : {}),
271
+ };
272
+ }),
273
+ });
274
+ }
275
+ if (url.pathname === "/api/oauth/accounts/active" && req.method === "PUT") {
276
+ const body = await readManagementJsonBodyOr(req, {}) as { provider?: string; accountId?: string };
277
+ const provider = (body.provider ?? "").trim().toLowerCase();
278
+ if (!isPublicOAuthProvider(provider)) return jsonResponse({ error: "unknown oauth provider" }, 400);
279
+ if (!body.accountId) return jsonResponse({ error: "missing accountId" }, 400);
280
+ const { setActiveAccount } = await import("../../oauth/store");
281
+ if (!(await setActiveAccount(provider, body.accountId))) return jsonResponse({ error: "account not found" }, 404);
282
+ if (provider === "anthropic") {
283
+ const { resetAnthropicRoutingForManualSelection } = await import("../../oauth/anthropic-routing");
284
+ resetAnthropicRoutingForManualSelection(body.accountId);
285
+ }
286
+ const { clearProviderQuotaCache } = await import("../../providers/quota");
287
+ clearProviderQuotaCache();
288
+ return jsonResponse({ ok: true, provider, activeAccountId: body.accountId });
289
+ }
290
+
291
+ // Opt-in Anthropic OAuth account pool (#294): enable/threshold/strategy + clear cooldown.
292
+ if (url.pathname === "/api/oauth/accounts/pool" && req.method === "GET") {
293
+ const provider = (url.searchParams.get("provider") ?? "").trim().toLowerCase();
294
+ if (provider !== "anthropic") return jsonResponse({ error: "pool config is only supported for anthropic" }, 400);
295
+ const pool = config.anthropicAccountPool ?? {};
296
+ return jsonResponse({
297
+ provider,
298
+ enabled: pool.enabled === true,
299
+ autoSwitchThreshold: typeof pool.autoSwitchThreshold === "number" ? pool.autoSwitchThreshold : 80,
300
+ strategy: normalizeAccountPoolStrategy(pool.strategy),
301
+ stickyLimit: normalizeAccountPoolStickyLimit(pool.stickyLimit),
302
+ experimental: true,
303
+ });
304
+ }
305
+ if (url.pathname === "/api/oauth/accounts/pool" && (req.method === "PUT" || req.method === "PATCH")) {
306
+ const parsedBody = await readManagementJsonBodyOr(req, {});
307
+ if (!isPlainRecord(parsedBody)) {
308
+ return jsonResponse({ error: "body must be an object" }, 400);
309
+ }
310
+ const body = parsedBody as {
311
+ provider?: unknown;
312
+ enabled?: unknown;
313
+ autoSwitchThreshold?: unknown;
314
+ strategy?: unknown;
315
+ stickyLimit?: unknown;
316
+ };
317
+ const provider = typeof body.provider === "string" ? body.provider.trim().toLowerCase() : "";
318
+ if (provider !== "anthropic") return jsonResponse({ error: "pool config is only supported for anthropic" }, 400);
319
+ let enabled = config.anthropicAccountPool?.enabled === true;
320
+ if (body.enabled !== undefined) {
321
+ if (typeof body.enabled !== "boolean") return jsonResponse({ error: "enabled must be a boolean" }, 400);
322
+ enabled = body.enabled;
323
+ }
324
+ let threshold = config.anthropicAccountPool?.autoSwitchThreshold ?? 80;
325
+ if (body.autoSwitchThreshold !== undefined) {
326
+ if (
327
+ typeof body.autoSwitchThreshold !== "number"
328
+ || !Number.isInteger(body.autoSwitchThreshold)
329
+ || body.autoSwitchThreshold < 0
330
+ || body.autoSwitchThreshold > 100
331
+ ) {
332
+ return jsonResponse({ error: "autoSwitchThreshold must be an integer 0-100" }, 400);
333
+ }
334
+ threshold = body.autoSwitchThreshold;
335
+ }
336
+ let strategy = config.anthropicAccountPool?.strategy;
337
+ if (body.strategy !== undefined) {
338
+ const parsed = parseAccountPoolStrategy(body.strategy);
339
+ if (parsed === null) {
340
+ return jsonResponse({ error: "strategy must be one of: quota, round-robin, fill-first" }, 400);
341
+ }
342
+ strategy = parsed;
343
+ }
344
+ let stickyLimit = config.anthropicAccountPool?.stickyLimit;
345
+ if (body.stickyLimit !== undefined) {
346
+ const parsed = parseAccountPoolStickyLimit(body.stickyLimit);
347
+ if (parsed === null) {
348
+ return jsonResponse({ error: "stickyLimit must be an integer 1-100" }, 400);
349
+ }
350
+ stickyLimit = parsed;
351
+ }
352
+ config.anthropicAccountPool = {
353
+ enabled,
354
+ autoSwitchThreshold: threshold,
355
+ ...(strategy !== undefined ? { strategy } : {}),
356
+ ...(stickyLimit !== undefined ? { stickyLimit } : {}),
357
+ };
358
+ saveConfigPreservingClaudeCode(config);
359
+ reconcileLiveStateStores();
360
+ return jsonResponse({
361
+ ok: true,
362
+ provider,
363
+ enabled,
364
+ autoSwitchThreshold: threshold,
365
+ strategy: normalizeAccountPoolStrategy(strategy),
366
+ stickyLimit: normalizeAccountPoolStickyLimit(stickyLimit),
367
+ experimental: true,
368
+ });
369
+ }
370
+ if (url.pathname === "/api/oauth/accounts/clear-cooldown" && req.method === "POST") {
371
+ const body = await readManagementJsonBodyOr(req, {}) as { provider?: unknown; accountId?: unknown };
372
+ const provider = typeof body.provider === "string" ? body.provider.trim().toLowerCase() : "";
373
+ const accountId = typeof body.accountId === "string" ? body.accountId.trim() : "";
374
+ if (provider !== "anthropic") return jsonResponse({ error: "clear-cooldown is only supported for anthropic" }, 400);
375
+ if (!accountId) return jsonResponse({ error: "missing accountId" }, 400);
376
+ const { clearAnthropicAccountCooldown } = await import("../../oauth/anthropic-routing");
377
+ const cleared = clearAnthropicAccountCooldown(accountId);
378
+ return jsonResponse({ ok: true, cleared });
379
+ }
380
+
381
+ if (url.pathname === "/api/oauth/accounts/alias" && req.method === "PUT") {
382
+ const body = await readManagementJsonBodyOr(req, {}) as { provider?: unknown; accountId?: unknown; alias?: unknown };
383
+ const provider = typeof body.provider === "string" ? body.provider.trim().toLowerCase() : "";
384
+ const accountId = typeof body.accountId === "string" ? body.accountId.trim() : "";
385
+ const alias = typeof body.alias === "string" ? body.alias.trim() : "";
386
+ if (!isPublicOAuthProvider(provider)) return jsonResponse({ error: "unknown oauth provider" }, 400);
387
+ if (!accountId) return jsonResponse({ error: "missing accountId" }, 400);
388
+ if (typeof body.alias !== "string" || alias.length > 80 || /[\x00-\x1f\x7f]/.test(alias)) {
389
+ return jsonResponse({ error: "alias must be at most 80 printable characters" }, 400);
390
+ }
391
+ const { setAccountAlias } = await import("../../oauth/store");
392
+ if (!(await setAccountAlias(provider, accountId, alias || undefined))) return jsonResponse({ error: "account not found" }, 404);
393
+ return jsonResponse({ ok: true, provider, accountId, alias: alias || null });
394
+ }
395
+ if (url.pathname === "/api/oauth/accounts" && req.method === "DELETE") {
396
+ const provider = (url.searchParams.get("provider") ?? "").trim().toLowerCase();
397
+ const id = url.searchParams.get("id") ?? "";
398
+ if (!isPublicOAuthProvider(provider)) return jsonResponse({ error: "unknown oauth provider" }, 400);
399
+ if (!id) return jsonResponse({ error: "missing id" }, 400);
400
+ const { removeAccount, getAccountSet } = await import("../../oauth/store");
401
+ if (!(await removeAccount(provider, id))) return jsonResponse({ error: "account not found" }, 404);
402
+ reconcileLiveStateStores();
403
+ if (provider === "anthropic") {
404
+ const { clearAnthropicAccountCooldown, clearAnthropicSessionAffinityForAccount } = await import("../../oauth/anthropic-routing");
405
+ clearAnthropicAccountCooldown(id);
406
+ clearAnthropicSessionAffinityForAccount(id);
407
+ }
408
+ if (!getAccountSet(provider)) clearLoginState(provider);
409
+ const { clearProviderQuotaCache, clearAccountQuotaCache } = await import("../../providers/quota");
410
+ clearProviderQuotaCache();
411
+ clearAccountQuotaCache(provider);
412
+ return jsonResponse({ ok: true });
413
+ }
414
+
415
+ // Multi-key pool for API-key providers (same GUI dropdown as OAuth multiauth): list masked
416
+ // keys, add one (upserts + activates), switch the active key, or remove one. `apiKey` always
417
+ // mirrors the active entry so routing is untouched.
418
+ if (url.pathname === "/api/providers/keys" && req.method === "GET") {
419
+ const name = (url.searchParams.get("name") ?? "").trim();
420
+ if (!name || !isValidProviderName(name) || !hasOwnProvider(config.providers, name)) return jsonResponse({ error: "unknown provider" }, 404);
421
+ const { listProviderApiKeys } = await import("../../providers/api-keys");
422
+ return jsonResponse(listProviderApiKeys(config, name));
423
+ }
424
+ if (url.pathname === "/api/providers/keys" && req.method === "POST") {
425
+ const body = await readManagementJsonBodyOr(req, {}) as { name?: string; key?: string; label?: string };
426
+ const name = (body.name ?? "").trim();
427
+ if (!name || !isValidProviderName(name) || !hasOwnProvider(config.providers, name)) return jsonResponse({ error: "unknown provider" }, 404);
428
+ if (typeof body.key !== "string" || !body.key.trim()) return jsonResponse({ error: "key is required" }, 400);
429
+ const { addProviderApiKey } = await import("../../providers/api-keys");
430
+ const result = addProviderApiKey(config, name, body.key, body.label);
431
+ if ("error" in result) return jsonResponse({ error: result.error }, 400);
432
+ const { clearModelCache } = await import("../../codex/model-cache");
433
+ clearModelCache(name);
434
+ const { clearProviderQuotaCache } = await import("../../providers/quota");
435
+ clearProviderQuotaCache();
436
+ const { clearKeyCooldowns } = await import("../../providers/key-failover");
437
+ clearKeyCooldowns(name); // manual key management resets 429 cooldown state
438
+ return jsonResponse({ ok: true, id: result.id }, 201);
439
+ }
440
+ if (url.pathname === "/api/providers/keys/active" && req.method === "PUT") {
441
+ const body = await readManagementJsonBodyOr(req, {}) as { name?: string; id?: string };
442
+ const name = (body.name ?? "").trim();
443
+ if (!name || !isValidProviderName(name) || !hasOwnProvider(config.providers, name)) return jsonResponse({ error: "unknown provider" }, 404);
444
+ if (!body.id) return jsonResponse({ error: "missing id" }, 400);
445
+ const { setActiveProviderApiKey } = await import("../../providers/api-keys");
446
+ if (!setActiveProviderApiKey(config, name, body.id)) return jsonResponse({ error: "key not found" }, 404);
447
+ const { clearModelCache } = await import("../../codex/model-cache");
448
+ clearModelCache(name);
449
+ const { clearProviderQuotaCache } = await import("../../providers/quota");
450
+ clearProviderQuotaCache();
451
+ const { clearKeyCooldowns } = await import("../../providers/key-failover");
452
+ clearKeyCooldowns(name); // manual key management resets 429 cooldown state
453
+ return jsonResponse({ ok: true, name, activeId: body.id });
454
+ }
455
+ if (url.pathname === "/api/providers/keys/alias" && req.method === "PUT") {
456
+ const body = await readManagementJsonBodyOr(req, {}) as { name?: unknown; id?: unknown; alias?: unknown };
457
+ const name = typeof body.name === "string" ? body.name.trim() : "";
458
+ const id = typeof body.id === "string" ? body.id.trim() : "";
459
+ const alias = typeof body.alias === "string" ? body.alias.trim() : "";
460
+ if (!name || !isValidProviderName(name) || !hasOwnProvider(config.providers, name)) return jsonResponse({ error: "unknown provider" }, 404);
461
+ if (!id) return jsonResponse({ error: "missing id" }, 400);
462
+ if (typeof body.alias !== "string" || alias.length > 80 || /[\x00-\x1f\x7f]/.test(alias)) {
463
+ return jsonResponse({ error: "alias must be at most 80 printable characters" }, 400);
464
+ }
465
+ const { setProviderApiKeyLabel } = await import("../../providers/api-keys");
466
+ if (!setProviderApiKeyLabel(config, name, id, alias || undefined)) return jsonResponse({ error: "key not found" }, 404);
467
+ return jsonResponse({ ok: true, name, id, alias: alias || null });
468
+ }
469
+ if (url.pathname === "/api/providers/keys" && req.method === "DELETE") {
470
+ const name = (url.searchParams.get("name") ?? "").trim();
471
+ const id = url.searchParams.get("id") ?? "";
472
+ if (!name || !isValidProviderName(name) || !hasOwnProvider(config.providers, name)) return jsonResponse({ error: "unknown provider" }, 404);
473
+ if (!id) return jsonResponse({ error: "missing id" }, 400);
474
+ const { removeProviderApiKey } = await import("../../providers/api-keys");
475
+ if (!removeProviderApiKey(config, name, id)) return jsonResponse({ error: "key not found" }, 404);
476
+ const { clearModelCache } = await import("../../codex/model-cache");
477
+ clearModelCache(name);
478
+ const { clearProviderQuotaCache } = await import("../../providers/quota");
479
+ clearProviderQuotaCache();
480
+ const { clearKeyCooldowns } = await import("../../providers/key-failover");
481
+ clearKeyCooldowns(name); // manual key management resets 429 cooldown state
482
+ return jsonResponse({ ok: true });
483
+ }
484
+
485
+ // ---------------------------------------------------------------------------
486
+ // API Keys management
487
+ // ---------------------------------------------------------------------------
488
+ if (url.pathname === "/api/keys" && req.method === "GET") {
489
+ const keys = config.apiKeys ?? [];
490
+ const endpoints = buildApiAccessEndpoints(config, {
491
+ requestUrl: req.url,
492
+ requestHost: req.headers.get("host"),
493
+ requestOrigin: req.headers.get("origin"),
494
+ });
495
+ const { readApiKeyUsageRollup } = await import("./api-key-usage");
496
+ const { rollup, attributionSince, historyTruncated } = await readApiKeyUsageRollup(keys.map(k => k.id), config.managementUsageMaxReadBytes);
497
+ return jsonResponse({
498
+ // 8 random hex past the fixed `ocx_data_` literal: enough to tell two keys
499
+ // apart in a list, with 128 bits of the tail still unrevealed. Masking only
500
+ // 8 characters showed `ocx_data...` for every key ever generated.
501
+ keys: keys.map(k => ({
502
+ id: k.id,
503
+ name: k.name,
504
+ prefix: k.key.slice(0, 17) + "...",
505
+ createdAt: k.createdAt,
506
+ usage: rollup.get(k.id) ?? { requests7d: 0, totalRequests: 0 },
507
+ })),
508
+ // Dataset-level and singular: it describes the usage log, not any one key.
509
+ ...(attributionSince ? { attributionSince } : {}),
510
+ ...(historyTruncated ? { historyTruncated: true } : {}),
511
+ authMatrix: AUTH_MATRIX,
512
+ ...endpoints,
513
+ }, 200, req, config);
514
+ }
515
+
516
+ if (url.pathname === "/api/keys" && req.method === "POST") {
517
+ const body = await readJsonBody(req);
518
+ if (!body) return jsonResponse({ error: "invalid body" }, 400, req, config);
519
+ const nameField = validateKeyName(body.name, { required: false });
520
+ if ("error" in nameField) return jsonResponse({ error: nameField.error }, 400, req, config);
521
+ const name = nameField.value || "default";
522
+ // A direct random draw. The previous derivation hashed every configured
523
+ // provider API key into the input, which was never needed for uniqueness and
524
+ // made this secret's safety argument depend on string concatenation rather
525
+ // than the RNG. 20 bytes is the same 40 hex characters as before, so nothing
526
+ // that pattern-matches the key shape changes.
527
+ const key = "ocx_data_" + randomBytes(20).toString("hex");
528
+ const entry = { id: randomUUID(), name, key, createdAt: new Date().toISOString() };
529
+ config.apiKeys = [...(config.apiKeys ?? []), entry];
530
+ saveConfigPreservingClaudeCode(config);
531
+ reconcileLiveStateStores();
532
+ return jsonResponse({ id: entry.id, name: entry.name, key: entry.key, createdAt: entry.createdAt }, 201, req, config);
533
+ }
534
+
535
+ if (url.pathname === "/api/keys" && req.method === "PATCH") {
536
+ const body = await readJsonBody(req);
537
+ if (!body) return jsonResponse({ error: "invalid body" }, 400, req, config);
538
+ if (typeof body.id !== "string" || !body.id) return jsonResponse({ error: "id required" }, 400, req, config);
539
+ const nameField = validateKeyName(body.name, { required: true });
540
+ if ("error" in nameField) return jsonResponse({ error: nameField.error }, 400, req, config);
541
+ const entry = (config.apiKeys ?? []).find(k => k.id === body.id);
542
+ if (!entry) return jsonResponse({ error: "key not found" }, 404, req, config);
543
+ entry.name = nameField.value;
544
+ saveConfigPreservingClaudeCode(config);
545
+ reconcileLiveStateStores();
546
+ // Never echo key material from a rename.
547
+ return jsonResponse({ id: entry.id, name: entry.name, createdAt: entry.createdAt }, 200, req, config);
548
+ }
549
+
550
+ if (url.pathname === "/api/keys" && req.method === "DELETE") {
551
+ const body = await readJsonBody(req);
552
+ if (!body) return jsonResponse({ error: "invalid body" }, 400, req, config);
553
+ if (typeof body.id !== "string" || !body.id) return jsonResponse({ error: "id required" }, 400, req, config);
554
+ const before = (config.apiKeys ?? []).length;
555
+ config.apiKeys = (config.apiKeys ?? []).filter(k => k.id !== body.id);
556
+ // A stale id must not read as a successful revocation.
557
+ if (config.apiKeys.length === before) return jsonResponse({ error: "key not found" }, 404, req, config);
558
+ saveConfigPreservingClaudeCode(config);
559
+ reconcileLiveStateStores();
560
+ return jsonResponse({ success: true }, 200, req, config);
561
+ }
562
+ return null;
563
+ }