@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,598 @@
1
+ /**
2
+ * /v1/live and /v1/realtime/calls relay (issue #371).
3
+ *
4
+ * Codex App / ChatGPT voice (GPT‑Live / Frameless Bidi) POSTs call-create against the injected
5
+ * `base_url`, then opens a sideband WebSocket at `/v1/live/{callId}` (Frameless) or
6
+ * `/v1/realtime?call_id=` (Realtime v1). Under Design B that host is this proxy.
7
+ *
8
+ * Inbound HTTP:
9
+ * - `POST /v1/live` — Frameless / ChatGPT App shape against an injected `/v1` base
10
+ * - `POST /v1/realtime/calls` — openai/codex RealtimeCallClient and the public OpenAI Realtime API
11
+ *
12
+ * Upstream HTTP (matches openai/codex `RealtimeCallClient`):
13
+ * - ChatGPT `backend-api` → JSON `{ sdp, session? }` at
14
+ * `{base}/realtime/calls?intent=quicksilver&architecture=avas`
15
+ * - OpenAI API-key provider → multipart at
16
+ * `{base}/v1/realtime/calls?intent=quicksilver&architecture=avas`
17
+ *
18
+ * Inbound sideband WebSocket (transparent bidirectional relay):
19
+ * - `GET /v1/live/{callId}` — Frameless
20
+ * - `GET /v1/realtime/calls/{callId}` — path-form join
21
+ * - `GET /v1/realtime?call_id=` — Realtime v1/v2 join
22
+ */
23
+ import { appendFileSync } from "node:fs";
24
+ import { formatErrorResponse } from "../bridge";
25
+ import {
26
+ CodexAccountCooldownError,
27
+ cooldownErrorResponse,
28
+ CodexAuthContextError,
29
+ CodexPoolAuthenticationError,
30
+ CodexThreadAffinityExpiredError,
31
+ } from "../codex/auth-context";
32
+ import { formatCodexProviderForLog } from "../codex/routing";
33
+ import { signalWithTimeout } from "../lib/abort";
34
+ import { sidecarEnter } from "../lib/sidecar-tracker";
35
+ import type { OcxConfig } from "../types";
36
+ import { resolveFirstUsableOpenAiSidecar, selectOpenAiImagesProvider } from "../providers/openai-sidecar";
37
+ import { ForwardAdmissionCredentialError, validateForwardAdmissionCredential } from "./auth-cors";
38
+ import type { RequestLogContext } from "./request-log";
39
+ import { codexLogAccountId } from "./responses";
40
+
41
+ /** Voice call create can wait on SDP negotiation; bound a hung upstream. */
42
+ const LIVE_UPSTREAM_TIMEOUT_MS = 120_000;
43
+ export const LIVE_REQUEST_MAX_BYTES = 16 * 1024 * 1024;
44
+ export const LIVE_RESPONSE_MAX_BYTES = 16 * 1024 * 1024;
45
+ const LIVE_RELAY_HEADERS = ["content-type", "location"] as const;
46
+
47
+ /** AVAS WebRTC call-create query (openai/codex `configure_realtime_call_request`). */
48
+ export const LIVE_AVAS_QUERY = "intent=quicksilver&architecture=avas";
49
+
50
+ /**
51
+ * Sideband WebSocket API root. openai/codex joins the sideband via the API provider default
52
+ * (`to_api_provider(AuthMode::ApiKey)` → https://api.openai.com/v1) even for ChatGPT-auth calls
53
+ * created through backend-api; chatgpt.com/backend-api rejects sideband upgrades pre-101
54
+ * (verified live 2026-07-24). The call-create bearer works on the API host unchanged.
55
+ */
56
+ export const LIVE_SIDEBAND_API_ROOT = "https://api.openai.com/v1";
57
+
58
+ /**
59
+ * Client protocol headers relayed verbatim to the upstream on call-create and sideband upgrade.
60
+ * `openai-alpha: quicksilver=v2` carries the Frameless protocol negotiation — without it the
61
+ * ChatGPT backend validates the type-less Frameless session as v1 quicksilver and 400s
62
+ * (openai/codex `realtime_request_headers`, core/src/realtime_conversation.rs). Auth headers
63
+ * (`authorization`, `chatgpt-account-id`) stay proxy-owned and are never taken from this list.
64
+ */
65
+ export const LIVE_CLIENT_PROTOCOL_HEADERS = [
66
+ "openai-alpha",
67
+ "x-session-id",
68
+ "session-id",
69
+ "thread-id",
70
+ "originator",
71
+ "x-oai-attestation",
72
+ ] as const;
73
+
74
+ /**
75
+ * Env-gated sideband frame forensics (diagnostic for multibyte transcript corruption).
76
+ *
77
+ * When `OCX_LIVE_FRAME_LOG` is set to a file path, every relayed sideband frame appends one
78
+ * JSONL record: direction, frame kind, byte length, and whether the payload contains U+FFFD.
79
+ * Privacy: full frame payloads are never written — only when U+FFFD is present, a short
80
+ * excerpt around the first replacement character is included so the corruption point can be
81
+ * attributed (upstream vs relay vs client). Disabled entirely when the env var is unset.
82
+ */
83
+ export const LIVE_FRAME_LOG_ENV = "OCX_LIVE_FRAME_LOG";
84
+ const LIVE_FRAME_LOG_CONTEXT_CHARS = 24;
85
+
86
+ function fffdContext(text: string): string | undefined {
87
+ const idx = text.indexOf("\uFFFD");
88
+ if (idx < 0) return undefined;
89
+ const start = Math.max(0, idx - LIVE_FRAME_LOG_CONTEXT_CHARS);
90
+ const end = Math.min(text.length, idx + LIVE_FRAME_LOG_CONTEXT_CHARS);
91
+ return text.slice(start, end);
92
+ }
93
+
94
+ export function logLiveSidebandFrame(dir: "c2u" | "u2c", data: unknown): void {
95
+ const logPath = process.env[LIVE_FRAME_LOG_ENV];
96
+ if (!logPath) return;
97
+ try {
98
+ let kind: "text" | "binary" = "binary";
99
+ let bytes = 0;
100
+ let context: string | undefined;
101
+ if (typeof data === "string") {
102
+ kind = "text";
103
+ bytes = Buffer.byteLength(data);
104
+ context = fffdContext(data);
105
+ } else if (data instanceof ArrayBuffer) {
106
+ bytes = data.byteLength;
107
+ context = fffdContext(new TextDecoder().decode(new Uint8Array(data)));
108
+ } else if (ArrayBuffer.isView(data)) {
109
+ const view = new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
110
+ bytes = data.byteLength;
111
+ context = fffdContext(new TextDecoder().decode(view));
112
+ } else {
113
+ return;
114
+ }
115
+ const record = {
116
+ ts: new Date().toISOString(),
117
+ dir,
118
+ kind,
119
+ bytes,
120
+ fffd: context !== undefined,
121
+ ...(context !== undefined ? { context } : {}),
122
+ };
123
+ appendFileSync(logPath, `${JSON.stringify(record)}\n`);
124
+ } catch {
125
+ // Frame forensics must never break the relay.
126
+ }
127
+ }
128
+
129
+ function clientProtocolHeaders(reqHeaders: Headers): Record<string, string> {
130
+ const out: Record<string, string> = {};
131
+ for (const name of LIVE_CLIENT_PROTOCOL_HEADERS) {
132
+ const value = reqHeaders.get(name);
133
+ if (value != null && value !== "") out[name] = value;
134
+ }
135
+ return out;
136
+ }
137
+
138
+ const LIVE_CALL_ID_RE = /^[A-Za-z0-9_-]{1,128}$/;
139
+
140
+ export type LiveSidebandTarget =
141
+ | { style: "frameless-path"; callId: string }
142
+ | { style: "realtime-calls-path"; callId: string }
143
+ | { style: "realtime-query"; callId: string };
144
+
145
+ export type LiveRelayTarget = {
146
+ headers: Record<string, string>;
147
+ providerBaseUrl: string;
148
+ usesBackendShape: boolean;
149
+ keyed: boolean;
150
+ recordOutcome?: (status: number | "timeout" | "connect_error") => void;
151
+ };
152
+
153
+ function isChatGptBackendBaseUrl(baseUrl: string): boolean {
154
+ return baseUrl.includes("/backend-api");
155
+ }
156
+
157
+ function withAvasQuery(url: string): string {
158
+ if (/[?&]intent=/.test(url) && /[?&]architecture=/.test(url)) return url;
159
+ return url.includes("?") ? `${url}&${LIVE_AVAS_QUERY}` : `${url}?${LIVE_AVAS_QUERY}`;
160
+ }
161
+
162
+ export function keyedLiveUrl(baseUrl: string): string {
163
+ return withAvasQuery(`${baseUrl.replace(/\/v1\/?$/, "")}/v1/realtime/calls`);
164
+ }
165
+
166
+ export function forwardLiveUrl(baseUrl: string, usesBackendShape: boolean): string {
167
+ const root = baseUrl.replace(/\/$/, "");
168
+ if (usesBackendShape) return withAvasQuery(`${root}/realtime/calls`);
169
+ // Frameless API shape posts to /live without the AVAS query (codex RealtimeCallClient).
170
+ return `${root}/live`;
171
+ }
172
+
173
+ function httpsToWss(httpUrl: string): string {
174
+ if (httpUrl.startsWith("https://")) return `wss://${httpUrl.slice("https://".length)}`;
175
+ if (httpUrl.startsWith("http://")) return `ws://${httpUrl.slice("http://".length)}`;
176
+ return httpUrl;
177
+ }
178
+
179
+ export function parseLiveSidebandTarget(pathname: string, searchParams: URLSearchParams): LiveSidebandTarget | null {
180
+ const liveMatch = pathname.match(/^\/v1\/live\/([^/]+)\/?$/);
181
+ if (liveMatch) {
182
+ const callId = decodeURIComponent(liveMatch[1]!);
183
+ if (!LIVE_CALL_ID_RE.test(callId)) return null;
184
+ return { style: "frameless-path", callId };
185
+ }
186
+ const callsMatch = pathname.match(/^\/v1\/realtime\/calls\/([^/]+)\/?$/);
187
+ if (callsMatch) {
188
+ const callId = decodeURIComponent(callsMatch[1]!);
189
+ if (!LIVE_CALL_ID_RE.test(callId)) return null;
190
+ return { style: "realtime-calls-path", callId };
191
+ }
192
+ if (pathname === "/v1/realtime" || pathname === "/v1/realtime/") {
193
+ const callId = searchParams.get("call_id")?.trim() ?? "";
194
+ if (!LIVE_CALL_ID_RE.test(callId)) return null;
195
+ return { style: "realtime-query", callId };
196
+ }
197
+ return null;
198
+ }
199
+
200
+ /**
201
+ * True for the loopback hosts plaintext development servers listen on.
202
+ * `URL.hostname` keeps the brackets on IPv6, so both forms are accepted.
203
+ */
204
+ function isLoopbackHost(hostname: string): boolean {
205
+ const lower = hostname.toLowerCase();
206
+ return lower === "localhost" || lower.endsWith(".localhost")
207
+ || lower === "127.0.0.1" || lower.startsWith("127.")
208
+ || lower === "::1" || lower === "[::1]";
209
+ }
210
+
211
+ /**
212
+ * Normalize the sideband base to end in exactly `/v1`, with no query, fragment,
213
+ * or userinfo. Any failure closes to the canonical Realtime API root — never to
214
+ * the input — because this string decides where upstream bearer credentials and
215
+ * user audio are sent.
216
+ *
217
+ * Bounds, all fail-closed:
218
+ * - scheme must be https/wss, or http/ws with a loopback host (the local
219
+ * development case this knob exists for);
220
+ * - URL userinfo is rejected (URL#toString would forward it verbatim);
221
+ * - unparseable input is rejected.
222
+ *
223
+ * Endpoint-form overrides are recognized the way upstream recognizes them
224
+ * (codex-rs realtime_websocket/methods.rs:994): a terminal `/realtime`,
225
+ * `/realtime/calls/<id>`, or `/live/<id>` is stripped so the root can be
226
+ * re-derived. A path prefix survives (`https://host/api/v1` keeps `/api`).
227
+ */
228
+ function normalizeSidebandRoot(baseUrl: string): string {
229
+ let parsed: URL;
230
+ try {
231
+ parsed = new URL(baseUrl);
232
+ } catch {
233
+ return LIVE_SIDEBAND_API_ROOT;
234
+ }
235
+ const secure = parsed.protocol === "https:" || parsed.protocol === "wss:";
236
+ const plaintext = parsed.protocol === "http:" || parsed.protocol === "ws:";
237
+ if ((!secure && !plaintext) || (plaintext && !isLoopbackHost(parsed.hostname)) || parsed.username || parsed.password) {
238
+ return LIVE_SIDEBAND_API_ROOT;
239
+ }
240
+ parsed.search = "";
241
+ parsed.hash = "";
242
+ const path = parsed.pathname
243
+ .replace(/\/+$/, "")
244
+ .replace(/\/realtime(?:\/calls\/[^/]+)?$/, "")
245
+ .replace(/\/live\/[^/]+$/, "")
246
+ .replace(/\/v1$/, "");
247
+ parsed.pathname = `${path}/v1`;
248
+ return parsed.toString().replace(/\/$/, "");
249
+ }
250
+
251
+ /**
252
+ * Resolve the sideband base. Upstream policy (codex-rs 438c9e98d): the sideband
253
+ * join is NOT derived from the selected model provider — precedence is exactly
254
+ * the explicit override when configured, otherwise the canonical Realtime API
255
+ * root. The provider base URL deliberately plays no part; a user who needs a
256
+ * non-canonical host sets the override, the same escape hatch upstream ships as
257
+ * `experimental_realtime_ws_base_url`.
258
+ */
259
+ function sidebandBaseRoot(overrideBaseUrl?: string): string {
260
+ return normalizeSidebandRoot(overrideBaseUrl?.trim() || LIVE_SIDEBAND_API_ROOT);
261
+ }
262
+
263
+ /**
264
+ * Build the upstream sideband WebSocket URL for a resolved OpenAI/ChatGPT provider.
265
+ * Mirrors openai/codex `websocket_url_from_api_url_for_call` + `normalize_realtime_path`.
266
+ *
267
+ * Deliberate deviation: the realtime-query style keeps `intent=quicksilver`,
268
+ * which upstream does not send. That URL is live against real OpenAI
269
+ * infrastructure for every canonical voice user and this parameter is known to
270
+ * work; dropping it is future work gated on a live smoke test. Parity here is
271
+ * scoped to the host, override precedence, and provider-query exclusion.
272
+ */
273
+ export function buildLiveSidebandUpstreamWsUrl(
274
+ target: LiveSidebandTarget,
275
+ overrideBaseUrl?: string,
276
+ ): string {
277
+ const sidebandRoot = sidebandBaseRoot(overrideBaseUrl);
278
+ if (target.style === "frameless-path") {
279
+ return httpsToWss(`${sidebandRoot}/live/${target.callId}`);
280
+ }
281
+ if (target.style === "realtime-calls-path") {
282
+ return httpsToWss(`${sidebandRoot}/realtime/calls/${target.callId}`);
283
+ }
284
+ return httpsToWss(
285
+ `${sidebandRoot}/realtime?intent=quicksilver&call_id=${encodeURIComponent(target.callId)}`,
286
+ );
287
+ }
288
+
289
+ async function backendJsonBodyFromApiMultipart(
290
+ body: ArrayBuffer,
291
+ contentType: string,
292
+ ): Promise<{ body: Uint8Array; contentType: string } | Response> {
293
+ let form: FormData;
294
+ try {
295
+ form = await new Response(body, { headers: { "content-type": contentType } }).formData();
296
+ } catch {
297
+ return formatErrorResponse(
298
+ 400,
299
+ "invalid_request_error",
300
+ "ChatGPT voice relay could not parse multipart call-create body",
301
+ );
302
+ }
303
+ const sdp = form.get("sdp");
304
+ if (typeof sdp !== "string") {
305
+ return formatErrorResponse(
306
+ 400,
307
+ "invalid_request_error",
308
+ "ChatGPT voice relay expects multipart field sdp on call-create",
309
+ );
310
+ }
311
+ // `session` is optional on the public Realtime calls API; omit when the client sends SDP only.
312
+ const sessionRaw = form.get("session");
313
+ let session: unknown | undefined;
314
+ if (sessionRaw != null) {
315
+ if (typeof sessionRaw !== "string") {
316
+ return formatErrorResponse(
317
+ 400,
318
+ "invalid_request_error",
319
+ "ChatGPT voice relay expected a string multipart session field",
320
+ );
321
+ }
322
+ try {
323
+ session = JSON.parse(sessionRaw);
324
+ } catch {
325
+ return formatErrorResponse(
326
+ 400,
327
+ "invalid_request_error",
328
+ "ChatGPT voice relay expected JSON in the multipart session field",
329
+ );
330
+ }
331
+ }
332
+ const payload = session === undefined ? { sdp } : { sdp, session };
333
+ const encoded = new TextEncoder().encode(JSON.stringify(payload));
334
+ return { body: encoded, contentType: "application/json" };
335
+ }
336
+
337
+ /** Read a body stream with a hard byte cap so oversized payloads abort before full buffering. */
338
+ export async function readBodyCapped(
339
+ stream: ReadableStream<Uint8Array> | null,
340
+ maxBytes: number,
341
+ tooLargeMessage: (total: number) => string,
342
+ ): Promise<ArrayBuffer | Response> {
343
+ if (!stream) return new ArrayBuffer(0);
344
+ const reader = stream.getReader();
345
+ const chunks: Uint8Array[] = [];
346
+ let total = 0;
347
+ try {
348
+ for (;;) {
349
+ const { done, value } = await reader.read();
350
+ if (done) break;
351
+ if (!value || value.byteLength === 0) continue;
352
+ total += value.byteLength;
353
+ if (total > maxBytes) {
354
+ await reader.cancel().catch(() => {});
355
+ return formatErrorResponse(502, "upstream_error", tooLargeMessage(total));
356
+ }
357
+ chunks.push(value);
358
+ }
359
+ } finally {
360
+ try {
361
+ reader.releaseLock();
362
+ } catch {
363
+ // already released / cancelled
364
+ }
365
+ }
366
+ if (chunks.length === 0) return new ArrayBuffer(0);
367
+ if (chunks.length === 1) {
368
+ const only = chunks[0]!;
369
+ return only.buffer.slice(only.byteOffset, only.byteOffset + only.byteLength) as ArrayBuffer;
370
+ }
371
+ const merged = new Uint8Array(total);
372
+ let offset = 0;
373
+ for (const chunk of chunks) {
374
+ merged.set(chunk, offset);
375
+ offset += chunk.byteLength;
376
+ }
377
+ return merged.buffer;
378
+ }
379
+
380
+ async function readRequestBodyCapped(req: Request, maxBytes: number): Promise<ArrayBuffer | Response> {
381
+ try {
382
+ const result = await readBodyCapped(
383
+ req.body,
384
+ maxBytes,
385
+ total => `live request body too large (${total} bytes)`,
386
+ );
387
+ if (result instanceof Response) {
388
+ // Oversize inbound is a client error, not an upstream failure.
389
+ return formatErrorResponse(413, "invalid_request_error", `live request body too large`);
390
+ }
391
+ return result;
392
+ } catch (err) {
393
+ if (req.signal.aborted) {
394
+ return formatErrorResponse(499, "client_closed_request", "live request canceled by client");
395
+ }
396
+ return formatErrorResponse(
397
+ 400,
398
+ "invalid_request_error",
399
+ `live request body unreadable: ${err instanceof Error ? err.message : String(err)}`,
400
+ );
401
+ }
402
+ }
403
+
404
+ /**
405
+ * Resolve OpenAI/ChatGPT auth + headers for live HTTP or sideband WebSocket relays.
406
+ * Shared by call-create and sideband so pool token override stays consistent.
407
+ */
408
+ export async function resolveLiveRelay(
409
+ req: Request,
410
+ config: OcxConfig,
411
+ logCtx: RequestLogContext,
412
+ ): Promise<LiveRelayTarget | Response> {
413
+ try {
414
+ validateForwardAdmissionCredential(req.headers, config);
415
+ } catch (err) {
416
+ if (err instanceof ForwardAdmissionCredentialError) {
417
+ return formatErrorResponse(401, "authentication_error", err.message);
418
+ }
419
+ throw err;
420
+ }
421
+
422
+ const candidates = selectOpenAiImagesProvider(config);
423
+ if (candidates.forwardCandidates.length === 0 && !candidates.keyed) {
424
+ return formatErrorResponse(
425
+ 400,
426
+ "invalid_request_error",
427
+ "Built-in ChatGPT voice needs an OpenAI upstream (ChatGPT login or an OpenAI API-key provider), "
428
+ + "but none is configured in opencodex. Routed providers cannot serve voice call-create.",
429
+ );
430
+ }
431
+
432
+ let forward: Awaited<ReturnType<typeof resolveFirstUsableOpenAiSidecar>> | undefined;
433
+ let forwardAuthError: Response | undefined;
434
+ if (candidates.forwardCandidates.length > 0) {
435
+ try {
436
+ forward = await resolveFirstUsableOpenAiSidecar(candidates.forwardCandidates, req.headers, config);
437
+ if (forward) {
438
+ logCtx.provider = formatCodexProviderForLog(
439
+ forward.providerName,
440
+ codexLogAccountId(forward.authContext),
441
+ config,
442
+ );
443
+ }
444
+ } catch (err) {
445
+ if (err instanceof CodexAccountCooldownError) {
446
+ forwardAuthError = cooldownErrorResponse(err);
447
+ } else if (err instanceof CodexThreadAffinityExpiredError) {
448
+ forwardAuthError = formatErrorResponse(
449
+ 409,
450
+ "invalid_request_error",
451
+ "Codex thread account affinity expired; start a new session",
452
+ );
453
+ } else if (err instanceof CodexAuthContextError) {
454
+ const safeAccountLabel = formatCodexProviderForLog("openai", err.accountId, config);
455
+ console.error(`[live] Pool account ${safeAccountLabel} token failed; reauthentication required`);
456
+ forwardAuthError = formatErrorResponse(
457
+ 401,
458
+ "authentication_error",
459
+ "Selected Codex account needs reauthentication",
460
+ );
461
+ } else if (err instanceof CodexPoolAuthenticationError) {
462
+ forwardAuthError = formatErrorResponse(401, "authentication_error", err.message);
463
+ } else {
464
+ throw err;
465
+ }
466
+ }
467
+ }
468
+
469
+ // Client protocol headers first so provider/auth headers below always win on conflict.
470
+ const headers: Record<string, string> = clientProtocolHeaders(req.headers);
471
+ if (forward) {
472
+ const { provider } = forward;
473
+ if (provider.headers) Object.assign(headers, provider.headers);
474
+ for (const [name, value] of forward.headers) headers[name] = value;
475
+ logCtx.model = "gpt-live";
476
+ return {
477
+ headers,
478
+ providerBaseUrl: provider.baseUrl,
479
+ usesBackendShape: isChatGptBackendBaseUrl(provider.baseUrl),
480
+ keyed: false,
481
+ recordOutcome: status => forward.recordOutcome?.(status),
482
+ };
483
+ }
484
+ if (forwardAuthError) return forwardAuthError;
485
+ if (candidates.keyed) {
486
+ const { provider, apiKey, providerName } = candidates.keyed;
487
+ if (provider.headers) Object.assign(headers, provider.headers);
488
+ headers.authorization = `Bearer ${apiKey}`;
489
+ logCtx.provider = providerName;
490
+ logCtx.model = "gpt-live";
491
+ return {
492
+ headers,
493
+ providerBaseUrl: provider.baseUrl,
494
+ usesBackendShape: false,
495
+ keyed: true,
496
+ };
497
+ }
498
+ return formatErrorResponse(
499
+ 401,
500
+ "authentication_error",
501
+ "voice relay needs ChatGPT auth (Authorization header) or an OpenAI API-key provider",
502
+ );
503
+ }
504
+
505
+ export async function handleLive(
506
+ req: Request,
507
+ config: OcxConfig,
508
+ logCtx: RequestLogContext,
509
+ ): Promise<Response> {
510
+ const inboundContentType = req.headers.get("content-type") ?? "application/octet-stream";
511
+ const inboundBodyOrError = await readRequestBodyCapped(req, LIVE_REQUEST_MAX_BYTES);
512
+ if (inboundBodyOrError instanceof Response) return inboundBodyOrError;
513
+ const inboundBody = inboundBodyOrError;
514
+
515
+ const relay = await resolveLiveRelay(req, config, logCtx);
516
+ if (relay instanceof Response) return relay;
517
+
518
+ const headers: Record<string, string> = { ...relay.headers };
519
+ let url: string;
520
+ let outboundBody: ArrayBuffer = inboundBody;
521
+ let outboundContentType = inboundContentType;
522
+
523
+ if (!relay.keyed) {
524
+ url = forwardLiveUrl(relay.providerBaseUrl, relay.usesBackendShape);
525
+ if (relay.usesBackendShape && inboundContentType.toLowerCase().includes("multipart/form-data")) {
526
+ const rewritten = await backendJsonBodyFromApiMultipart(inboundBody, inboundContentType);
527
+ if (rewritten instanceof Response) return rewritten;
528
+ outboundBody = rewritten.body.buffer.slice(
529
+ rewritten.body.byteOffset,
530
+ rewritten.body.byteOffset + rewritten.body.byteLength,
531
+ ) as ArrayBuffer;
532
+ outboundContentType = rewritten.contentType;
533
+ }
534
+ } else {
535
+ url = keyedLiveUrl(relay.providerBaseUrl);
536
+ }
537
+
538
+ headers["content-type"] = outboundContentType;
539
+
540
+ const linkedSignal = signalWithTimeout(LIVE_UPSTREAM_TIMEOUT_MS, req.signal);
541
+ const sidecarExit = sidecarEnter("live");
542
+ try {
543
+ const upstreamResponse = await fetch(url, {
544
+ method: "POST",
545
+ headers,
546
+ body: outboundBody,
547
+ signal: linkedSignal.signal,
548
+ });
549
+ // Record every completed upstream response before body size handling so account health /
550
+ // cooldown still updates when we reject an oversized payload.
551
+ relay.recordOutcome?.(upstreamResponse.status);
552
+ const payload = await readBodyCapped(
553
+ upstreamResponse.body,
554
+ LIVE_RESPONSE_MAX_BYTES,
555
+ total => `live response too large (${total} bytes)`,
556
+ );
557
+ if (payload instanceof Response) return payload;
558
+ const relayHeaders: Record<string, string> = {};
559
+ for (const name of LIVE_RELAY_HEADERS) {
560
+ const value = upstreamResponse.headers.get(name);
561
+ if (value) relayHeaders[name] = value;
562
+ }
563
+ return new Response(payload, { status: upstreamResponse.status, headers: relayHeaders });
564
+ } catch (err) {
565
+ if (req.signal.aborted) {
566
+ return formatErrorResponse(499, "client_closed_request", "live request canceled by client");
567
+ }
568
+ if (err instanceof Error && err.name === "TimeoutError") {
569
+ relay.recordOutcome?.("timeout");
570
+ return formatErrorResponse(504, "upstream_error", "live upstream timed out");
571
+ }
572
+ relay.recordOutcome?.("connect_error");
573
+ return formatErrorResponse(
574
+ 502,
575
+ "upstream_error",
576
+ `live relay failed: ${err instanceof Error ? err.message : String(err)}`,
577
+ );
578
+ } finally {
579
+ sidecarExit();
580
+ linkedSignal.cleanup();
581
+ }
582
+ }
583
+
584
+ /** Resolve sideband upstream WebSocket URL + headers for an accepted upgrade. */
585
+ export async function resolveLiveSidebandUpgrade(
586
+ req: Request,
587
+ config: OcxConfig,
588
+ logCtx: RequestLogContext,
589
+ target: LiveSidebandTarget,
590
+ ): Promise<{ headers: Record<string, string>; upstreamWsUrl: string; recordOutcome?: LiveRelayTarget["recordOutcome"] } | Response> {
591
+ const relay = await resolveLiveRelay(req, config, logCtx);
592
+ if (relay instanceof Response) return relay;
593
+ return {
594
+ headers: relay.headers,
595
+ upstreamWsUrl: buildLiveSidebandUpstreamWsUrl(target, config.experimentalRealtimeWsBaseUrl),
596
+ recordOutcome: relay.recordOutcome,
597
+ };
598
+ }