@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,829 @@
1
+ /**
2
+ * Media bridge agentic loop — supports both image and video generation sidecars.
3
+ *
4
+ * The routed (non-OpenAI) model runs in a bounded loop. Each iteration is streamed and fully
5
+ * buffered internally. If the model calls an image-generation or video-generation tool, the
6
+ * bridge fulfills it via the xAI sidecar, injects the result as a tool_result, and loops
7
+ * (bounded by maxRounds). When the model produces a real tool call or the budget is exhausted,
8
+ * the passthrough events are replayed to the bridge for final SSE output.
9
+ *
10
+ * Removed vs web-search: no sidecar backend selection, no forced-answer nudge, no failed-query
11
+ * dedup, no describeImages/structuredOutput, no recordSidecarOutcome.
12
+ */
13
+ import type { AdapterRequest, IncomingMeta, ProviderAdapter } from "../adapters/base";
14
+ import { existsSync } from "node:fs";
15
+ import { pathToFileURL } from "node:url";
16
+ import { createAdapterEventQueue } from "../adapters/run-turn-queue";
17
+ import type { AdapterEvent, OcxMessage, OcxParsedRequest, OcxProviderContinuationState, OcxRequestOptions, OcxThinkingContent, OcxUsage } from "../types";
18
+ import { namespacedToolName } from "../types";
19
+ import { bridgeToResponsesSSE } from "../bridge";
20
+ import { clearableDeadline, idleDeadline } from "../lib/abort";
21
+ import { readBoundedResponseBody } from "../lib/bounded-body";
22
+ import { fetchWithResetRetry } from "../lib/upstream-retry";
23
+ import {
24
+ isTranslatorBudgetExceededError,
25
+ TRANSLATOR_MAX_TURN_BYTES,
26
+ TranslatorBudgetExceededError,
27
+ } from "../lib/translator-budget";
28
+ import { parseStreamWithProgress, RoutedModelInactivityError, WebSearchStreamProtocolError } from "../web-search/progress-stream";
29
+ import { fulfillImageCall } from "./fulfill";
30
+ import { parseVideoCallArgs, pollVideoWithHeartbeats, buildVideoResult, createVideoBudget } from "./fulfill-video";
31
+ import { submitVideoJob } from "./xai-video-client";
32
+ import { downloadVideoToArtifact, createImageBudget, pruneArtifacts } from "./artifacts";
33
+ import { IMAGE_GEN_TOOL_NAME, VIDEO_GEN_TOOL_NAME } from "./synthetic-tool";
34
+ import type { ImageBridgePlan, VideoBridgePlan } from "./types";
35
+
36
+ const SSE_HEADERS = {
37
+ "Content-Type": "text/event-stream",
38
+ "Cache-Control": "no-cache",
39
+ "Connection": "keep-alive",
40
+ "X-Accel-Buffering": "no",
41
+ };
42
+
43
+ const CONNECT_TIMEOUT_MS = 200_000;
44
+ const STALL_TIMEOUT_MS = 200_000;
45
+ export const DEFAULT_MAX_ROUNDS = 3;
46
+ /** Absolute ceiling so a hand-edited `images.maxRounds: 10000` cannot unbound paid xAI calls. */
47
+ export const MAX_ROUNDS_HARD_LIMIT = 10;
48
+ /** Cap paid xAI image fulfillments per turn (parallel calls in one round count separately). */
49
+ export const MAX_IMAGE_CALLS_PER_TURN = 10;
50
+ /** Cap paid xAI video fulfillments per turn (video is slower/costlier than image). */
51
+ export const MAX_VIDEO_CALLS_PER_TURN = 3;
52
+
53
+ /**
54
+ * Clamp a configured maxRounds value to a safe integer in [0, MAX_ROUNDS_HARD_LIMIT].
55
+ * Non-finite / non-number inputs fall back to DEFAULT_MAX_ROUNDS.
56
+ */
57
+ export function clampImageMaxRounds(value: unknown): number {
58
+ if (typeof value !== "number" || !Number.isFinite(value)) return DEFAULT_MAX_ROUNDS;
59
+ return Math.max(0, Math.min(MAX_ROUNDS_HARD_LIMIT, Math.floor(value)));
60
+ }
61
+
62
+ /** Drop image/video-specific tool_choice when media tools are stripped for a forced-final pass. */
63
+ function stripMediaToolChoice(
64
+ options: OcxRequestOptions,
65
+ plan?: ImageBridgePlan,
66
+ videoPlan?: VideoBridgePlan,
67
+ ): OcxRequestOptions {
68
+ const tc = options.toolChoice;
69
+ if (!tc || typeof tc !== "object") return options;
70
+ const isMediaTool = (name: string): boolean =>
71
+ name === IMAGE_GEN_TOOL_NAME ||
72
+ name === VIDEO_GEN_TOOL_NAME ||
73
+ (plan?.toolNames.has(name) ?? false) ||
74
+ (videoPlan?.toolNames.has(name) ?? false);
75
+ if ("name" in tc && typeof tc.name === "string") {
76
+ if (isMediaTool(tc.name)) {
77
+ return { ...options, toolChoice: "auto" };
78
+ }
79
+ return options;
80
+ }
81
+ if ("allowedTools" in tc && Array.isArray(tc.allowedTools)) {
82
+ const filtered = tc.allowedTools.filter(name => !isMediaTool(name));
83
+ if (filtered.length === tc.allowedTools.length) return options;
84
+ if (filtered.length === 0) return { ...options, toolChoice: "auto" };
85
+ return { ...options, toolChoice: { ...tc, allowedTools: filtered } };
86
+ }
87
+ return options;
88
+ }
89
+
90
+ interface ImageCall {
91
+ id: string;
92
+ name: string;
93
+ args: string;
94
+ }
95
+
96
+ /**
97
+ * Split an iteration's adapter events into (a) the image-generation tool calls to intercept and
98
+ * (b) the events to pass through to Codex. An image tool-call's own start/delta/end events are
99
+ * dropped (Codex never sees the synthetic tool); every other event — text, thinking, real tool
100
+ * calls, done — is preserved in order.
101
+ */
102
+ function scanEventsForImageCall(events: AdapterEvent[], toolNames: Set<string>): {
103
+ calls: ImageCall[];
104
+ passthrough: AdapterEvent[];
105
+ hasRealToolCall: boolean;
106
+ } {
107
+ const calls: ImageCall[] = [];
108
+ const passthrough: AdapterEvent[] = [];
109
+ let hasRealToolCall = false;
110
+ let pending: { name: string; id: string; argsBuf: string; events: AdapterEvent[] } | null = null;
111
+ const flushPending = (): void => {
112
+ if (!pending) return;
113
+ if (toolNames.has(pending.name)) {
114
+ // Unterminated image call still carries buffered args — fulfill so malformed JSON
115
+ // becomes a normal tool_result error instead of silently vanishing.
116
+ calls.push({ id: pending.id, name: pending.name, args: pending.argsBuf });
117
+ } else {
118
+ passthrough.push(...pending.events);
119
+ hasRealToolCall = true;
120
+ }
121
+ pending = null;
122
+ };
123
+ for (const e of events) {
124
+ if (e.type === "tool_call_start") {
125
+ flushPending();
126
+ pending = { name: e.name, id: e.id, argsBuf: "", events: [e] };
127
+ } else if (e.type === "tool_call_delta" && pending) {
128
+ pending.argsBuf += e.arguments;
129
+ pending.events.push(e);
130
+ } else if (e.type === "tool_call_end" && pending) {
131
+ pending.events.push(e);
132
+ if (toolNames.has(pending.name)) {
133
+ calls.push({ id: pending.id, name: pending.name, args: pending.argsBuf });
134
+ } else {
135
+ passthrough.push(...pending.events);
136
+ hasRealToolCall = true;
137
+ }
138
+ pending = null;
139
+ } else {
140
+ flushPending();
141
+ passthrough.push(e);
142
+ }
143
+ }
144
+ flushPending();
145
+ return { calls, passthrough, hasRealToolCall };
146
+ }
147
+
148
+ async function* replay(events: AdapterEvent[]): AsyncGenerator<AdapterEvent> {
149
+ for (const e of events) yield e;
150
+ }
151
+
152
+ /**
153
+ * Collect thinking / redacted_thinking blocks that preceded an image tool call, preserving
154
+ * stream order and per-block signatures. Anthropic extended thinking REQUIRES the assistant
155
+ * message containing tool_use to start with its signed thinking blocks — flattening multiple
156
+ * blocks into one signature 400s on replay.
157
+ */
158
+ function extractIterationThinking(events: AdapterEvent[]): OcxThinkingContent[] {
159
+ const parts: OcxThinkingContent[] = [];
160
+ let thinking = "";
161
+ let signature: string | undefined;
162
+
163
+ const flushVisible = () => {
164
+ if (!thinking && !signature) return;
165
+ parts.push({
166
+ type: "thinking",
167
+ thinking,
168
+ ...(signature ? { signature } : {}),
169
+ });
170
+ thinking = "";
171
+ signature = undefined;
172
+ };
173
+
174
+ for (const e of events) {
175
+ if (e.type === "thinking_delta") {
176
+ thinking += e.thinking;
177
+ } else if (e.type === "thinking_signature") {
178
+ signature = e.signature;
179
+ flushVisible();
180
+ } else if (e.type === "redacted_thinking") {
181
+ flushVisible();
182
+ parts.push({ type: "thinking", thinking: "", redacted: [e.data] });
183
+ }
184
+ }
185
+ flushVisible();
186
+ return parts;
187
+ }
188
+
189
+ function jsonError(status: number, message: string): Response {
190
+ return new Response(JSON.stringify({ error: { message, type: "upstream_error", code: null } }), {
191
+ status,
192
+ headers: { "Content-Type": "application/json" },
193
+ });
194
+ }
195
+
196
+ /** Hard provider/parse failure inside an iteration. The eager first iteration converts it to a
197
+ * non-2xx jsonError; later (already-streaming) iterations surface it as an in-stream error event. */
198
+ class LoopError extends Error {
199
+ constructor(readonly status: number, message: string) {
200
+ super(message);
201
+ this.name = "LoopError";
202
+ }
203
+ }
204
+
205
+ export interface ImageBridgeDeps {
206
+ parsed: OcxParsedRequest;
207
+ adapter: ProviderAdapter;
208
+ incomingMeta: IncomingMeta;
209
+ plan?: ImageBridgePlan;
210
+ videoPlan?: VideoBridgePlan;
211
+ /** Per-video generation timeout (ms) including polling. */
212
+ videoTimeoutMs?: number;
213
+ /** Headers forwarded from the original request (e.g. Codex auth). Cloned per iteration. */
214
+ forwardHeaders?: Headers;
215
+ /** Called before each routed-model dispatch in the bridge loop, for attempt telemetry. */
216
+ onAttemptSend?: () => void;
217
+ /** Called after each upstream request is built (parity with web-search / normal path). */
218
+ onRequestBuilt?: (request: AdapterRequest) => void;
219
+ abortSignal?: AbortSignal;
220
+ onFirstOutput?: () => void;
221
+ /** Max image-generation rounds before forcing a final answer. Defaults to 3; clamped to [0, 10]. */
222
+ maxRounds?: number;
223
+ /** Connect / response-header budget for non-runTurn iterations. */
224
+ connectTimeoutMs?: number;
225
+ /** Stall budget (seconds) forwarded to bridgeToResponsesSSE; also bounds runTurn collect. */
226
+ stallTimeoutSec?: number;
227
+ /** Provider-specific fetch (e.g. xAI transport wrapper). Falls back to global fetch. */
228
+ fetchImpl?: typeof globalThis.fetch;
229
+ /** Raw adapter usage at the terminal event, pre wire-normalization (see bridgeToResponsesSSE onUsage). */
230
+ onUsage?: (usage: OcxUsage | undefined) => void;
231
+ /**
232
+ * Optional 429 key-failover for the routed (non-xAI) model. Return a rebuilt adapter for the
233
+ * rotated key, or null when the pool is exhausted.
234
+ */
235
+ on429?: (retryAfterHeader: string | null) => ProviderAdapter | null;
236
+ /** Called when the bridged Responses stream completes (parity with runTurn / routed paths). */
237
+ onCompletedResponse?: (response: Record<string, unknown>, providerState?: OcxProviderContinuationState) => void;
238
+ /** WebSocket Responses path only — leave response id empty for protocol compatibility. */
239
+ forceEmptyResponseId?: boolean;
240
+ }
241
+
242
+ /**
243
+ * Run the main (non-OpenAI) model in a small agentic loop. Each upstream iteration is streamed and
244
+ * fully buffered internally so raw byte progress is observable without leaking the synthetic tool or
245
+ * preliminary assistant output. If the model invokes image generation, run it via the xAI sidecar,
246
+ * inject the answer as a tool_result, and loop (bounded by `maxRounds`).
247
+ */
248
+ export async function runWithImageBridge(deps: ImageBridgeDeps): Promise<Response> {
249
+ const translatorBudget = deps.incomingMeta.translatorBudget;
250
+ const { parsed, plan, videoPlan, videoTimeoutMs, abortSignal } = deps;
251
+ let adapter = deps.adapter;
252
+ const maxRounds = clampImageMaxRounds(deps.maxRounds ?? DEFAULT_MAX_ROUNDS);
253
+ const HARD_CAP = maxRounds + 1;
254
+ const connectTimeoutMs = typeof deps.connectTimeoutMs === "number" && Number.isFinite(deps.connectTimeoutMs) && deps.connectTimeoutMs > 0
255
+ ? Math.floor(deps.connectTimeoutMs)
256
+ : CONNECT_TIMEOUT_MS;
257
+ const stallTimeoutMs = typeof deps.stallTimeoutSec === "number" && Number.isFinite(deps.stallTimeoutSec) && deps.stallTimeoutSec > 0
258
+ ? Math.floor(deps.stallTimeoutSec * 1000)
259
+ : STALL_TIMEOUT_MS;
260
+ const fetchImpl = deps.fetchImpl ?? globalThis.fetch;
261
+ let paidImageCalls = 0;
262
+ let paidVideoCalls = 0;
263
+ let hiddenUsage: OcxUsage | undefined;
264
+
265
+ const addUsage = (a: OcxUsage | undefined, b: OcxUsage | undefined): OcxUsage | undefined => {
266
+ if (!a) return b;
267
+ if (!b) return a;
268
+ return {
269
+ inputTokens: a.inputTokens + b.inputTokens,
270
+ outputTokens: a.outputTokens + b.outputTokens,
271
+ ...(a.contextTotalTokens !== undefined || b.contextTotalTokens !== undefined
272
+ ? { contextTotalTokens: Math.max(a.contextTotalTokens ?? 0, b.contextTotalTokens ?? 0) }
273
+ : {}),
274
+ ...(a.cachedInputTokens !== undefined || b.cachedInputTokens !== undefined
275
+ ? { cachedInputTokens: (a.cachedInputTokens ?? 0) + (b.cachedInputTokens ?? 0) }
276
+ : {}),
277
+ ...(a.cacheReadInputTokens !== undefined || b.cacheReadInputTokens !== undefined
278
+ ? { cacheReadInputTokens: (a.cacheReadInputTokens ?? 0) + (b.cacheReadInputTokens ?? 0) }
279
+ : {}),
280
+ ...(a.cacheCreationInputTokens !== undefined || b.cacheCreationInputTokens !== undefined
281
+ ? { cacheCreationInputTokens: (a.cacheCreationInputTokens ?? 0) + (b.cacheCreationInputTokens ?? 0) }
282
+ : {}),
283
+ ...(a.reasoningOutputTokens !== undefined || b.reasoningOutputTokens !== undefined
284
+ ? { reasoningOutputTokens: (a.reasoningOutputTokens ?? 0) + (b.reasoningOutputTokens ?? 0) }
285
+ : {}),
286
+ ...(a.estimated || b.estimated ? { estimated: true } : {}),
287
+ };
288
+ };
289
+ const takeUsageFrom = (events: AdapterEvent[]): void => {
290
+ for (const e of events) {
291
+ if ((e.type === "done" || e.type === "incomplete") && e.usage) {
292
+ hiddenUsage = addUsage(hiddenUsage, e.usage);
293
+ }
294
+ }
295
+ };
296
+
297
+ const messages: OcxMessage[] = [...parsed.context.messages];
298
+ const allTools = parsed.context.tools ?? [];
299
+ // Merge tool names from both plans for event scanning.
300
+ const mediaToolNames = new Set<string>();
301
+ if (plan) for (const n of plan.toolNames) mediaToolNames.add(n);
302
+ if (videoPlan) for (const n of videoPlan.toolNames) mediaToolNames.add(n);
303
+ // Forced-final must strip every image/video-generation alias the plans know about — not only
304
+ // tools flagged `imageGeneration:true` or `videoGeneration:true`. Hosted `image_generation` /
305
+ // function aliases would otherwise remain callable; scanEventsForImageCall would strip the
306
+ // call while forceFinal blocks fulfillment, leaving the client an empty completion.
307
+ const toolsNoMedia = allTools.filter(t => {
308
+ if (t.imageGeneration) return false;
309
+ if (t.videoGeneration) return false;
310
+ if (plan && plan.toolNames.has(t.name)) return false;
311
+ if (plan && t.namespace && plan.toolNames.has(namespacedToolName(t.namespace, t.name))) return false;
312
+ if (videoPlan && videoPlan.toolNames.has(t.name)) return false;
313
+ return true;
314
+ });
315
+ const budget = createImageBudget();
316
+ const vBudget = createVideoBudget();
317
+
318
+ // Link an internal AbortController to the turn signal so a client cancel of the SSE body aborts
319
+ // in-flight model fetches AND the sidecar.
320
+ const internalAbort = new AbortController();
321
+ const linkAbort = (): void => internalAbort.abort(abortSignal?.reason);
322
+ if (abortSignal) {
323
+ if (abortSignal.aborted) linkAbort();
324
+ else abortSignal.addEventListener("abort", linkAbort, { once: true });
325
+ }
326
+ const signal = internalAbort.signal;
327
+
328
+ interface IterationResponse {
329
+ response: Response;
330
+ responseAdapter: ProviderAdapter;
331
+ }
332
+ type IterationSplit = ReturnType<typeof scanEventsForImageCall>;
333
+
334
+ // Acquire one iteration's final response headers. The first call is drained eagerly so an initial
335
+ // connect/header/HTTP failure stays a non-2xx JSON response — except for runTurn adapters, which
336
+ // have no HTTP status surface and must not block SSE headers behind queue.collect().
337
+ const prepareIterationEvents = async function* (forceFinal: boolean): AsyncGenerator<AdapterEvent, IterationResponse> {
338
+ const iterParsed: OcxParsedRequest = {
339
+ ...parsed,
340
+ stream: true,
341
+ context: { ...parsed.context, messages, tools: forceFinal ? toolsNoMedia : allTools },
342
+ options: forceFinal ? stripMediaToolChoice(parsed.options, plan, videoPlan) : parsed.options,
343
+ };
344
+
345
+ // runTurn adapters (Cursor) own all upstream communication via an emit callback. They don't
346
+ // expose buildRequest/fetchResponse/parseStream to the bridge, so collect their events through
347
+ // an AdapterEventQueue and wrap them in a pseudo-response whose parseStream replays them.
348
+ if (adapter.runTurn) {
349
+ const queue = createAdapterEventQueue({
350
+ onBacklogExceeded: () => internalAbort.abort("runTurn backlog exceeded"),
351
+ });
352
+ // Attempt telemetry must fire at dispatch time (parity with fetchOnce), not after collect.
353
+ deps.onAttemptSend?.();
354
+ void adapter
355
+ .runTurn(
356
+ iterParsed,
357
+ {
358
+ headers: deps.forwardHeaders ? new Headers(deps.forwardHeaders) : new Headers(),
359
+ abortSignal: signal,
360
+ translatorBudget,
361
+ },
362
+ queue.push,
363
+ )
364
+ .then(() => queue.close())
365
+ .catch(err => {
366
+ queue.push({ type: "error", message: err instanceof Error ? err.message : String(err) });
367
+ queue.close();
368
+ });
369
+
370
+ // Bound collect with a real *idle* deadline that resets on each emitted event.
371
+ // A fixed wall-clock race would abort legitimate long Cursor turns that keep
372
+ // producing tokens. Do NOT manufacture adapter heartbeats here — bridgeToResponsesSSE
373
+ // treats those as upstream activity and would defeat the stall guard. SSE keepalives
374
+ // come from the bridge heartbeat interval instead.
375
+ //
376
+ // On idle expiry: abort the runTurn signal AND close the queue so the consumer
377
+ // unblocks even when adapter.runTurn ignores cancellation and never settles.
378
+ let timedOut = false;
379
+ const idle = idleDeadline(stallTimeoutMs, () => {
380
+ timedOut = true;
381
+ // Cancel the fire-and-forget runTurn so a well-behaved adapter can stop.
382
+ internalAbort.abort(`runTurn inactivity timeout after ${stallTimeoutMs}ms`);
383
+ // Independently unblock queue.stream() — do not wait for runTurn to observe abort.
384
+ queue.close();
385
+ });
386
+ const events: AdapterEvent[] = [];
387
+ try {
388
+ idle.reset();
389
+ for await (const event of queue.stream()) {
390
+ if (timedOut) break;
391
+ idle.reset();
392
+ events.push(event);
393
+ }
394
+ } finally {
395
+ idle.cancel();
396
+ }
397
+ if (timedOut) {
398
+ throw new LoopError(504, `runTurn inactivity timeout after ${stallTimeoutMs}ms during image-bridge`);
399
+ }
400
+
401
+ // Preserve Cursor conversation continuity across image-loop iterations. runTurn mutates
402
+ // iterParsed (shallow copy); copy the id back onto the shared parsed request.
403
+ if (iterParsed._cursorConversationId) {
404
+ parsed._cursorConversationId = iterParsed._cursorConversationId;
405
+ }
406
+
407
+ // runTurn adapters signal errors via {type:"error"} events, not HTTP status codes.
408
+ const errorEvent = events.find(e => e.type === "error");
409
+ if (errorEvent && errorEvent.type === "error") {
410
+ if (errorEvent.code === "translation_buffer_limit") {
411
+ throw new TranslatorBudgetExceededError("retained_collectors", TRANSLATOR_MAX_TURN_BYTES);
412
+ }
413
+ throw new LoopError(502, errorEvent.message);
414
+ }
415
+
416
+ const wrappedAdapter: ProviderAdapter = {
417
+ ...adapter,
418
+ async *parseStream() {
419
+ for (const e of events) yield e;
420
+ },
421
+ };
422
+ return { response: new Response(new Uint8Array(0), { status: 200 }), responseAdapter: wrappedAdapter };
423
+ }
424
+
425
+ const headerDeadline = clearableDeadline(connectTimeoutMs, signal);
426
+ try {
427
+ const fetchOnce = async (requestAdapter: ProviderAdapter): Promise<IterationResponse> => {
428
+ const request = await requestAdapter.buildRequest(iterParsed, {
429
+ headers: deps.forwardHeaders ? new Headers(deps.forwardHeaders) : new Headers(),
430
+ abortSignal: headerDeadline.signal,
431
+ translatorBudget,
432
+ });
433
+ try { deps.onRequestBuilt?.(request); } catch { /* diagnostics are best-effort */ }
434
+ deps.onAttemptSend?.();
435
+ let response: Response;
436
+ try {
437
+ response = requestAdapter.fetchResponse
438
+ ? await requestAdapter.fetchResponse(request, {
439
+ abortSignal: headerDeadline.signal,
440
+ timeoutMs: connectTimeoutMs,
441
+ returnRawErrors: true,
442
+ stream: true,
443
+ })
444
+ : await fetchWithResetRetry(
445
+ () => {
446
+ const h = new Headers(request.headers);
447
+ if (!h.has("accept-encoding")) h.set("accept-encoding", "identity");
448
+ return fetchImpl(request.url, {
449
+ method: request.method,
450
+ headers: h,
451
+ body: request.body,
452
+ signal: headerDeadline.signal,
453
+ });
454
+ },
455
+ { abortSignal: headerDeadline.signal, label: "image-bridge-loop" },
456
+ );
457
+ } finally {
458
+ request.releaseBodyObservation?.();
459
+ }
460
+ return { response, responseAdapter: requestAdapter };
461
+ };
462
+
463
+ let prepared = await fetchOnce(adapter);
464
+ // 429 key-failover parity with web-search / normal routed path.
465
+ while (prepared.response.status === 429 && deps.on429) {
466
+ const rotated = deps.on429(prepared.response.headers.get("retry-after"));
467
+ if (!rotated) break;
468
+ try { void prepared.response.body?.cancel().catch(() => {}); } catch { /* already closed */ }
469
+ adapter = rotated;
470
+ yield { type: "heartbeat" };
471
+ prepared = await fetchOnce(adapter);
472
+ }
473
+
474
+ // Final headers have arrived. Clear only the deadline timer before ANY body read.
475
+ headerDeadline.clear();
476
+ if (!prepared.response.ok) {
477
+ let body: Awaited<ReturnType<typeof readBoundedResponseBody>>;
478
+ try {
479
+ body = await readBoundedResponseBody(prepared.response, { signal });
480
+ } catch {
481
+ if (signal.aborted) throw new LoopError(499, "client closed request during image-bridge");
482
+ throw new LoopError(prepared.response.status, `Provider error ${prepared.response.status}`);
483
+ }
484
+ let formatted = "";
485
+ if (body.displaySafe && !body.truncated && body.text.trim() && prepared.responseAdapter.formatErrorBody) {
486
+ try {
487
+ formatted = prepared.responseAdapter.formatErrorBody(
488
+ prepared.response.status,
489
+ prepared.response.headers,
490
+ body.text,
491
+ ).trim();
492
+ } catch { /* formatter hooks are best-effort */ }
493
+ }
494
+ const suffix = formatted ? `: ${formatted.slice(0, 400)}` : "";
495
+ throw new LoopError(prepared.response.status, `Provider error ${prepared.response.status}${suffix}`);
496
+ }
497
+ return prepared;
498
+ } catch (error) {
499
+ if (isTranslatorBudgetExceededError(error)) throw error;
500
+ if (headerDeadline.didExpire()) {
501
+ throw new LoopError(504, `Provider response-header timeout after ${connectTimeoutMs}ms during image-bridge`);
502
+ }
503
+ if (signal.aborted) throw new LoopError(499, "client closed request during image-bridge");
504
+ if (error instanceof LoopError) throw error;
505
+ throw new LoopError(502, `Provider unreachable: ${error instanceof Error ? error.message : String(error)}`);
506
+ } finally {
507
+ headerDeadline.clear();
508
+ }
509
+ };
510
+
511
+ const prepareIterationDrained = async (forceFinal: boolean): Promise<IterationResponse> => {
512
+ const it = prepareIterationEvents(forceFinal);
513
+ let r = await it.next();
514
+ while (!r.done) r = await it.next();
515
+ return r.value;
516
+ };
517
+
518
+ // Consume and validate one successful response body. Only invisible heartbeat events escape while
519
+ // semantic output remains buffered for safe scanning.
520
+ const consumeIterationEvents = async function* (prepared: IterationResponse): AsyncGenerator<AdapterEvent, IterationSplit> {
521
+ const events: AdapterEvent[] = [];
522
+ try {
523
+ const parse = prepared.responseAdapter.parseStream.bind(prepared.responseAdapter);
524
+ for await (const event of parseStreamWithProgress(prepared.response, parse, {
525
+ signal,
526
+ inactivityTimeoutMs: stallTimeoutMs,
527
+ translatorBudget,
528
+ })) {
529
+ if (event.type === "heartbeat") yield event;
530
+ else events.push(event);
531
+ }
532
+ } catch (error) {
533
+ if (isTranslatorBudgetExceededError(error)) throw error;
534
+ if (signal.aborted) throw new LoopError(499, "client closed request during image-bridge");
535
+ if (error instanceof RoutedModelInactivityError) throw new LoopError(504, error.message);
536
+ if (error instanceof WebSearchStreamProtocolError) throw new LoopError(502, error.message);
537
+ throw new LoopError(502, `Provider stream error: ${error instanceof Error ? error.message : String(error)}`);
538
+ }
539
+
540
+ const terminalIndexes = events.flatMap((event, index) =>
541
+ event.type === "done" || event.type === "incomplete" || event.type === "error" ? [index] : []);
542
+ if (terminalIndexes.length !== 1 || terminalIndexes[0] !== events.length - 1) {
543
+ throw new LoopError(502, `Image-bridge adapter stream protocol error: expected one final terminal event, received ${terminalIndexes.length}`);
544
+ }
545
+ const terminal = events[terminalIndexes[0]!];
546
+ if (terminal.type === "error") {
547
+ if (terminal.code === "translation_buffer_limit") {
548
+ throw new TranslatorBudgetExceededError("retained_collectors", TRANSLATOR_MAX_TURN_BYTES);
549
+ }
550
+ throw new LoopError(502, terminal.message);
551
+ }
552
+ return scanEventsForImageCall(events, mediaToolNames);
553
+ };
554
+
555
+ // Eagerly acquire only the FIRST iteration's final headers so connect/header/HTTP failures remain
556
+ // non-2xx JSON. Skip for runTurn adapters: their "headers" are synthetic, and awaiting
557
+ // queue.collect() before returning SSE starves clients of headers/heartbeats on slow first turns.
558
+ const skipEagerDrain = !!adapter.runTurn;
559
+ let firstPrepared: IterationResponse | undefined;
560
+ if (!skipEagerDrain) {
561
+ try {
562
+ firstPrepared = await prepareIterationDrained(maxRounds <= 0);
563
+ } catch (e) {
564
+ if (abortSignal) abortSignal.removeEventListener("abort", linkAbort);
565
+ if (e instanceof LoopError) return jsonError(e.status, e.message);
566
+ throw e;
567
+ }
568
+ }
569
+
570
+ const toolNsMap = new Map<string, { namespace: string; name: string }>();
571
+ const freeform = new Set<string>();
572
+ const toolSearch = new Set<string>();
573
+ for (const t of parsed.context.tools ?? []) {
574
+ if (t.namespace) toolNsMap.set(namespacedToolName(t.namespace, t.name), { namespace: t.namespace, name: t.name });
575
+ if (t.freeform) freeform.add(t.name);
576
+ if (t.toolSearch) toolSearch.add(t.name);
577
+ }
578
+
579
+ // Drive the remaining iterations live. Image generation runs interleaved with the real sidecar
580
+ // timing; the final answer's passthrough events come last.
581
+ async function* produce(): AsyncGenerator<AdapterEvent> {
582
+ let prepared = firstPrepared;
583
+ try {
584
+ for (let i = 0; i < HARD_CAP; i++) {
585
+ const forceFinal = i >= maxRounds;
586
+ try {
587
+ // First loop turn reuses the eager HEADERS when present. runTurn (and later iterations)
588
+ // acquire headers inside the live SSE stream so clients already have the response open.
589
+ if (!prepared || i > 0) {
590
+ yield { type: "heartbeat" };
591
+ prepared = yield* prepareIterationEvents(forceFinal);
592
+ }
593
+ // Raw-byte progress heartbeats reach the bridge; semantic events remain buffered.
594
+ const split = yield* consumeIterationEvents(prepared);
595
+ prepared = undefined;
596
+
597
+ // Loop (fulfill + re-ask) ONLY when the model's actionable output is purely image_gen. A
598
+ // real tool call means this turn is terminal for Codex — finalize so those calls reach
599
+ // Codex. forceFinal also finalizes.
600
+ const shouldLoop = split.calls.length > 0 && !split.hasRealToolCall && !forceFinal;
601
+ if (!shouldLoop) {
602
+ if (hiddenUsage) {
603
+ for (let i = split.passthrough.length - 1; i >= 0; i--) {
604
+ const e = split.passthrough[i];
605
+ if (e?.type === "done" || e?.type === "incomplete") {
606
+ split.passthrough[i] = { ...e, usage: addUsage(hiddenUsage, e.usage) };
607
+ break;
608
+ }
609
+ }
610
+ }
611
+ yield* replay(split.passthrough);
612
+ return;
613
+ }
614
+
615
+ // Discarded iteration still contributed tokens — accumulate for the final onUsage.
616
+ takeUsageFrom(split.passthrough);
617
+
618
+ // Fulfill each image/video call, then inject ONE assistant turn (thinking once + all tool
619
+ // calls) so Anthropic extended-thinking continuations stay valid across parallel calls.
620
+ const iterationThinking = extractIterationThinking(split.passthrough);
621
+ const fulfilled: Array<{ call: ImageCall; result: Awaited<ReturnType<typeof fulfillImageCall>>; args: Record<string, unknown> }> = [];
622
+ for (const call of split.calls) {
623
+ const isVideoCall = videoPlan?.toolNames.has(call.name) === true;
624
+ if (isVideoCall) {
625
+ yield { type: "heartbeat" };
626
+ if (paidVideoCalls >= MAX_VIDEO_CALLS_PER_TURN) {
627
+ const vResult = {
628
+ ok: false, model: videoPlan!.model, prompt: "", files: [], count: 0,
629
+ error: `video call budget exhausted (max ${MAX_VIDEO_CALLS_PER_TURN} per turn)`,
630
+ };
631
+ let pArgs: Record<string, unknown> = {};
632
+ try { const raw: unknown = JSON.parse(call.args || "{}"); if (raw !== null && typeof raw === "object" && !Array.isArray(raw)) pArgs = raw as Record<string, unknown>; } catch { /* malformed args */ }
633
+ fulfilled.push({ call, result: vResult, args: pArgs });
634
+ continue;
635
+ }
636
+ const vArgs = parseVideoCallArgs(call.args);
637
+ let vResult;
638
+ if (!vArgs.ok) {
639
+ vResult = { ok: false, model: videoPlan!.model, prompt: "", files: [], count: 0, error: vArgs.error };
640
+ } else {
641
+ paidVideoCalls += 1;
642
+ const videoTimeout = videoTimeoutMs ?? 300_000;
643
+ const deadlineSignal = AbortSignal.timeout(videoTimeout);
644
+ try {
645
+ const videoDeadline = Date.now() + videoTimeout;
646
+ // Bind submit to the shared deadline so submit + poll fit one budget.
647
+ const linkedDeadline = signal
648
+ ? AbortSignal.any([signal, deadlineSignal])
649
+ : deadlineSignal;
650
+ const { requestId } = await submitVideoJob(
651
+ {
652
+ prompt: vArgs.prompt, model: videoPlan!.model,
653
+ ...(vArgs.duration != null ? { duration: vArgs.duration } : {}),
654
+ ...(vArgs.resolution != null ? { resolution: vArgs.resolution } : {}),
655
+ ...(vArgs.aspectRatio != null ? { aspectRatio: vArgs.aspectRatio } : {}),
656
+ },
657
+ videoPlan!.auth, linkedDeadline,
658
+ );
659
+ const remainingMs = videoDeadline - Date.now();
660
+ if (remainingMs <= 0) {
661
+ vResult = { ok: false, model: videoPlan!.model, prompt: vArgs.prompt, files: [], count: 0, error: `video generation timed out after ${Math.floor(videoTimeout / 1000)}s` };
662
+ } else {
663
+ const pollGen = pollVideoWithHeartbeats(requestId, videoPlan!.auth, linkedDeadline, remainingMs);
664
+ let pollResult: { ok: true; videoUrl: string } | { ok: false; error: string };
665
+ try {
666
+ for (;;) {
667
+ const { value, done } = await pollGen.next();
668
+ if (done) { pollResult = value; break; }
669
+ yield { type: "heartbeat" };
670
+ }
671
+ } finally {
672
+ await pollGen.return({ ok: false, error: "cancelled" }).catch(() => {});
673
+ }
674
+ if (signal.aborted) throw new LoopError(499, "client closed request during video-bridge");
675
+ if (pollResult.ok) {
676
+ const dlPath = await downloadVideoToArtifact(pollResult.videoUrl, vBudget, signal);
677
+ vResult = buildVideoResult(dlPath, vArgs.prompt, videoPlan!.model);
678
+ } else {
679
+ vResult = { ok: false, model: videoPlan!.model, prompt: vArgs.prompt, files: [], count: 0, error: pollResult.error };
680
+ }
681
+ }
682
+ } catch (e) {
683
+ if (deadlineSignal.aborted && !signal.aborted) {
684
+ vResult = { ok: false, model: videoPlan!.model, prompt: vArgs.prompt ?? "", files: [], count: 0, error: `video generation timed out after ${Math.floor(videoTimeout / 1000)}s` };
685
+ } else if (signal.aborted) {
686
+ throw new LoopError(499, "client closed request during video-bridge");
687
+ } else {
688
+ const error = e instanceof Error ? e.message : String(e);
689
+ vResult = { ok: false, model: videoPlan!.model, prompt: vArgs.prompt ?? "", files: [], count: 0, error };
690
+ }
691
+ }
692
+ }
693
+ if (signal.aborted) throw new LoopError(499, "client closed request during video-bridge");
694
+ let vParsedArgs: Record<string, unknown> = {};
695
+ try { const raw: unknown = JSON.parse(call.args || "{}"); if (raw !== null && typeof raw === "object" && !Array.isArray(raw)) vParsedArgs = raw as Record<string, unknown>; } catch { /* malformed args */ }
696
+ fulfilled.push({ call, result: vResult, args: vParsedArgs });
697
+ } else {
698
+ yield { type: "heartbeat" };
699
+ if (!plan) {
700
+ fulfilled.push({ call, result: { ok: false, model: "", prompt: "", files: [], count: 0, error: "image bridge not configured" }, args: {} });
701
+ continue;
702
+ }
703
+ let result: Awaited<ReturnType<typeof fulfillImageCall>>;
704
+ if (paidImageCalls >= MAX_IMAGE_CALLS_PER_TURN) {
705
+ result = {
706
+ ok: false,
707
+ model: plan.model,
708
+ prompt: "",
709
+ files: [],
710
+ count: 0,
711
+ error: `image call budget exhausted (max ${MAX_IMAGE_CALLS_PER_TURN} per turn)`,
712
+ };
713
+ } else {
714
+ paidImageCalls += 1;
715
+ result = await fulfillImageCall(
716
+ { id: call.id, name: call.name, arguments: call.args },
717
+ plan, budget, signal,
718
+ );
719
+ }
720
+ if (signal.aborted) throw new LoopError(499, "client closed request during image-bridge");
721
+ let parsedArgs: Record<string, unknown> = {};
722
+ try {
723
+ const raw: unknown = JSON.parse(call.args || "{}");
724
+ if (raw !== null && typeof raw === "object" && !Array.isArray(raw)) {
725
+ parsedArgs = raw as Record<string, unknown>;
726
+ }
727
+ } catch { /* malformed args */ }
728
+ fulfilled.push({ call, result, args: parsedArgs });
729
+ }
730
+ }
731
+ // Prune artifacts once after the entire batch so a tight keepCount
732
+ // cannot delete a video from an earlier call in this same iteration.
733
+ pruneArtifacts(videoPlan?.artifactsKeepCount ?? plan?.artifactsKeepCount);
734
+ // Drop results whose artifact files were pruned — never hand the model a dead path.
735
+ for (const f of fulfilled) {
736
+ if (!f.result.ok || !f.result.files || f.result.files.length === 0) continue;
737
+ const survivors = f.result.files.filter(p => existsSync(p));
738
+ if (survivors.length === f.result.files.length) continue; // nothing pruned
739
+ if (survivors.length === 0) {
740
+ f.result = {
741
+ ok: false, model: f.result.model, prompt: f.result.prompt ?? "",
742
+ files: [], count: 0,
743
+ error: "artifact was pruned before delivery (increase artifactsKeepCount)",
744
+ } as typeof f.result;
745
+ } else {
746
+ // Some files survived — refresh from survivors
747
+ f.result = { ...f.result, files: survivors, count: survivors.length };
748
+ const primary = survivors[0]!;
749
+ (f.result as { path?: string }).path = primary;
750
+ if ("markdown" in f.result && f.result.markdown) {
751
+ // Image markdown references the primary path; video uses pathToFileURL
752
+ if (f.result.markdown.startsWith("![")) {
753
+ (f.result as { markdown: string }).markdown = `![image](${pathToFileURL(primary).href})`;
754
+ } else {
755
+ (f.result as { markdown: string }).markdown = `[video](${pathToFileURL(primary).href})`;
756
+ }
757
+ }
758
+ }
759
+ }
760
+ const now = Date.now();
761
+ messages.push({
762
+ role: "assistant",
763
+ content: [
764
+ ...iterationThinking,
765
+ ...fulfilled.map(({ call, args }) => ({
766
+ type: "toolCall" as const,
767
+ id: call.id,
768
+ name: call.name,
769
+ arguments: args,
770
+ })),
771
+ ],
772
+ timestamp: now,
773
+ });
774
+ for (const { call, result } of fulfilled) {
775
+ messages.push({
776
+ role: "toolResult",
777
+ toolCallId: call.id,
778
+ toolName: call.name,
779
+ content: JSON.stringify(result),
780
+ isError: !result.ok,
781
+ timestamp: now,
782
+ });
783
+ }
784
+ } catch (e) {
785
+ if (isTranslatorBudgetExceededError(e)) {
786
+ yield {
787
+ type: "error",
788
+ status: 502,
789
+ errorType: "upstream_error",
790
+ code: e.code,
791
+ message: "upstream translation buffer exceeded the safe limit",
792
+ ...(hiddenUsage ? { usage: hiddenUsage } : {}),
793
+ };
794
+ } else {
795
+ yield {
796
+ type: "error",
797
+ message: e instanceof LoopError ? e.message : (e instanceof Error ? e.message : String(e)),
798
+ ...(e instanceof LoopError ? { status: e.status } : {}),
799
+ ...(hiddenUsage ? { usage: hiddenUsage } : {}),
800
+ };
801
+ }
802
+ return;
803
+ }
804
+ }
805
+ } finally {
806
+ if (abortSignal) abortSignal.removeEventListener("abort", linkAbort);
807
+ }
808
+ }
809
+
810
+ const sse = bridgeToResponsesSSE(
811
+ produce(), parsed.modelId, toolNsMap, freeform, toolSearch, () => {
812
+ internalAbort.abort("client closed responses stream");
813
+ }, 2_000,
814
+ {
815
+ translatorBudget,
816
+ ...(deps.forceEmptyResponseId ? { responseId: "" } : {}),
817
+ hideThinkingSummary: parsed.options.hideThinkingSummary,
818
+ stallTimeoutSec: deps.stallTimeoutSec,
819
+ ...(deps.onFirstOutput ? { onFirstOutput: deps.onFirstOutput } : {}),
820
+ ...(deps.onUsage ? {
821
+ // Terminal done/incomplete already includes hiddenUsage (merged above). Do not
822
+ // add it again here or request logs double-count multi-iteration image turns.
823
+ onUsage: (usage: OcxUsage | undefined) => deps.onUsage?.(usage),
824
+ } : {}),
825
+ ...(deps.onCompletedResponse ? { onCompletedResponse: deps.onCompletedResponse } : {}),
826
+ },
827
+ );
828
+ return new Response(sse, { headers: SSE_HEADERS });
829
+ }