@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,872 @@
1
+ /**
2
+ * Claude Code outbound: internal /v1/responses output -> Anthropic Messages API shapes.
3
+ *
4
+ * Wire contract pinned in devlog/260711_claude_inbound/003_evidence.md (all Tier 2):
5
+ * - SSE order: message_start -> (content_block_start -> deltas -> content_block_stop)*
6
+ * -> message_delta -> message_stop; any number of `ping`.
7
+ * - thinking blocks get thinking_delta(s) then ONE synthetic signature_delta just
8
+ * before content_block_stop (CCR precedent: Claude Code does not verify signatures).
9
+ * - message_delta.usage is cumulative; message_start embeds a full message snapshot.
10
+ * - errors: {type:"error", error:{type,message}}; may arrive mid-stream after HTTP 200.
11
+ */
12
+ import { isTransientUpstreamStatus } from "../lib/upstream-retry";
13
+ import {
14
+ isTranslatorBudgetExceededError,
15
+ TRANSLATOR_MAX_TURN_BYTES,
16
+ TranslatorBudgetExceededError,
17
+ type TranslatorBudget,
18
+ } from "../lib/translator-budget";
19
+
20
+ type Rec = Record<string, unknown>;
21
+
22
+ function isRec(v: unknown): v is Rec {
23
+ return !!v && typeof v === "object" && !Array.isArray(v);
24
+ }
25
+
26
+ function uuid(): string {
27
+ return crypto.randomUUID().replace(/-/g, "");
28
+ }
29
+
30
+ /** HTTP status -> Anthropic error taxonomy (010 amendment #4; full official table per devlog 100). */
31
+ export function anthropicErrorType(status: number): string {
32
+ switch (status) {
33
+ case 400: return "invalid_request_error";
34
+ case 401: return "authentication_error";
35
+ case 402: return "billing_error";
36
+ case 403: return "permission_error";
37
+ case 404: return "not_found_error";
38
+ case 409: return "conflict_error";
39
+ case 413: return "request_too_large";
40
+ case 429: return "rate_limit_error";
41
+ case 504: return "timeout_error";
42
+ case 529: return "overloaded_error";
43
+ default: return status >= 500 ? "api_error" : "invalid_request_error";
44
+ }
45
+ }
46
+
47
+ export function anthropicErrorBody(status: number, message: string, type?: string, code?: string): Rec {
48
+ return { type: "error", error: { type: type ?? anthropicErrorType(status), message, ...(code ? { code } : {}) } };
49
+ }
50
+
51
+ export function anthropicErrorResponse(status: number, message: string, type?: string, code?: string): Response {
52
+ return new Response(JSON.stringify(anthropicErrorBody(status, message, type, code)), {
53
+ status,
54
+ headers: { "Content-Type": "application/json" },
55
+ });
56
+ }
57
+
58
+ /**
59
+ * Responses usage -> Anthropic usage. Responses `input_tokens` is INCLUSIVE of cache
60
+ * read+write (types.ts convention); Anthropic `input_tokens` excludes both, so
61
+ * subtract the full cache detail (devlog 070 — subtracting reads only inflated the
62
+ * non-cached input Claude Code displays by the write share).
63
+ */
64
+ export function anthropicUsage(usage: unknown, webSearchRequests = 0): Rec {
65
+ const u = isRec(usage) ? usage : {};
66
+ const details = isRec(u.input_tokens_details) ? u.input_tokens_details : {};
67
+ const cached = typeof details.cached_tokens === "number" ? details.cached_tokens : 0;
68
+ const cacheWrite = typeof details.cache_write_tokens === "number" ? details.cache_write_tokens : 0;
69
+ const input = typeof u.input_tokens === "number" ? u.input_tokens : 0;
70
+ const output = typeof u.output_tokens === "number" ? u.output_tokens : 0;
71
+ return {
72
+ input_tokens: Math.max(0, input - cached - cacheWrite),
73
+ output_tokens: output,
74
+ cache_read_input_tokens: cached,
75
+ cache_creation_input_tokens: cacheWrite,
76
+ // Only successful searches are billed/counted (Anthropic contract; Claude Code cost accounting).
77
+ ...(webSearchRequests > 0 ? { server_tool_use: { web_search_requests: webSearchRequests } } : {}),
78
+ };
79
+ }
80
+
81
+ function sseFrame(name: string, data: Rec): string {
82
+ return `event: ${name}\ndata: ${JSON.stringify(data)}\n\n`;
83
+ }
84
+
85
+ function appendedUtf8Bytes(previous: string, previousBytes: number, fragment: string): number {
86
+ let nextBytes = previousBytes + Buffer.byteLength(fragment);
87
+ const previousLast = previous.charCodeAt(previous.length - 1);
88
+ const fragmentFirst = fragment.charCodeAt(0);
89
+ if (previousLast >= 0xd800 && previousLast <= 0xdbff
90
+ && fragmentFirst >= 0xdc00 && fragmentFirst <= 0xdfff) {
91
+ nextBytes -= 2;
92
+ }
93
+ return nextBytes;
94
+ }
95
+
96
+ /**
97
+ * Claude Code / Anthropic WebSearch domain filters are optional and mutually exclusive.
98
+ * Empty arrays are rejected ("ambiguous"); both fields together are rejected. Routed models
99
+ * often emit both shapes — sanitize before Claude Code sees the tool_use / server_tool_use
100
+ * input (issue #381).
101
+ */
102
+ export function isClaudeWebSearchToolName(name: string): boolean {
103
+ const trimmed = name.trim();
104
+ return trimmed === "WebSearch" || /^web_search/i.test(trimmed);
105
+ }
106
+
107
+ function normalizeWebSearchDomainList(value: unknown): string[] | undefined {
108
+ if (!Array.isArray(value)) return undefined;
109
+ const domains = value
110
+ .filter((entry): entry is string => typeof entry === "string" && entry.trim().length > 0)
111
+ .map(entry => entry.trim());
112
+ return domains.length > 0 ? domains : undefined;
113
+ }
114
+
115
+ /** Strip empty domain filters; if both remain, keep `allowed_domains` and drop `blocked_domains`. */
116
+ export function sanitizeWebSearchInput(input: unknown): Rec {
117
+ const src: Rec = isRec(input) ? { ...input } : {};
118
+ const allowed = normalizeWebSearchDomainList(src.allowed_domains);
119
+ const blocked = normalizeWebSearchDomainList(src.blocked_domains);
120
+ delete src.allowed_domains;
121
+ delete src.blocked_domains;
122
+ if (allowed && blocked) {
123
+ // Prefer allow-list (restrict-to) when a routed model sets both non-empty fields.
124
+ src.allowed_domains = allowed;
125
+ } else if (allowed) {
126
+ src.allowed_domains = allowed;
127
+ } else if (blocked) {
128
+ src.blocked_domains = blocked;
129
+ }
130
+ return src;
131
+ }
132
+
133
+ /**
134
+ * Map a Responses `web_search_call` item to its Anthropic pair: the server_tool_use
135
+ * input (query/queries) and the web_search_tool_result content (hits, or the error
136
+ * object when the search failed). Shared by the SSE and JSON translation paths.
137
+ */
138
+ function webSearchPairFromItem(item: Rec): { id: string; input: Rec; resultContent: unknown; completed: boolean } {
139
+ const action = isRec(item.action) ? item.action : {};
140
+ const queries = Array.isArray(action.queries)
141
+ ? action.queries.filter((q): q is string => typeof q === "string" && q.length > 0)
142
+ : [];
143
+ const query = typeof action.query === "string" ? action.query : "";
144
+ const input = sanitizeWebSearchInput(
145
+ queries.length > 1 ? { queries } : { query: queries[0] ?? query },
146
+ );
147
+ const completed = item.status !== "failed";
148
+ let resultContent: unknown;
149
+ if (completed) {
150
+ const hits: Rec[] = [];
151
+ if (Array.isArray(item.sources)) {
152
+ for (const s of item.sources) {
153
+ if (isRec(s) && typeof s.url === "string" && s.url.length > 0) {
154
+ hits.push({ type: "web_search_result", title: typeof s.title === "string" ? s.title : "", url: s.url });
155
+ }
156
+ }
157
+ }
158
+ resultContent = hits;
159
+ } else {
160
+ resultContent = { type: "web_search_tool_result_error", error_code: "unavailable" };
161
+ }
162
+ const id = typeof item.id === "string" && item.id.length > 0 ? item.id : `srvtoolu_${uuid()}`;
163
+ return { id, input, resultContent, completed };
164
+ }
165
+
166
+ function messageSnapshot(model: string): Rec {
167
+ return {
168
+ id: `msg_${uuid()}`,
169
+ type: "message",
170
+ role: "assistant",
171
+ content: [],
172
+ model,
173
+ stop_reason: null,
174
+ stop_sequence: null,
175
+ usage: { input_tokens: 0, output_tokens: 0 },
176
+ };
177
+ }
178
+
179
+ interface OpenBlock {
180
+ kind: "text" | "thinking" | "tool_use";
181
+ index: number;
182
+ /** Responses item_id (tool calls) so output_item.done can match. */
183
+ itemId?: string;
184
+ /** Buffer WebSearch args and emit one sanitized input_json_delta on close (#381). */
185
+ bufferWebSearchArgs?: boolean;
186
+ argsBuf?: string;
187
+ argsBufBytes?: number;
188
+ webSearchArgsEmitted?: boolean;
189
+ callId?: string;
190
+ }
191
+
192
+ /** Streaming: Responses SSE bytes -> Anthropic Messages SSE bytes. */
193
+ export function responsesSseToAnthropicSse(
194
+ upstream: ReadableStream<Uint8Array>,
195
+ model: string,
196
+ opts: { pingIntervalMs?: number; translatorBudget: TranslatorBudget },
197
+ ): ReadableStream<Uint8Array> {
198
+ const translatorBudget = opts.translatorBudget;
199
+ const pingIntervalMs = opts?.pingIntervalMs ?? 20_000;
200
+ const decoder = new TextDecoder();
201
+ const encoder = new TextEncoder();
202
+ let buffer = "";
203
+ let bufferBytes = 0;
204
+ let started = false;
205
+ let terminated = false;
206
+ let cancelled = false;
207
+ let blockIndex = 0;
208
+ let open: OpenBlock | null = null;
209
+ let sawToolUse = false;
210
+ let webSearchRequests = 0;
211
+ let pingTimer: ReturnType<typeof setInterval> | undefined;
212
+ let reader: ReadableStreamDefaultReader<Uint8Array> | undefined;
213
+ const utf8SliceBytes = (value: string, start: number, end: number): number => {
214
+ let bytes = 0;
215
+ for (let index = start; index < end; index++) {
216
+ const codePoint = value.codePointAt(index)!;
217
+ if (codePoint <= 0x7f) bytes += 1;
218
+ else if (codePoint <= 0x7ff) bytes += 2;
219
+ else if (codePoint <= 0xffff) bytes += 3;
220
+ else { bytes += 4; index += 1; }
221
+ }
222
+ return bytes;
223
+ };
224
+ const queuedLiveFrameBytes: number[] = [];
225
+ const releaseDeliveredFrame = () => {
226
+ const bytes = queuedLiveFrameBytes.shift();
227
+ if (bytes !== undefined) translatorBudget.releaseRetained(bytes, { kind: "live_transient" });
228
+ };
229
+
230
+ return new ReadableStream<Uint8Array>({
231
+ start(controller) {
232
+ const emit = (name: string, data: Rec) => {
233
+ const frame = encoder.encode(sseFrame(name, data));
234
+ const reservation = translatorBudget.reserveTransient(frame.byteLength, { kind: "live_transient" });
235
+ controller.enqueue(frame);
236
+ reservation.commitRetained();
237
+ queuedLiveFrameBytes.push(frame.byteLength);
238
+ };
239
+ const ensureStarted = () => {
240
+ if (started) return;
241
+ started = true;
242
+ emit("message_start", { type: "message_start", message: messageSnapshot(model) });
243
+ emit("ping", { type: "ping" });
244
+ };
245
+ // Idle keepalive (devlog 100): real Anthropic streams may interleave pings anywhere;
246
+ // synthesizing one during upstream silence protects remote deployments behind
247
+ // LB/NAT idle timeouts and covers slow first tokens. Cheap and spec-legal.
248
+ if (pingIntervalMs > 0) {
249
+ pingTimer = setInterval(() => {
250
+ if (terminated) return;
251
+ try {
252
+ ensureStarted();
253
+ emit("ping", { type: "ping" });
254
+ } catch { /* controller torn down; the read loop is ending anyway */ }
255
+ }, pingIntervalMs);
256
+ }
257
+ const closeOpenBlock = () => {
258
+ if (!open) return;
259
+ if (open.kind === "tool_use" && open.bufferWebSearchArgs && !open.webSearchArgsEmitted) {
260
+ // Emit sanitized args even if output_item.done never supplied a full arguments field
261
+ // (finish/fail paths, or deltas-only streams).
262
+ let parsed: unknown = {};
263
+ const rawArgs = open.argsBuf ?? "";
264
+ try { parsed = rawArgs.length > 0 ? JSON.parse(rawArgs) : {}; } catch { parsed = {}; }
265
+ emit("content_block_delta", {
266
+ type: "content_block_delta",
267
+ index: open.index,
268
+ delta: { type: "input_json_delta", partial_json: JSON.stringify(sanitizeWebSearchInput(parsed)) },
269
+ });
270
+ open.webSearchArgsEmitted = true;
271
+ }
272
+ if (open.kind === "thinking") {
273
+ // Synthetic signature: Claude Code accepts it (003 E6); inbound drops replays anyway.
274
+ emit("content_block_delta", {
275
+ type: "content_block_delta", index: open.index,
276
+ delta: { type: "signature_delta", signature: `ocx${Date.now()}` },
277
+ });
278
+ }
279
+ emit("content_block_stop", { type: "content_block_stop", index: open.index });
280
+ if (open.callId) translatorBudget.closeCall(open.callId);
281
+ open = null;
282
+ };
283
+ const ensureBlock = (kind: "text" | "thinking") => {
284
+ ensureStarted();
285
+ if (open && open.kind === kind) return;
286
+ closeOpenBlock();
287
+ const index = blockIndex++;
288
+ const contentBlock: Rec = kind === "text"
289
+ ? { type: "text", text: "" }
290
+ : { type: "thinking", thinking: "", signature: "" };
291
+ emit("content_block_start", { type: "content_block_start", index, content_block: contentBlock });
292
+ open = { kind, index };
293
+ };
294
+ const finish = (stopReason: string, usage: unknown) => {
295
+ if (terminated) return;
296
+ terminated = true;
297
+ ensureStarted();
298
+ closeOpenBlock();
299
+ emit("message_delta", {
300
+ type: "message_delta",
301
+ delta: { stop_reason: stopReason, stop_sequence: null },
302
+ usage: anthropicUsage(usage, webSearchRequests),
303
+ });
304
+ emit("message_stop", { type: "message_stop" });
305
+ };
306
+ // upstreamDerived: transient upstream statuses become overloaded_error so the
307
+ // Anthropic-SDK client retries with backoff; proxy-internal exceptions stay
308
+ // api_error — a deterministic ocx bug must not be masked as retryable
309
+ // (devlog/_plan/260716_claudecode_hardening/020). On win32 mid-stream socket
310
+ // resets reach the reader catch (no failed-tail relay) and stay api_error —
311
+ // same as today, deliberate residual.
312
+ const fail = (status: number, message: string, upstreamDerived = false, code?: string) => {
313
+ if (terminated) return;
314
+ terminated = true;
315
+ if (code === "translation_buffer_limit") {
316
+ if (open?.callId) translatorBudget.closeCall(open.callId);
317
+ open = null;
318
+ // No normal close frames are valid after overflow. Emit exactly one bounded
319
+ // typed terminal without consulting the exhausted budget.
320
+ controller.enqueue(encoder.encode(sseFrame("error", anthropicErrorBody(
321
+ 413,
322
+ message,
323
+ "request_too_large",
324
+ "translation_buffer_limit",
325
+ ))));
326
+ return;
327
+ }
328
+ ensureStarted();
329
+ closeOpenBlock();
330
+ const type = upstreamDerived && isTransientUpstreamStatus(status) ? "overloaded_error" : undefined;
331
+ emit("error", anthropicErrorBody(status, message, type, code));
332
+ };
333
+
334
+ const handleFrame = (eventName: string, data: Rec) => {
335
+ switch (eventName) {
336
+ case "response.created":
337
+ ensureStarted();
338
+ break;
339
+ case "response.heartbeat":
340
+ ensureStarted();
341
+ emit("ping", { type: "ping" });
342
+ break;
343
+ case "response.output_text.delta": {
344
+ if (typeof data.delta !== "string" || data.delta.length === 0) break;
345
+ ensureBlock("text");
346
+ emit("content_block_delta", {
347
+ type: "content_block_delta", index: open!.index,
348
+ delta: { type: "text_delta", text: data.delta },
349
+ });
350
+ break;
351
+ }
352
+ case "response.reasoning_summary_text.delta":
353
+ case "response.reasoning_text.delta": {
354
+ if (typeof data.delta !== "string" || data.delta.length === 0) break;
355
+ ensureBlock("thinking");
356
+ emit("content_block_delta", {
357
+ type: "content_block_delta", index: open!.index,
358
+ delta: { type: "thinking_delta", thinking: data.delta },
359
+ });
360
+ break;
361
+ }
362
+ case "response.output_item.added": {
363
+ const item = isRec(data.item) ? data.item : null;
364
+ if (!item || item.type !== "function_call") break;
365
+ ensureStarted();
366
+ closeOpenBlock();
367
+ sawToolUse = true;
368
+ const index = blockIndex++;
369
+ const name = typeof item.name === "string" ? item.name : "";
370
+ const bufferWebSearchArgs = isClaudeWebSearchToolName(name);
371
+ const callId = typeof item.call_id === "string" ? item.call_id : `toolu_${uuid()}`;
372
+ emit("content_block_start", {
373
+ type: "content_block_start", index,
374
+ content_block: {
375
+ type: "tool_use",
376
+ id: callId,
377
+ name,
378
+ input: {},
379
+ },
380
+ });
381
+ translatorBudget.openCall(callId);
382
+ open = {
383
+ kind: "tool_use",
384
+ index,
385
+ callId,
386
+ itemId: typeof item.id === "string" ? item.id : undefined,
387
+ bufferWebSearchArgs,
388
+ argsBuf: "",
389
+ argsBufBytes: 0,
390
+ webSearchArgsEmitted: false,
391
+ };
392
+ break;
393
+ }
394
+ case "response.function_call_arguments.delta": {
395
+ if (typeof data.delta !== "string" || data.delta.length === 0) break;
396
+ if (!open || open.kind !== "tool_use") break;
397
+ if (open.bufferWebSearchArgs) {
398
+ const previous = open.argsBuf ?? "";
399
+ const previousBytes = open.argsBufBytes ?? 0;
400
+ const nextBytes = appendedUtf8Bytes(previous, previousBytes, data.delta);
401
+ const scope = {
402
+ kind: "tool_args",
403
+ ...(open.callId ? { callId: open.callId } : {}),
404
+ } as const;
405
+ const reservation = translatorBudget.reserveTransient(nextBytes, scope);
406
+ try {
407
+ open.argsBuf = previous + data.delta;
408
+ open.argsBufBytes = nextBytes;
409
+ reservation.commitRetained();
410
+ translatorBudget.releaseRetained(previousBytes, scope);
411
+ } catch (error) {
412
+ reservation.release();
413
+ throw error;
414
+ }
415
+ break;
416
+ }
417
+ emit("content_block_delta", {
418
+ type: "content_block_delta", index: open.index,
419
+ delta: { type: "input_json_delta", partial_json: data.delta },
420
+ });
421
+ break;
422
+ }
423
+ case "response.output_item.done": {
424
+ const item = isRec(data.item) ? data.item : null;
425
+ if (!item) break;
426
+ // Server-side web search (native passthrough or sidecar bridge): translate the
427
+ // finished call into the Anthropic pair Claude Code natively parses —
428
+ // server_tool_use (query via input_json_delta) + web_search_tool_result.
429
+ // Never marks sawToolUse (stop_reason stays end_turn unless a real tool ran).
430
+ if (item.type === "web_search_call") {
431
+ ensureStarted();
432
+ closeOpenBlock();
433
+ const pair = webSearchPairFromItem(item);
434
+ const toolIndex = blockIndex++;
435
+ emit("content_block_start", {
436
+ type: "content_block_start", index: toolIndex,
437
+ content_block: { type: "server_tool_use", id: pair.id, name: "web_search" },
438
+ });
439
+ emit("content_block_delta", {
440
+ type: "content_block_delta", index: toolIndex,
441
+ delta: { type: "input_json_delta", partial_json: JSON.stringify(pair.input) },
442
+ });
443
+ emit("content_block_stop", { type: "content_block_stop", index: toolIndex });
444
+ const resultIndex = blockIndex++;
445
+ emit("content_block_start", {
446
+ type: "content_block_start", index: resultIndex,
447
+ content_block: { type: "web_search_tool_result", tool_use_id: pair.id, content: pair.resultContent },
448
+ });
449
+ emit("content_block_stop", { type: "content_block_stop", index: resultIndex });
450
+ if (pair.completed) webSearchRequests++;
451
+ break;
452
+ }
453
+ if (!open) break;
454
+ // Close the matching open block (message/reasoning items close implicitly on
455
+ // the next block; function_call items must close here so tool input parses).
456
+ if (open.kind === "tool_use" && item.type === "function_call") {
457
+ if (open.bufferWebSearchArgs && !open.webSearchArgsEmitted) {
458
+ const rawArgs = typeof item.arguments === "string" && item.arguments.length > 0
459
+ ? item.arguments
460
+ : (open.argsBuf ?? "");
461
+ let parsed: unknown = {};
462
+ try { parsed = rawArgs.length > 0 ? JSON.parse(rawArgs) : {}; } catch { parsed = {}; }
463
+ emit("content_block_delta", {
464
+ type: "content_block_delta",
465
+ index: open.index,
466
+ delta: { type: "input_json_delta", partial_json: JSON.stringify(sanitizeWebSearchInput(parsed)) },
467
+ });
468
+ open.webSearchArgsEmitted = true;
469
+ }
470
+ closeOpenBlock();
471
+ }
472
+ else if (open.kind === "text" && item.type === "message") closeOpenBlock();
473
+ else if (open.kind === "thinking" && item.type === "reasoning") closeOpenBlock();
474
+ break;
475
+ }
476
+ case "response.completed": {
477
+ const response = isRec(data.response) ? data.response : {};
478
+ if (response.end_turn === false && !sawToolUse) {
479
+ fail(529, "upstream turn ended without a final answer", true);
480
+ break;
481
+ }
482
+ finish(sawToolUse ? "tool_use" : "end_turn", response.usage);
483
+ break;
484
+ }
485
+ case "response.incomplete": {
486
+ const response = isRec(data.response) ? data.response : {};
487
+ const details = isRec(response.incomplete_details) ? response.incomplete_details : {};
488
+ if (details.reason === "max_output_tokens") {
489
+ finish("max_tokens", response.usage);
490
+ } else if (details.reason === "content_filter") {
491
+ finish("refusal", response.usage);
492
+ } else {
493
+ const message = typeof details.message === "string" && details.message.trim()
494
+ ? details.message
495
+ : `upstream response was incomplete${typeof details.reason === "string" ? ` (${details.reason})` : ""}`;
496
+ fail(529, message, true);
497
+ }
498
+ break;
499
+ }
500
+ case "response.failed": {
501
+ const response = isRec(data.response) ? data.response : {};
502
+ const error = isRec(response.error) ? response.error : {};
503
+ const message = typeof error.message === "string" ? error.message : "upstream request failed";
504
+ const code = typeof error.code === "string" ? error.code : undefined;
505
+ if (code === "translation_buffer_limit") {
506
+ throw new TranslatorBudgetExceededError("live_transient", TRANSLATOR_MAX_TURN_BYTES);
507
+ }
508
+ const status = code === "translation_buffer_limit"
509
+ ? 413
510
+ : typeof error.status === "number" ? error.status : 500;
511
+ // status-absent response.failed (relaySseWithFailedTail synthetic tail) defaults
512
+ // to 500, which is in the transient set — the mid-stream reset shape maps to
513
+ // overloaded_error by design.
514
+ fail(
515
+ status,
516
+ message,
517
+ true,
518
+ );
519
+ break;
520
+ }
521
+ default:
522
+ break; // web_search_call / custom_tool_call / content_part frames: ignored v1
523
+ }
524
+ };
525
+
526
+ reader = upstream.getReader();
527
+ void (async () => {
528
+ try {
529
+ for (;;) {
530
+ const { done, value } = await reader.read();
531
+ if (done) break;
532
+ const decodedReservation = translatorBudget.reserveTransient(value.byteLength, { kind: "live_transient" });
533
+ let decodedCommitted = false;
534
+ try {
535
+ const fragment = decoder.decode(value, { stream: true });
536
+ decodedReservation.commitRetained();
537
+ decodedCommitted = true;
538
+ const nextBufferBytes = appendedUtf8Bytes(buffer, bufferBytes, fragment);
539
+ const appendReservation = translatorBudget.reserveTransient(nextBufferBytes, { kind: "live_transient" });
540
+ try {
541
+ buffer += fragment;
542
+ appendReservation.commitRetained();
543
+ translatorBudget.releaseRetained(bufferBytes, { kind: "live_transient" });
544
+ bufferBytes = nextBufferBytes;
545
+ } catch (error) {
546
+ appendReservation.release();
547
+ throw error;
548
+ }
549
+ let sep: number;
550
+ while ((sep = buffer.indexOf("\n\n")) !== -1) {
551
+ const rawFrameBytes = utf8SliceBytes(buffer, 0, sep);
552
+ const residualBytes = bufferBytes - rawFrameBytes - 2;
553
+ const rawReservation = translatorBudget.reserveTransient(rawFrameBytes, { kind: "live_transient" });
554
+ let residualReservation: ReturnType<TranslatorBudget["reserveTransient"]> | undefined;
555
+ try {
556
+ residualReservation = translatorBudget.reserveTransient(residualBytes, { kind: "live_transient" });
557
+ const rawFrame = buffer.slice(0, sep);
558
+ const residual = buffer.slice(sep + 2);
559
+ rawReservation.commitRetained();
560
+ residualReservation.commitRetained();
561
+ residualReservation = undefined;
562
+ buffer = residual;
563
+ translatorBudget.releaseRetained(bufferBytes, { kind: "live_transient" });
564
+ bufferBytes = residualBytes;
565
+ try {
566
+ let eventName = "";
567
+ let dataLine = "";
568
+ let dataLineBytes = 0;
569
+ try {
570
+ let lineStart = 0;
571
+ while (lineStart <= rawFrame.length) {
572
+ const newline = rawFrame.indexOf("\n", lineStart);
573
+ const lineEnd = newline === -1 ? rawFrame.length : newline;
574
+ if (rawFrame.startsWith("event: ", lineStart)) {
575
+ eventName = rawFrame.slice(lineStart + 7, lineEnd).trim();
576
+ } else if (rawFrame.startsWith("data: ", lineStart)) {
577
+ const fragmentStart = lineStart + 6;
578
+ const fragmentBytes = utf8SliceBytes(rawFrame, fragmentStart, lineEnd);
579
+ const fragmentReservation = translatorBudget.reserveTransient(fragmentBytes, { kind: "live_transient" });
580
+ let fragmentCommitted = false;
581
+ try {
582
+ const fragment = rawFrame.slice(fragmentStart, lineEnd);
583
+ fragmentReservation.commitRetained();
584
+ fragmentCommitted = true;
585
+ const nextDataLineBytes = appendedUtf8Bytes(dataLine, dataLineBytes, fragment);
586
+ const dataReservation = translatorBudget.reserveTransient(nextDataLineBytes, { kind: "live_transient" });
587
+ try {
588
+ dataLine += fragment;
589
+ dataReservation.commitRetained();
590
+ translatorBudget.releaseRetained(dataLineBytes, { kind: "live_transient" });
591
+ dataLineBytes = nextDataLineBytes;
592
+ } catch (error) {
593
+ dataReservation.release();
594
+ throw error;
595
+ }
596
+ } finally {
597
+ if (fragmentCommitted) {
598
+ translatorBudget.releaseRetained(fragmentBytes, { kind: "live_transient" });
599
+ } else fragmentReservation.release();
600
+ }
601
+ }
602
+ if (newline === -1) break;
603
+ lineStart = newline + 1;
604
+ }
605
+ if (!dataLine) continue;
606
+ let data: unknown;
607
+ try { data = JSON.parse(dataLine); } catch { continue; }
608
+ if (!isRec(data)) continue;
609
+ // Responses-compatible gateways may omit the optional SSE event field
610
+ // while retaining the event name in the JSON payload's required type.
611
+ const resolvedEventName = eventName || (typeof data.type === "string" ? data.type : "");
612
+ if (!resolvedEventName || terminated) continue;
613
+ handleFrame(resolvedEventName, data);
614
+ } finally {
615
+ translatorBudget.releaseRetained(dataLineBytes, { kind: "live_transient" });
616
+ }
617
+ } finally {
618
+ translatorBudget.releaseRetained(rawFrameBytes, { kind: "live_transient" });
619
+ }
620
+ } catch (error) {
621
+ rawReservation.release();
622
+ residualReservation?.release();
623
+ throw error;
624
+ }
625
+ }
626
+ } finally {
627
+ if (decodedCommitted) translatorBudget.releaseRetained(value.byteLength, { kind: "live_transient" });
628
+ else decodedReservation.release();
629
+ }
630
+ }
631
+ // EOF without a terminal frame is a TRUNCATION, not success (devlog 100:
632
+ // gateways that close such streams politely hand Claude Code an empty/partial
633
+ // turn with no retryable error — CLIProxyAPI#2189 failure pattern). Fail closed
634
+ // with a mid-stream Anthropic error event so the client can retry.
635
+ if (!cancelled) fail(502, "upstream stream ended before a terminal frame (truncated response)", true);
636
+ } catch (err) {
637
+ if (isTranslatorBudgetExceededError(err)) {
638
+ try { await reader.cancel(err); } catch { /* already closed */ }
639
+ fail(413, "upstream translation buffer exceeded the safe limit", false, "translation_buffer_limit");
640
+ } else fail(500, err instanceof Error ? err.message : String(err));
641
+ } finally {
642
+ translatorBudget.releaseRetained(bufferBytes, { kind: "live_transient" });
643
+ if (pingTimer !== undefined) clearInterval(pingTimer);
644
+ reader.releaseLock();
645
+ if (!cancelled) controller.close();
646
+ }
647
+ })();
648
+ },
649
+ pull() {
650
+ releaseDeliveredFrame();
651
+ },
652
+ cancel(reason) {
653
+ cancelled = true;
654
+ while (queuedLiveFrameBytes.length > 0) releaseDeliveredFrame();
655
+ if (open?.callId) translatorBudget.closeCall(open.callId);
656
+ if (pingTimer !== undefined) clearInterval(pingTimer);
657
+ return reader?.cancel(reason);
658
+ },
659
+ });
660
+ }
661
+
662
+ /** Non-streaming: /v1/responses JSON -> Anthropic message JSON. */
663
+ export function responsesJsonToAnthropicMessage(json: unknown, model: string): Rec {
664
+ const body = isRec(json) ? json : {};
665
+ const output = Array.isArray(body.output) ? body.output : [];
666
+ const content: Rec[] = [];
667
+ let sawToolUse = false;
668
+ let webSearchRequests = 0;
669
+
670
+ for (const raw of output) {
671
+ if (!isRec(raw)) continue;
672
+ switch (raw.type) {
673
+ case "message": {
674
+ if (!Array.isArray(raw.content)) break;
675
+ for (const part of raw.content) {
676
+ if (isRec(part) && part.type === "output_text" && typeof part.text === "string" && part.text.length > 0) {
677
+ content.push({ type: "text", text: part.text });
678
+ }
679
+ }
680
+ break;
681
+ }
682
+ case "reasoning": {
683
+ const parts: string[] = [];
684
+ if (Array.isArray(raw.summary)) {
685
+ for (const s of raw.summary) {
686
+ if (isRec(s) && typeof s.text === "string" && s.text.length > 0) parts.push(s.text);
687
+ }
688
+ }
689
+ if (Array.isArray(raw.content)) {
690
+ for (const s of raw.content) {
691
+ if (isRec(s) && typeof s.text === "string" && s.text.length > 0) parts.push(s.text);
692
+ }
693
+ }
694
+ if (parts.length > 0) {
695
+ content.push({ type: "thinking", thinking: parts.join("\n\n"), signature: `ocx${Date.now()}` });
696
+ }
697
+ break;
698
+ }
699
+ case "function_call": {
700
+ sawToolUse = true;
701
+ let input: unknown = {};
702
+ if (typeof raw.arguments === "string" && raw.arguments.length > 0) {
703
+ try { input = JSON.parse(raw.arguments); } catch { input = {}; }
704
+ }
705
+ const name = typeof raw.name === "string" ? raw.name : "";
706
+ content.push({
707
+ type: "tool_use",
708
+ id: typeof raw.call_id === "string" ? raw.call_id : `toolu_${uuid()}`,
709
+ name,
710
+ input: isClaudeWebSearchToolName(name) ? sanitizeWebSearchInput(input) : input,
711
+ });
712
+ break;
713
+ }
714
+ case "web_search_call": {
715
+ // Server-side search: emit the Anthropic pair. Does NOT set sawToolUse.
716
+ const pair = webSearchPairFromItem(raw);
717
+ content.push({ type: "server_tool_use", id: pair.id, name: "web_search", input: pair.input });
718
+ content.push({ type: "web_search_tool_result", tool_use_id: pair.id, content: pair.resultContent });
719
+ if (pair.completed) webSearchRequests++;
720
+ break;
721
+ }
722
+ default:
723
+ break;
724
+ }
725
+ }
726
+
727
+ const details = isRec(body.incomplete_details) ? body.incomplete_details : {};
728
+ if (body.status === "incomplete"
729
+ && details.reason !== "max_output_tokens"
730
+ && details.reason !== "content_filter") {
731
+ const message = typeof details.message === "string" && details.message.trim()
732
+ ? details.message
733
+ : `upstream response was incomplete${typeof details.reason === "string" ? ` (${details.reason})` : ""}`;
734
+ return anthropicErrorBody(529, message, "overloaded_error");
735
+ }
736
+ if (body.status === "completed" && body.end_turn === false && !sawToolUse) {
737
+ return anthropicErrorBody(529, "upstream turn ended without a final answer", "overloaded_error");
738
+ }
739
+ const stopReason = body.status === "incomplete" && details.reason === "max_output_tokens"
740
+ ? "max_tokens"
741
+ : body.status === "incomplete" && details.reason === "content_filter"
742
+ ? "refusal"
743
+ : sawToolUse ? "tool_use" : "end_turn";
744
+
745
+ return {
746
+ id: `msg_${uuid()}`,
747
+ type: "message",
748
+ role: "assistant",
749
+ content,
750
+ model,
751
+ stop_reason: stopReason,
752
+ stop_sequence: null,
753
+ usage: anthropicUsage(body.usage, webSearchRequests),
754
+ };
755
+ }
756
+
757
+ /**
758
+ * Fold an Anthropic SSE stream (our own emission vocabulary) into a message JSON.
759
+ * Used for non-streaming client requests: the internal replay always streams
760
+ * (routed adapters do not support non-stream turns), so the translated stream is
761
+ * aggregated here instead of translating a JSON body.
762
+ */
763
+ export async function collectAnthropicMessage(
764
+ stream: ReadableStream<Uint8Array>,
765
+ model: string,
766
+ translatorBudget: TranslatorBudget,
767
+ ): Promise<Rec> {
768
+ const decoder = new TextDecoder();
769
+ const reader = stream.getReader();
770
+ let buffer = "";
771
+ const content: Rec[] = [];
772
+ let openBlock: Rec | null = null;
773
+ let toolJson = "";
774
+ let stopReason: string | null = "end_turn";
775
+ let usage: Rec = anthropicUsage(undefined);
776
+ let error: Rec | null = null;
777
+ const replaceRetained = (previous: string, next: string, kind: "live_transient" | "retained_collectors") => {
778
+ const reservation = translatorBudget.reserveTransient(Buffer.byteLength(next), { kind });
779
+ reservation.commitRetained();
780
+ translatorBudget.releaseRetained(Buffer.byteLength(previous), { kind });
781
+ return next;
782
+ };
783
+
784
+ const closeBlock = () => {
785
+ if (!openBlock) return;
786
+ // server_tool_use streams its query via input_json_delta exactly like tool_use (audit F3).
787
+ if (openBlock.type === "tool_use" || openBlock.type === "server_tool_use") {
788
+ try { openBlock.input = toolJson.length > 0 ? JSON.parse(toolJson) : {}; } catch { openBlock.input = {}; }
789
+ }
790
+ translatorBudget.chargeRetained(Buffer.byteLength(JSON.stringify(openBlock)), { kind: "retained_collectors" });
791
+ content.push(openBlock);
792
+ openBlock = null;
793
+ toolJson = "";
794
+ };
795
+
796
+ const handle = (name: string, data: Rec) => {
797
+ switch (name) {
798
+ case "content_block_start":
799
+ closeBlock();
800
+ if (isRec(data.content_block)) openBlock = { ...data.content_block };
801
+ break;
802
+ case "content_block_delta": {
803
+ const delta = isRec(data.delta) ? data.delta : {};
804
+ if (!openBlock) break;
805
+ if (delta.type === "text_delta" && typeof delta.text === "string") {
806
+ const previous = typeof openBlock.text === "string" ? openBlock.text : "";
807
+ openBlock.text = replaceRetained(previous, previous + delta.text, "retained_collectors");
808
+ } else if (delta.type === "thinking_delta" && typeof delta.thinking === "string") {
809
+ const previous = typeof openBlock.thinking === "string" ? openBlock.thinking : "";
810
+ openBlock.thinking = replaceRetained(previous, previous + delta.thinking, "retained_collectors");
811
+ } else if (delta.type === "signature_delta" && typeof delta.signature === "string") {
812
+ openBlock.signature = delta.signature;
813
+ } else if (delta.type === "input_json_delta" && typeof delta.partial_json === "string") {
814
+ toolJson = replaceRetained(toolJson, toolJson + delta.partial_json, "retained_collectors");
815
+ }
816
+ break;
817
+ }
818
+ case "content_block_stop":
819
+ closeBlock();
820
+ break;
821
+ case "message_delta": {
822
+ const delta = isRec(data.delta) ? data.delta : {};
823
+ if (typeof delta.stop_reason === "string") stopReason = delta.stop_reason;
824
+ if (isRec(data.usage)) usage = data.usage;
825
+ break;
826
+ }
827
+ case "error":
828
+ error = data;
829
+ break;
830
+ default:
831
+ break;
832
+ }
833
+ };
834
+
835
+ try {
836
+ for (;;) {
837
+ const { done, value } = await reader.read();
838
+ if (done) break;
839
+ buffer = replaceRetained(buffer, buffer + decoder.decode(value, { stream: true }), "live_transient");
840
+ let sep: number;
841
+ while ((sep = buffer.indexOf("\n\n")) !== -1) {
842
+ const rawFrame = buffer.slice(0, sep);
843
+ buffer = replaceRetained(buffer, buffer.slice(sep + 2), "live_transient");
844
+ let eventName = "";
845
+ let dataLine = "";
846
+ for (const line of rawFrame.split("\n")) {
847
+ if (line.startsWith("event: ")) eventName = line.slice(7).trim();
848
+ else if (line.startsWith("data: ")) dataLine += line.slice(6);
849
+ }
850
+ if (!eventName || !dataLine) continue;
851
+ let data: unknown;
852
+ try { data = JSON.parse(dataLine); } catch { continue; }
853
+ if (isRec(data)) handle(eventName, data);
854
+ }
855
+ }
856
+ } finally {
857
+ reader.releaseLock();
858
+ }
859
+ closeBlock();
860
+
861
+ if (error) return error;
862
+ return {
863
+ id: `msg_${uuid()}`,
864
+ type: "message",
865
+ role: "assistant",
866
+ content,
867
+ model,
868
+ stop_reason: stopReason,
869
+ stop_sequence: null,
870
+ usage,
871
+ };
872
+ }