@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,914 @@
1
+ /**
2
+ * Anthropic Messages inbound (/v1/messages + /v1/messages/count_tokens) for Claude Code.
3
+ *
4
+ * Translate-and-replay (devlog/260711_claude_inbound/010): the Anthropic request is
5
+ * converted to a /v1/responses body and replayed through handleResponses on an
6
+ * internal Request, so routing/OAuth/account-pool/failover/sidecars are inherited
7
+ * unchanged. The Responses output (SSE or JSON) is converted back to Anthropic shape.
8
+ */
9
+ import { FORWARD_HEADERS } from "../adapters/openai-responses";
10
+ import { enforceAnthropicImageLimits } from "../adapters/anthropic-image-guard";
11
+ import { normalizeAnthropicImages } from "../adapters/anthropic-image-normalize";
12
+ import { AnthropicRequestError, anthropicToResponsesTranslation, extractOcxEffortDirective, extractOcxRouteDirective, resolveInboundModel, type ClaudeCacheKeySource } from "../claude/inbound";
13
+ import { resolveDesktop3pAlias } from "../claude/desktop-3p";
14
+ import { recordDesktopRequest } from "../claude/desktop-health";
15
+ import { stripOneMillionMarker } from "../claude/context-windows";
16
+ import { captureClaudeInbound } from "../claude/inbound-debug";
17
+ import { isTransientUpstreamStatus } from "../lib/upstream-retry";
18
+ import { resolveClientRetryAfter } from "../lib/retry-after";
19
+ import {
20
+ anthropicErrorBody,
21
+ anthropicErrorResponse,
22
+ collectAnthropicMessage,
23
+ responsesJsonToAnthropicMessage,
24
+ responsesSseToAnthropicSse,
25
+ } from "../claude/outbound";
26
+ import { clearableDeadline, idleDeadline } from "../lib/abort";
27
+ import { estimateTokens } from "../lib/token-estimate";
28
+ import { routeModel } from "../router";
29
+ import { resolveWireProtocolOverride } from "./adapter-resolve";
30
+ import type { OcxConfig } from "../types";
31
+ import { readJsonRequestBody } from "./request-decompress";
32
+ import { addFinalRequestLog, httpStatusForTerminalStatus, recordFirstOutput, type RequestLogContext, type RequestLogEntry } from "./request-log";
33
+ import { conversationIdFromClaudeMetadata } from "./request-log-conversation";
34
+ import { responseWithDeferredRequestLog } from "./relay";
35
+ import { handleResponses } from "./responses";
36
+ import type { AdmissionLease } from "../lib/admission";
37
+ import {
38
+ createTranslatorBudget,
39
+ finalizeTranslatorBudgetResponse,
40
+ isTranslatorBudgetExceededError,
41
+ type TranslatorBudget,
42
+ } from "../lib/translator-budget";
43
+
44
+ type Rec = Record<string, unknown>;
45
+
46
+ function isRec(v: unknown): v is Rec {
47
+ return !!v && typeof v === "object" && !Array.isArray(v);
48
+ }
49
+
50
+ /** Resolve Claude-only sidecar overrides without mutating the shared server config. */
51
+ export function buildClaudeReplayConfig(config: OcxConfig): OcxConfig {
52
+ return {
53
+ ...config,
54
+ webSearchSidecar: {
55
+ ...config.webSearchSidecar,
56
+ ...config.claudeCode?.webSearchSidecar,
57
+ },
58
+ visionSidecar: {
59
+ ...config.visionSidecar,
60
+ ...config.claudeCode?.visionSidecar,
61
+ },
62
+ };
63
+ }
64
+
65
+ function claudeInboundDisabled(config: OcxConfig): Response | null {
66
+ if (config.claudeCode?.enabled === false) {
67
+ return anthropicErrorResponse(403, "Claude inbound is disabled (GUI: Claude ON toggle / config.claudeCode.enabled)", "permission_error");
68
+ }
69
+ return null;
70
+ }
71
+
72
+ async function readAnthropicBody(req: Request, budget: TranslatorBudget): Promise<unknown> {
73
+ try {
74
+ return await readJsonRequestBody(req, budget);
75
+ } catch (err) {
76
+ if (isTranslatorBudgetExceededError(err)) throw err;
77
+ throw new AnthropicRequestError(err instanceof Error && err.message ? err.message : "Invalid JSON body");
78
+ }
79
+ }
80
+
81
+ // ── Native Anthropic passthrough (subscription OAuth pierce) ──────────────────────
82
+ // When Claude Code runs with ONLY ANTHROPIC_BASE_URL set (subscription mode — the
83
+ // connectors warning stays off), it sends its OWN claude.ai OAuth Bearer to us.
84
+ // Requests for genuine claude/anthropic models that no alias/modelMap claims are
85
+ // forwarded VERBATIM to api.anthropic.com with the caller's credential and all
86
+ // end-to-end headers, so betas/thinking signatures/billing identity stay native.
87
+ // (Evidence: teamclaude --no-mitm + Vercel gateway docs, devlog 003/060.)
88
+
89
+ const PASSTHROUGH_STRIP_HEADERS = new Set([
90
+ "connection", "keep-alive", "transfer-encoding", "upgrade", "te", "trailer",
91
+ "proxy-authenticate", "proxy-authorization", "host", "content-length",
92
+ "accept-encoding", "x-opencodex-api-key", "origin",
93
+ ]);
94
+
95
+ function hasAnthropicNativeCredential(req: Request): boolean {
96
+ const bearer = req.headers.get("authorization")?.replace(/^Bearer\s+/i, "").trim() ?? "";
97
+ const apiKey = req.headers.get("x-api-key")?.trim() ?? "";
98
+ return bearer.startsWith("sk-ant-") || apiKey.startsWith("sk-ant-");
99
+ }
100
+
101
+ function wantsNativePassthrough(req: Request, config: OcxConfig, model: unknown): model is string {
102
+ if (config.claudeCode?.nativePassthrough === false) return false;
103
+ if (typeof model !== "string" || !/^(claude|anthropic)/i.test(model)) return false;
104
+ if (!hasAnthropicNativeCredential(req)) return false;
105
+ // An alias or modelMap hit means the user asked for a ROUTED model: translate instead.
106
+ return resolveInboundModel(model, config.claudeCode) === model;
107
+ }
108
+
109
+ /** Format a 32-hex cache key as a uuid-shaped session id (version/variant nibbles forced). */
110
+ function uuidFromHex(hex32: string): string {
111
+ const h = (hex32 + "0".repeat(32)).slice(0, 32);
112
+ return `${h.slice(0, 8)}-${h.slice(8, 12)}-4${h.slice(13, 16)}-8${h.slice(17, 20)}-${h.slice(20, 32)}`;
113
+ }
114
+
115
+ function anthropicUsageToOcx(usage: Rec | undefined): { inputTokens: number; outputTokens: number; cachedInputTokens?: number; cacheReadInputTokens?: number; cacheCreationInputTokens?: number } | undefined {
116
+ if (!usage) return undefined;
117
+ const num = (v: unknown) => typeof v === "number" ? v : 0;
118
+ const hasCache = usage.cache_read_input_tokens !== undefined || usage.cache_creation_input_tokens !== undefined;
119
+ const read = num(usage.cache_read_input_tokens);
120
+ const write = num(usage.cache_creation_input_tokens);
121
+ // Anthropic input_tokens excludes cache read/write; normalize to the canonical
122
+ // inclusive convention (types.ts OcxUsage / devlog 070). cached = READS only.
123
+ return {
124
+ inputTokens: num(usage.input_tokens) + read + write,
125
+ outputTokens: num(usage.output_tokens),
126
+ ...(hasCache ? {
127
+ cachedInputTokens: read,
128
+ cacheReadInputTokens: read,
129
+ cacheCreationInputTokens: write,
130
+ } : {}),
131
+ };
132
+ }
133
+
134
+ /** Body-occupancy guard for the native passthrough (devlog 260716_passthrough_followups/010). */
135
+ export interface PassthroughBodyGuard {
136
+ /** Idle window in ms — raw upstream-byte inactivity while a read is pending. 0 disables. */
137
+ stallMs: number;
138
+ /** Cumulative body byte cap. 0 disables. */
139
+ maxBytes: number;
140
+ /** Client request signal for deterministic cancel classification. */
141
+ reqSignal?: AbortSignal;
142
+ }
143
+
144
+ type PassthroughCloseReason = "terminal" | "client_cancel" | "body_stall" | "body_overflow";
145
+
146
+ /**
147
+ * Tap an Anthropic-vocabulary SSE stream for the request log (usage + terminal),
148
+ * bounding body occupancy: idle (silence-only, timed ONLY while a reader.read() is
149
+ * pending so downstream backpressure never counts as upstream inactivity) and a
150
+ * cumulative byte cap. On stall/overflow it appends a protocol-compatible Anthropic
151
+ * `event: error` terminal frame after a blank-line boundary, closes, and cancels the
152
+ * upstream reader — never a total-wall-clock bound (slow-but-alive streams live).
153
+ * Exported for deterministic unit tests.
154
+ */
155
+ export function tapAnthropicSseForLog(
156
+ upstream: ReadableStream<Uint8Array>,
157
+ logCtx: RequestLogContext,
158
+ finalize: (status: number, meta: { closeReason: PassthroughCloseReason }) => void,
159
+ guard?: PassthroughBodyGuard,
160
+ ): ReadableStream<Uint8Array> {
161
+ const decoder = new TextDecoder();
162
+ const encoder = new TextEncoder();
163
+ let buffer = "";
164
+ let usageAcc: Rec = {};
165
+ const inspect = (chunk: Uint8Array) => {
166
+ buffer += decoder.decode(chunk, { stream: true });
167
+ let sep: number;
168
+ while ((sep = buffer.indexOf("\n\n")) !== -1) {
169
+ const frame = buffer.slice(0, sep);
170
+ buffer = buffer.slice(sep + 2);
171
+ const dataLine = frame.split("\n").filter(l => l.startsWith("data: ")).map(l => l.slice(6)).join("");
172
+ if (!dataLine) continue;
173
+ let data: unknown;
174
+ try { data = JSON.parse(dataLine); } catch { continue; }
175
+ if (!isRec(data)) continue;
176
+ if (data.type === "message_start" && isRec(data.message) && isRec(data.message.usage)) {
177
+ usageAcc = { ...usageAcc, ...data.message.usage };
178
+ } else if (data.type === "message_delta" && isRec(data.usage)) {
179
+ usageAcc = { ...usageAcc, ...data.usage };
180
+ }
181
+ }
182
+ };
183
+ const reader = upstream.getReader();
184
+ let settled = false;
185
+ let bodyBytes = 0;
186
+ let tapController: ReadableStreamDefaultController<Uint8Array> | undefined;
187
+
188
+ const recordUsage = () => {
189
+ logCtx.usage = anthropicUsageToOcx(Object.keys(usageAcc).length > 0 ? usageAcc : undefined);
190
+ };
191
+ const failBody = (closeReason: "body_stall" | "body_overflow", errType: string, message: string) => {
192
+ if (settled) return;
193
+ settled = true;
194
+ idle.cancel();
195
+ detachAbort();
196
+ recordUsage();
197
+ finalize(200, { closeReason });
198
+ const payload = JSON.stringify({ type: "error", error: { type: errType, message } });
199
+ try {
200
+ // Leading blank line terminates any partial SSE block so the frame parses cleanly
201
+ // (relaySseWithFailedTail policy, Anthropic wire shape).
202
+ tapController?.enqueue(encoder.encode(`\n\nevent: error\ndata: ${payload}\n\n`));
203
+ tapController?.close();
204
+ } catch { /* client already torn down */ }
205
+ reader.cancel(new DOMException(message, closeReason === "body_stall" ? "TimeoutError" : "QuotaExceededError")).catch(() => {});
206
+ };
207
+ const idle = idleDeadline(guard?.stallMs ?? 0, () => {
208
+ failBody(
209
+ "body_stall",
210
+ "timeout_error",
211
+ `anthropic passthrough body stalled: no upstream bytes for ${Math.round((guard?.stallMs ?? 0) / 1000)}s`,
212
+ );
213
+ });
214
+ // Deterministic client-cancel classification: Bun may surface a client abort as a
215
+ // reader.read() rejection OR a resolved done (src/lib/abort.ts cancelBodyOnAbort
216
+ // rationale), so the listener performs first-wins settlement itself instead of
217
+ // relying on which shape the read takes.
218
+ const onClientAbort = () => {
219
+ if (settled) return;
220
+ settled = true;
221
+ idle.cancel();
222
+ detachAbort();
223
+ finalize(499, { closeReason: "client_cancel" });
224
+ try { tapController?.close(); } catch { /* downstream already torn down */ }
225
+ reader.cancel(guard?.reqSignal?.reason).catch(() => {});
226
+ };
227
+ const detachAbort = (() => {
228
+ const signal = guard?.reqSignal;
229
+ if (!signal) return () => {};
230
+ if (signal.aborted) {
231
+ queueMicrotask(onClientAbort);
232
+ return () => {};
233
+ }
234
+ signal.addEventListener("abort", onClientAbort, { once: true });
235
+ return () => signal.removeEventListener("abort", onClientAbort);
236
+ })();
237
+
238
+ return new ReadableStream<Uint8Array>({
239
+ start(controller) {
240
+ tapController = controller;
241
+ },
242
+ async pull(controller) {
243
+ if (settled) return;
244
+ try {
245
+ idle.reset();
246
+ const { done, value } = await reader.read();
247
+ idle.pause();
248
+ if (settled) return; // stall/overflow/abort won the race while we awaited
249
+ if (done) {
250
+ settled = true;
251
+ idle.cancel();
252
+ detachAbort();
253
+ recordUsage();
254
+ finalize(200, { closeReason: "terminal" });
255
+ controller.close();
256
+ return;
257
+ }
258
+ if (value.byteLength > 0) {
259
+ bodyBytes += value.byteLength;
260
+ if (guard && guard.maxBytes > 0 && bodyBytes > guard.maxBytes) {
261
+ failBody(
262
+ "body_overflow",
263
+ "api_error",
264
+ `anthropic passthrough body exceeded ${guard.maxBytes} bytes`,
265
+ );
266
+ return;
267
+ }
268
+ }
269
+ inspect(value);
270
+ controller.enqueue(value);
271
+ } catch (err) {
272
+ if (settled) return;
273
+ settled = true;
274
+ idle.cancel();
275
+ detachAbort();
276
+ recordUsage();
277
+ finalize(200, { closeReason: "terminal" });
278
+ try { controller.error(err); } catch { /* torn down */ }
279
+ }
280
+ },
281
+ cancel(reason) {
282
+ if (!settled) {
283
+ settled = true;
284
+ idle.cancel();
285
+ detachAbort();
286
+ finalize(499, { closeReason: "client_cancel" });
287
+ }
288
+ reader.cancel(reason).catch(() => {});
289
+ },
290
+ });
291
+ }
292
+
293
+ async function anthropicNativePassthrough(
294
+ req: Request,
295
+ config: OcxConfig,
296
+ logCtx: RequestLogContext,
297
+ logIds: { requestId: string; start: number } | undefined,
298
+ body: Rec,
299
+ pathname: string,
300
+ ): Promise<Response> {
301
+ const model = typeof body.model === "string" ? body.model : "unknown";
302
+ logCtx.model = model;
303
+ logCtx.provider = "anthropic-native";
304
+ logCtx.requestedModel = model;
305
+ let logged = false;
306
+ const finalize = (status: number, meta: { closeReason: PassthroughCloseReason | "non_stream" }) => {
307
+ if (!logIds || logged) return;
308
+ logged = true;
309
+ addFinalRequestLog(logIds.requestId, logIds.start, logCtx, status, meta);
310
+ };
311
+
312
+ const base = (config.claudeCode?.anthropicBaseUrl ?? "https://api.anthropic.com").replace(/\/$/, "");
313
+ const search = new URL(req.url).search;
314
+ // Native passthrough bypasses the anthropic adapter, so the generous image pipeline
315
+ // (devlog/260714_image_normalization_pipeline/040) must run here: tier-normalize then
316
+ // guard the already-Anthropic-wire messages before serialization. Applies to
317
+ // count_tokens too — counts must match what the real send will contain, and the 32MB
318
+ // body cap applies to it equally. Non-message bodies pass through untouched.
319
+ if (Array.isArray(body.messages)) {
320
+ await normalizeAnthropicImages(body.messages);
321
+ enforceAnthropicImageLimits(body.messages);
322
+ }
323
+ const headers = new Headers();
324
+ req.headers.forEach((value, name) => {
325
+ if (!PASSTHROUGH_STRIP_HEADERS.has(name.toLowerCase())) headers.set(name, value);
326
+ });
327
+ headers.set("content-type", "application/json");
328
+
329
+ const result = await fetchWithHeaderDeadline(
330
+ `${base}${pathname}${search}`,
331
+ { method: "POST", headers, body: JSON.stringify(body) },
332
+ config.connectTimeoutMs ?? 200_000,
333
+ req.signal,
334
+ );
335
+ if (result.kind === "timeout") {
336
+ finalize(504, { closeReason: "non_stream" });
337
+ return anthropicErrorResponse(504, "anthropic passthrough timed out waiting for response headers", "timeout_error");
338
+ }
339
+ if (result.kind === "error") {
340
+ const err = result.error;
341
+ finalize(502, { closeReason: "non_stream" });
342
+ return anthropicErrorResponse(502, `anthropic passthrough failed: ${err instanceof Error ? err.message : String(err)}`, "api_error");
343
+ }
344
+ const upstream = result.upstream;
345
+
346
+ const contentType = upstream.headers.get("content-type") ?? "application/json";
347
+ const bodyGuard = resolvePassthroughBodyGuard(config, req.signal);
348
+ if (upstream.ok && contentType.includes("text/event-stream") && upstream.body) {
349
+ return new Response(tapAnthropicSseForLog(upstream.body, logCtx, finalize, bodyGuard), {
350
+ status: upstream.status,
351
+ headers: {
352
+ "Content-Type": contentType,
353
+ "Cache-Control": "no-cache",
354
+ "Connection": "keep-alive",
355
+ },
356
+ });
357
+ }
358
+ // Non-stream (count_tokens, errors, stream:false): relay verbatim under the same
359
+ // idle/size bounds — headers are NOT yet sent here, so real statuses are available.
360
+ const bodyResult = await readBoundedPassthroughBody(upstream, bodyGuard);
361
+ if (bodyResult.kind === "client_cancel") {
362
+ finalize(499, { closeReason: "client_cancel" });
363
+ return anthropicErrorResponse(499, "client closed request during anthropic passthrough", "api_error");
364
+ }
365
+ if (bodyResult.kind === "stall") {
366
+ finalize(504, { closeReason: "body_stall" });
367
+ return anthropicErrorResponse(504, `anthropic passthrough body stalled: no upstream bytes for ${Math.round(bodyGuard.stallMs / 1000)}s`, "timeout_error");
368
+ }
369
+ if (bodyResult.kind === "overflow") {
370
+ finalize(502, { closeReason: "body_overflow" });
371
+ return anthropicErrorResponse(502, `anthropic passthrough body exceeded ${bodyGuard.maxBytes} bytes`, "api_error");
372
+ }
373
+ const text = bodyResult.text;
374
+ if (upstream.ok) {
375
+ try {
376
+ const parsed = JSON.parse(text) as { usage?: Rec };
377
+ if (isRec(parsed?.usage)) logCtx.usage = anthropicUsageToOcx(parsed.usage);
378
+ } catch { /* count_tokens etc. */ }
379
+ }
380
+ finalize(upstream.status, { closeReason: "non_stream" });
381
+ const retryAfter = upstream.headers.get("retry-after");
382
+ return new Response(text, {
383
+ status: upstream.status,
384
+ headers: { "Content-Type": contentType, ...(retryAfter ? { "Retry-After": retryAfter } : {}) },
385
+ });
386
+ }
387
+
388
+ const DEFAULT_BODY_STALL_SEC = 90;
389
+ const DEFAULT_BODY_MAX_BYTES = 64 * 1024 * 1024;
390
+
391
+ /**
392
+ * Normalize the claudeCode body-guard config (devlog 260716_passthrough_followups/010).
393
+ * Policy: exactly 0 disables; finite positive values are honored (stall clamped to
394
+ * min 1s); negative/non-finite/absent values fall back to the defaults.
395
+ */
396
+ export function resolvePassthroughBodyGuard(config: OcxConfig, reqSignal?: AbortSignal): PassthroughBodyGuard {
397
+ const rawSec = config.claudeCode?.bodyStallSec;
398
+ const stallSec = rawSec === 0
399
+ ? 0
400
+ : typeof rawSec === "number" && Number.isFinite(rawSec) && rawSec > 0
401
+ ? Math.max(1, rawSec)
402
+ : DEFAULT_BODY_STALL_SEC;
403
+ const rawBytes = config.claudeCode?.bodyMaxBytes;
404
+ const maxBytes = rawBytes === 0
405
+ ? 0
406
+ : typeof rawBytes === "number" && Number.isFinite(rawBytes) && rawBytes > 0
407
+ ? Math.floor(rawBytes)
408
+ : DEFAULT_BODY_MAX_BYTES;
409
+ return { stallMs: stallSec * 1000, maxBytes, ...(reqSignal ? { reqSignal } : {}) };
410
+ }
411
+
412
+ type BoundedPassthroughBody =
413
+ | { kind: "ok"; text: string }
414
+ | { kind: "stall" }
415
+ | { kind: "overflow" }
416
+ | { kind: "client_cancel" };
417
+
418
+ /**
419
+ * Bounded replacement for `await upstream.text()` on the non-stream passthrough
420
+ * branch: same idle-only + size-cap semantics as the SSE tap. NOTE: reader.cancel()
421
+ * resolves a pending read as done rather than rejecting, so the stalled flag is
422
+ * re-checked after every read settlement (audit round 3).
423
+ */
424
+ export async function readBoundedPassthroughBody(
425
+ upstream: Response,
426
+ guard: PassthroughBodyGuard,
427
+ ): Promise<BoundedPassthroughBody> {
428
+ if (!upstream.body) return { kind: "ok", text: await upstream.text() };
429
+ const reader = upstream.body.getReader();
430
+ const decoder = new TextDecoder();
431
+ let text = "";
432
+ let bytes = 0;
433
+ let stalled = false;
434
+ let aborted = false;
435
+ const idle = idleDeadline(guard.stallMs, () => {
436
+ stalled = true;
437
+ reader.cancel(new DOMException("anthropic passthrough body stalled", "TimeoutError")).catch(() => {});
438
+ });
439
+ // Deterministic client-abort classification (audit round 4): Bun may surface the
440
+ // abort as a read rejection OR a resolved done, so we cancel the reader ourselves
441
+ // and classify via the flag rather than the read's settlement shape.
442
+ const signal = guard.reqSignal;
443
+ const onAbort = () => {
444
+ aborted = true;
445
+ reader.cancel(signal?.reason).catch(() => {});
446
+ };
447
+ if (signal?.aborted) onAbort();
448
+ else signal?.addEventListener("abort", onAbort, { once: true });
449
+ try {
450
+ while (true) {
451
+ idle.reset();
452
+ let result: Awaited<ReturnType<typeof reader.read>>;
453
+ try {
454
+ result = await reader.read();
455
+ } catch (err) {
456
+ if (aborted) return { kind: "client_cancel" };
457
+ if (stalled) return { kind: "stall" };
458
+ throw err;
459
+ } finally {
460
+ idle.pause();
461
+ }
462
+ if (aborted) return { kind: "client_cancel" };
463
+ if (stalled) return { kind: "stall" };
464
+ if (result.done) break;
465
+ if (result.value.byteLength === 0) continue;
466
+ bytes += result.value.byteLength;
467
+ if (guard.maxBytes > 0 && bytes > guard.maxBytes) {
468
+ reader.cancel(new DOMException("anthropic passthrough body exceeded byte cap", "QuotaExceededError")).catch(() => {});
469
+ return { kind: "overflow" };
470
+ }
471
+ text += decoder.decode(result.value, { stream: true });
472
+ }
473
+ text += decoder.decode();
474
+ return { kind: "ok", text };
475
+ } finally {
476
+ idle.cancel();
477
+ signal?.removeEventListener("abort", onAbort);
478
+ }
479
+ }
480
+
481
+ /**
482
+ * Header-phase fetch guarded by a clearable deadline (PR #136 follow-up hardening).
483
+ *
484
+ * The deadline covers ONLY the wait for response headers; once `fetch` settles —
485
+ * fulfilled OR rejected — the timer must die. The `finally` block guarantees
486
+ * `clear()` on every path (success, upstream reject, deadline expiry), fixing the
487
+ * timer leak where a rejected fetch left the deadline running until expiry.
488
+ * `didExpire()` stays truthful after `clear()` (see src/lib/abort.ts), so timeout
489
+ * classification inside the catch is unaffected by the finally cleanup.
490
+ *
491
+ * `makeDeadline`/`fetchImpl` are injectable for deterministic unit tests.
492
+ */
493
+ export type HeaderDeadlineFetchResult =
494
+ | { kind: "response"; upstream: Response }
495
+ | { kind: "timeout" }
496
+ | { kind: "error"; error: unknown };
497
+
498
+ export async function fetchWithHeaderDeadline(
499
+ input: string | URL,
500
+ init: RequestInit,
501
+ timeoutMs: number,
502
+ parent?: AbortSignal,
503
+ makeDeadline: typeof clearableDeadline = clearableDeadline,
504
+ fetchImpl: typeof fetch = fetch,
505
+ ): Promise<HeaderDeadlineFetchResult> {
506
+ const deadline = makeDeadline(timeoutMs, parent);
507
+ try {
508
+ const upstream = await fetchImpl(input, { ...init, signal: deadline.signal });
509
+ return { kind: "response", upstream };
510
+ } catch (error) {
511
+ if (deadline.didExpire()) return { kind: "timeout" };
512
+ return { kind: "error", error };
513
+ } finally {
514
+ deadline.clear();
515
+ }
516
+ }
517
+
518
+ export async function handleClaudeMessages(
519
+ req: Request,
520
+ config: OcxConfig,
521
+ logCtx: RequestLogContext,
522
+ logIds?: { requestId: string; start: number; turnAdmissionLease?: AdmissionLease },
523
+ ): Promise<Response> {
524
+ const translatorBudget = createTranslatorBudget();
525
+ try {
526
+ return finalizeTranslatorBudgetResponse(
527
+ await handleClaudeMessagesWithBudget(req, config, logCtx, translatorBudget, logIds),
528
+ translatorBudget,
529
+ );
530
+ } catch (error) {
531
+ translatorBudget.dispose();
532
+ throw error;
533
+ }
534
+ }
535
+
536
+ async function handleClaudeMessagesWithBudget(
537
+ req: Request,
538
+ config: OcxConfig,
539
+ logCtx: RequestLogContext,
540
+ translatorBudget: TranslatorBudget,
541
+ logIds?: { requestId: string; start: number; turnAdmissionLease?: AdmissionLease },
542
+ ): Promise<Response> {
543
+ logCtx.surface = "claude";
544
+ const disabled = claudeInboundDisabled(config);
545
+ if (disabled) {
546
+ if (logIds) addFinalRequestLog(logIds.requestId, logIds.start, logCtx, 403, { closeReason: "non_stream" });
547
+ return disabled;
548
+ }
549
+
550
+ let anthropicBody: unknown;
551
+ let internalBody: Rec;
552
+ let cacheKeySource: ClaudeCacheKeySource = null;
553
+ let effortOverride: ReturnType<typeof extractOcxEffortDirective> = null;
554
+ try {
555
+ anthropicBody = await readAnthropicBody(req, translatorBudget);
556
+ // Defensive [1m] strip (devlog 138): clients normally remove the context-variant
557
+ // marker themselves; the 1M signal we act on is the anthropic-beta header.
558
+ // Case-insensitive — the CLI matches /\[1m\]/i (audit 021 #7).
559
+ if (isRec(anthropicBody) && typeof anthropicBody.model === "string") {
560
+ anthropicBody.model = stripOneMillionMarker(anthropicBody.model);
561
+ }
562
+ // ocx-route override (devlog 072): injected agent bodies pin their model via a
563
+ // system-prompt directive because 2.1.207 ignores custom ids in agent
564
+ // frontmatter. Must run BEFORE the native-passthrough branch — the CLI sends
565
+ // these subagent turns under a fallback claude model id.
566
+ if (isRec(anthropicBody)) {
567
+ const routeOverride = extractOcxRouteDirective(anthropicBody);
568
+ if (routeOverride && typeof anthropicBody.model === "string") {
569
+ anthropicBody.model = stripOneMillionMarker(routeOverride);
570
+ effortOverride = extractOcxEffortDirective(anthropicBody);
571
+ }
572
+ }
573
+ // Debug capture (opt-in allowlist scalars) BEFORE the passthrough branch so
574
+ // native, routed, and disabled-alias paths are all observable (devlog 130 B1).
575
+ captureClaudeInbound(
576
+ "messages",
577
+ anthropicBody,
578
+ isRec(anthropicBody) && typeof anthropicBody.model === "string"
579
+ ? resolveInboundModel(anthropicBody.model, config.claudeCode)
580
+ : undefined,
581
+ req.headers.get("anthropic-beta") ?? undefined,
582
+ );
583
+ // Client surface discrimination: Desktop 3P aliases resolve through the
584
+ // desktop registry; Code uses readable aliases or direct model names.
585
+ if (isRec(anthropicBody) && typeof anthropicBody.model === "string" && resolveDesktop3pAlias(anthropicBody.model)) {
586
+ logCtx.surface = "claude-desktop";
587
+ recordDesktopRequest();
588
+ }
589
+ // Correlate before native passthrough so Anthropic-credential turns still filter/total (#330 / #522).
590
+ if (isRec(anthropicBody)) {
591
+ const claudeConversationId = conversationIdFromClaudeMetadata(
592
+ isRec(anthropicBody.metadata) ? anthropicBody.metadata : undefined,
593
+ );
594
+ if (claudeConversationId) logCtx.conversationId = claudeConversationId;
595
+ }
596
+ if (isRec(anthropicBody) && wantsNativePassthrough(req, config, anthropicBody.model)) {
597
+ return await anthropicNativePassthrough(req, config, logCtx, logIds, anthropicBody, "/v1/messages");
598
+ }
599
+ if (isRec(anthropicBody) && effortOverride) {
600
+ anthropicBody.output_config = { effort: effortOverride };
601
+ delete anthropicBody.thinking;
602
+ }
603
+ const translation = anthropicToResponsesTranslation(anthropicBody, config.claudeCode);
604
+ internalBody = translation.body;
605
+ translatorBudget.chargeRetained(new TextEncoder().encode(JSON.stringify(internalBody)).byteLength, { kind: "request_copies" });
606
+ cacheKeySource = translation.cacheKeySource;
607
+ } catch (err) {
608
+ const overflow = isTranslatorBudgetExceededError(err);
609
+ const status = overflow ? 413 : err instanceof AnthropicRequestError ? 400 : 500;
610
+ if (logIds) addFinalRequestLog(logIds.requestId, logIds.start, logCtx, status, { closeReason: "non_stream" });
611
+ return anthropicErrorResponse(
612
+ status,
613
+ overflow ? "request translation buffer exceeded the safe limit" : err instanceof Error ? err.message : String(err),
614
+ overflow ? "request_too_large" : undefined,
615
+ overflow ? "translation_buffer_limit" : undefined,
616
+ );
617
+ }
618
+
619
+ const requestedModel = (anthropicBody as Rec).model as string;
620
+ const stream = internalBody.stream === true;
621
+ // Routed adapters only support streamed turns; always stream internally and fold
622
+ // the translated Anthropic SSE into a message JSON for non-streaming clients.
623
+ internalBody.stream = true;
624
+
625
+ // Native ChatGPT passthrough (openai-responses forward) accepts only Codex-shaped
626
+ // bodies: it 400s on sampling params ("Unsupported parameter: max_output_tokens",
627
+ // verified live 2026-07-11). Strip them for that route; routed providers keep them.
628
+ let nativeRoute = false;
629
+ try {
630
+ const route = routeModel(config, internalBody.model as string);
631
+ // Settle the wire once so the sampling decision below reads the effective
632
+ // adapter rather than the provider-wide default (#404).
633
+ route.provider = resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, "anthropic");
634
+ if (route.provider.adapter === "openai-responses") {
635
+ nativeRoute = true;
636
+ delete internalBody.max_output_tokens;
637
+ delete internalBody.temperature;
638
+ delete internalBody.top_p;
639
+ delete internalBody.stop;
640
+ delete internalBody.user;
641
+ }
642
+ // Estimated-usage adapters (cursor/kiro) report no per-turn input tokens; stash a
643
+ // request-side estimate so the log's in:0 rows get a floor. NEVER set this for
644
+ // accurate-usage adapters — the request-log merge is max(reported, estimate) and
645
+ // would overwrite real usage (audit 133 R1#7).
646
+ if (route.provider.adapter === "cursor" || route.provider.adapter === "kiro") {
647
+ const raw = anthropicBody as Rec;
648
+ const parts: string[] = [];
649
+ if (raw.system !== undefined) parts.push(typeof raw.system === "string" ? raw.system : JSON.stringify(raw.system));
650
+ if (raw.messages !== undefined) parts.push(JSON.stringify(raw.messages));
651
+ if (raw.tools !== undefined) parts.push(JSON.stringify(raw.tools));
652
+ logCtx.usageLogInputTokens = Math.max(1, estimateTokens(parts.join("\n"), requestedModel));
653
+ }
654
+ // Effort safety valve (devlog 136 B6, audit 139 R2#2): opus-shaped aliases make
655
+ // every routed model look like a reasoning model to Claude clients, so a forced
656
+ // effort (CLAUDE_CODE_ALWAYS_ENABLE_EFFORT) would leak reasoning params to routes
657
+ // that affirmatively expose NO effort control. Strip only on a definitive [] from
658
+ // supportedLadderFor; unknown (undefined) passes through untouched.
659
+ if (internalBody.reasoning !== undefined) {
660
+ const { supportedLadderFor } = await import("./effort-policy");
661
+ const ladder = supportedLadderFor({ provider: route.provider, modelId: route.modelId });
662
+ if (ladder !== undefined && ladder.length === 0) delete internalBody.reasoning;
663
+ }
664
+ } catch { /* unknown model: let handleResponses shape the 404 */ }
665
+
666
+ const headers = new Headers({ "content-type": "application/json" });
667
+ for (const name of FORWARD_HEADERS) {
668
+ // The caller's bearer is the proxy admission token (ocx claude placeholder), never a
669
+ // ChatGPT credential — forwarding it upstream turns into {"detail":"Unauthorized"}.
670
+ if (name === "authorization") continue;
671
+ const value = req.headers.get(name);
672
+ if (value) headers.set(name, value);
673
+ }
674
+ if (!nativeRoute) {
675
+ // Routed replays need main ChatGPT auth so OpenAI-backed sidecars remain reachable.
676
+ const { getMainAccountToken } = await import("../codex/main-account");
677
+ const token = getMainAccountToken();
678
+ if (token) {
679
+ headers.set("authorization", `Bearer ${token.accessToken}`);
680
+ headers.set("chatgpt-account-id", token.chatgptAccountId);
681
+ }
682
+ }
683
+ if (nativeRoute) {
684
+ // No forwarded ChatGPT auth exists on this surface. Attach the main codex login
685
+ // (read-only auth.json token); account-pool rotation still overrides downstream.
686
+ const { getMainAccountToken } = await import("../codex/main-account");
687
+ const token = getMainAccountToken();
688
+ if (token) {
689
+ headers.set("authorization", `Bearer ${token.accessToken}`);
690
+ headers.set("chatgpt-account-id", token.chatgptAccountId);
691
+ }
692
+ // ChatGPT-backend prompt-cache affinity rides the session_id HEADER (codex
693
+ // clients always send their session uuid; devlog 090 follow-up: body-level
694
+ // prompt_cache_key alone still yielded cached_tokens:0). Claude Code never sends
695
+ // the header, so synthesize a stable per-session uuid from the same cache key —
696
+ // but ONLY for a real per-session key (metadata.user_id). The system-hash fallback
697
+ // key is shared across Desktop conversations, and a shared session_id's backend
698
+ // semantics are unproven (audit 133 R2#3): body prompt_cache_key only there.
699
+ if (cacheKeySource === "metadata" && !headers.has("session_id") && typeof internalBody.prompt_cache_key === "string") {
700
+ headers.set("session_id", uuidFromHex(internalBody.prompt_cache_key));
701
+ }
702
+ }
703
+ const internalBodyJson = JSON.stringify(internalBody);
704
+ translatorBudget.chargeRetained(new TextEncoder().encode(internalBodyJson).byteLength, { kind: "request_copies" });
705
+ const internalReq = new Request("http://localhost/v1/responses", {
706
+ method: "POST",
707
+ headers,
708
+ body: internalBodyJson,
709
+ });
710
+
711
+ // Request-log wiring mirrors the /v1/responses route: native passthrough finalizes
712
+ // via the terminal callbacks; routed streams get the Responses-vocabulary log tap
713
+ // BEFORE translation (the translated Anthropic stream has no response.completed
714
+ // frame, so tapping it records a bogus 502 with no usage/cache detail).
715
+ let nativeLogged = false;
716
+ const finalizeNativeLog = (status: number, meta: { terminalStatus?: RequestLogEntry["terminalStatus"]; closeReason: "terminal" | "client_cancel" }) => {
717
+ if (!logIds || nativeLogged) return;
718
+ nativeLogged = true;
719
+ addFinalRequestLog(logIds.requestId, logIds.start, logCtx, status, meta);
720
+ };
721
+ const upstream = await handleResponses(internalReq, buildClaudeReplayConfig(config), logCtx, {
722
+ ...(logIds?.turnAdmissionLease ? { turnAdmissionLease: logIds.turnAdmissionLease } : {}),
723
+ abortSignal: req.signal,
724
+ promptCacheKeyIsSharedCohort: cacheKeySource === "system",
725
+ // The body is Responses-shaped by now, but the client spoke Anthropic Messages.
726
+ // Without this the replay would look native and a Responses-scoped wire default
727
+ // would fire, disagreeing with the pre-flight decision above.
728
+ inboundWire: "anthropic",
729
+ translatorBudget,
730
+ ...(logIds ? { onFirstOutput: () => recordFirstOutput(logCtx, logIds.start) } : {}),
731
+ onNativePassthroughTerminal: status => finalizeNativeLog(httpStatusForTerminalStatus(status), { terminalStatus: status, closeReason: "terminal" }),
732
+ onNativePassthroughCancel: () => finalizeNativeLog(499, { closeReason: "client_cancel" }),
733
+ });
734
+ const response = logIds ? responseWithDeferredRequestLog(upstream, logIds.requestId, logIds.start, logCtx) : upstream;
735
+
736
+ if (!response.ok) {
737
+ // Re-shape the OpenAI-style error envelope into the Anthropic one, preserving status.
738
+ let message = `upstream error (${response.status})`;
739
+ try {
740
+ const text = await response.text();
741
+ try {
742
+ const parsed = JSON.parse(text) as { error?: { message?: string; type?: string } | string; message?: string };
743
+ const nested = typeof parsed?.error === "object" && parsed.error ? parsed.error.message : undefined;
744
+ const flat = typeof parsed?.error === "string" ? parsed.error : parsed?.message;
745
+ message = nested || flat || (text ? `upstream error (${response.status}): ${text.slice(0, 400)}` : message);
746
+ } catch {
747
+ if (text) message = `upstream error (${response.status}): ${text.slice(0, 400)}`;
748
+ }
749
+ } catch { /* keep fallback message */ }
750
+ const upstreamRetryAfter = response.headers.get("retry-after");
751
+ const retryAfter = resolveClientRetryAfter({
752
+ status: response.status,
753
+ message,
754
+ upstreamRetryAfter,
755
+ })
756
+ // Instant-retry "0" is a valid client directive but rejected by cooldown parsers.
757
+ // Preserve it so it still wins over the transient "2" fallback (claude-529 mapping).
758
+ ?? (upstreamRetryAfter?.trim() === "0" ? "0" : undefined);
759
+ // Transient upstream 5xx (already retried pre-stream, 010): reclassify as Anthropic
760
+ // 529 overloaded_error so the Claude Code client applies its built-in backoff retry
761
+ // instead of dying on a fatal api_error (260716 sol-builder incident). The request
762
+ // log keeps the upstream status (captured in the deferred-log closure before this
763
+ // rewrite): log = upstream truth, client = retry signal.
764
+ // Retryable 429s also get Retry-After (#507) so Codex-shaped clients and Claude Code
765
+ // share a backoff hint when the upstream omitted the header.
766
+ const transient = isTransientUpstreamStatus(response.status);
767
+ const outStatus = transient ? 529 : response.status;
768
+ const out = new Response(JSON.stringify(anthropicErrorBody(outStatus, message)), {
769
+ status: outStatus,
770
+ headers: {
771
+ "Content-Type": "application/json",
772
+ ...(retryAfter ? { "Retry-After": retryAfter } : (transient ? { "Retry-After": "2" } : {})),
773
+ },
774
+ });
775
+ return out;
776
+ }
777
+
778
+ const contentType = response.headers.get("content-type") ?? "";
779
+ if (contentType.includes("text/event-stream") && response.body) {
780
+ const anthropicSse = responsesSseToAnthropicSse(response.body, requestedModel, { translatorBudget });
781
+ if (stream) {
782
+ return new Response(anthropicSse, {
783
+ status: 200,
784
+ headers: {
785
+ "Content-Type": "text/event-stream; charset=utf-8",
786
+ "Cache-Control": "no-cache",
787
+ "Connection": "keep-alive",
788
+ },
789
+ });
790
+ }
791
+ let message: Rec;
792
+ try {
793
+ message = await collectAnthropicMessage(anthropicSse, requestedModel, translatorBudget);
794
+ } catch (error) {
795
+ if (isTranslatorBudgetExceededError(error)) {
796
+ return anthropicErrorResponse(413, error.message, "request_too_large", error.code);
797
+ }
798
+ return anthropicErrorResponse(502, error instanceof Error ? error.message : String(error), "api_error");
799
+ }
800
+ const isError = (message as Rec).type === "error";
801
+ const translatedError = isError && typeof (message as Rec).error === "object"
802
+ ? (message as { error: { code?: unknown; message?: unknown } }).error
803
+ : undefined;
804
+ if (translatedError?.code === "translation_buffer_limit") {
805
+ return anthropicErrorResponse(
806
+ 413,
807
+ typeof translatedError.message === "string"
808
+ ? translatedError.message
809
+ : "upstream translation buffer exceeded the safe limit",
810
+ "request_too_large",
811
+ "translation_buffer_limit",
812
+ );
813
+ }
814
+ return new Response(JSON.stringify(message), {
815
+ status: isError ? 502 : 200,
816
+ headers: { "Content-Type": "application/json" },
817
+ });
818
+ }
819
+
820
+ // Defensive: some passthrough paths may answer JSON despite stream:true.
821
+ let json: unknown;
822
+ try {
823
+ json = await response.json();
824
+ } catch {
825
+ return anthropicErrorResponse(502, "internal replay returned a non-JSON response", "api_error");
826
+ }
827
+ const status = (json as Rec)?.status;
828
+ if (status === "failed") {
829
+ const error = (json as { error?: { message?: string; code?: string } }).error;
830
+ if (error?.code === "translation_buffer_limit") {
831
+ return anthropicErrorResponse(
832
+ 413,
833
+ error.message ?? "upstream translation buffer exceeded the safe limit",
834
+ "request_too_large",
835
+ "translation_buffer_limit",
836
+ );
837
+ }
838
+ return anthropicErrorResponse(502, error?.message ?? "upstream request failed", "api_error");
839
+ }
840
+ const message = responsesJsonToAnthropicMessage(json, requestedModel);
841
+ if ((message as Rec).type === "error") {
842
+ return new Response(JSON.stringify(message), {
843
+ status: 529,
844
+ headers: { "Content-Type": "application/json", "Retry-After": "2" },
845
+ });
846
+ }
847
+ if (!stream) {
848
+ return new Response(JSON.stringify(message), { status: 200, headers: { "Content-Type": "application/json" } });
849
+ }
850
+ // Streaming client + JSON upstream: synthesize a minimal valid Anthropic stream.
851
+ const encoder = new TextEncoder();
852
+ const frames: string[] = [];
853
+ const emit = (name: string, data: Rec) => frames.push(`event: ${name}\ndata: ${JSON.stringify(data)}\n\n`);
854
+ emit("message_start", { type: "message_start", message: { ...message, content: [], stop_reason: null, usage: { input_tokens: 0, output_tokens: 0 } } });
855
+ const blocks = Array.isArray((message as Rec).content) ? (message as Rec).content as Rec[] : [];
856
+ blocks.forEach((block, index) => {
857
+ emit("content_block_start", { type: "content_block_start", index, content_block: block });
858
+ emit("content_block_stop", { type: "content_block_stop", index });
859
+ });
860
+ emit("message_delta", { type: "message_delta", delta: { stop_reason: (message as Rec).stop_reason ?? "end_turn", stop_sequence: null }, usage: (message as Rec).usage ?? {} });
861
+ emit("message_stop", { type: "message_stop" });
862
+ return new Response(encoder.encode(frames.join("")), {
863
+ status: 200,
864
+ headers: { "Content-Type": "text/event-stream; charset=utf-8", "Cache-Control": "no-cache" },
865
+ });
866
+ }
867
+
868
+ /** Documented approximation: serialize system+messages+tools, run the char estimator. */
869
+ export async function handleClaudeCountTokens(req: Request, config: OcxConfig): Promise<Response> {
870
+ const disabled = claudeInboundDisabled(config);
871
+ if (disabled) return disabled;
872
+
873
+ let body: unknown;
874
+ const translatorBudget = createTranslatorBudget();
875
+ try {
876
+ body = await readAnthropicBody(req, translatorBudget);
877
+ } catch (err) {
878
+ if (err instanceof AnthropicRequestError) return anthropicErrorResponse(400, err.message);
879
+ return anthropicErrorResponse(500, err instanceof Error ? err.message : String(err));
880
+ } finally { translatorBudget.dispose(); }
881
+ if (!body || typeof body !== "object" || Array.isArray(body)) {
882
+ return anthropicErrorResponse(400, "request body must be a JSON object");
883
+ }
884
+ const raw = body as Rec;
885
+ if (typeof raw.model !== "string" || raw.model.length === 0) {
886
+ return anthropicErrorResponse(400, "model is required");
887
+ }
888
+ let model = raw.model;
889
+ // Case-insensitive [1m] strip (audit 021 #7 — the CLI matches /\[1m\]/i).
890
+ const stripped = stripOneMillionMarker(model);
891
+ if (stripped !== model) {
892
+ model = stripped;
893
+ raw.model = model;
894
+ }
895
+ // ocx-route override (devlog 072): keep count_tokens consistent with messages.
896
+ const countRoute = extractOcxRouteDirective(raw);
897
+ if (countRoute) {
898
+ model = stripOneMillionMarker(countRoute);
899
+ raw.model = model;
900
+ }
901
+ captureClaudeInbound("count_tokens", raw, resolveInboundModel(model, config.claudeCode), req.headers.get("anthropic-beta") ?? undefined);
902
+ if (wantsNativePassthrough(req, config, model)) {
903
+ return await anthropicNativePassthrough(req, config, { model, provider: "anthropic-native", surface: "claude" }, undefined, raw, "/v1/messages/count_tokens");
904
+ }
905
+ const parts: string[] = [];
906
+ if (raw.system !== undefined) parts.push(typeof raw.system === "string" ? raw.system : JSON.stringify(raw.system));
907
+ if (raw.messages !== undefined) parts.push(JSON.stringify(raw.messages));
908
+ if (raw.tools !== undefined) parts.push(JSON.stringify(raw.tools));
909
+ const inputTokens = Math.max(1, estimateTokens(parts.join("\n"), model));
910
+ return new Response(JSON.stringify({ input_tokens: inputTokens }), {
911
+ status: 200,
912
+ headers: { "Content-Type": "application/json" },
913
+ });
914
+ }