@yansigit/opencodex 2.31.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 (826) hide show
  1. package/AGENTS_INSTALL.md +109 -0
  2. package/LICENSE +21 -0
  3. package/README.md +303 -0
  4. package/assets/architecture.png +0 -0
  5. package/assets/banner.png +0 -0
  6. package/assets/claude-code-models.gif +0 -0
  7. package/assets/codex-app-picker.png +0 -0
  8. package/bin/ocx.mjs +587 -0
  9. package/bin/package-main.mjs +9 -0
  10. package/gui/dist/assets/index-BNJ7r4Gd.js +102 -0
  11. package/gui/dist/assets/index-CGoDO3uO.css +1 -0
  12. package/gui/dist/favicon.png +0 -0
  13. package/gui/dist/icons.svg +24 -0
  14. package/gui/dist/index.html +25 -0
  15. package/gui/dist/logo.png +0 -0
  16. package/gui/dist/provider-icons/alibaba-color.svg +1 -0
  17. package/gui/dist/provider-icons/antigravity-color.svg +1 -0
  18. package/gui/dist/provider-icons/claude-color.svg +1 -0
  19. package/gui/dist/provider-icons/cline-color.svg +16 -0
  20. package/gui/dist/provider-icons/cloudflare-ai-gateway-color.svg +1 -0
  21. package/gui/dist/provider-icons/commandcode-color.svg +1 -0
  22. package/gui/dist/provider-icons/copilot-color.svg +1 -0
  23. package/gui/dist/provider-icons/cursor-color.svg +2 -0
  24. package/gui/dist/provider-icons/deepseek-color.svg +1 -0
  25. package/gui/dist/provider-icons/discord.svg +1 -0
  26. package/gui/dist/provider-icons/firepass-color.svg +1 -0
  27. package/gui/dist/provider-icons/fireworks-color.svg +1 -0
  28. package/gui/dist/provider-icons/gemini-color.svg +1 -0
  29. package/gui/dist/provider-icons/github-copilot-color.svg +1 -0
  30. package/gui/dist/provider-icons/gitlab-duo-color.svg +1 -0
  31. package/gui/dist/provider-icons/grok.svg +1 -0
  32. package/gui/dist/provider-icons/groq-color.svg +1 -0
  33. package/gui/dist/provider-icons/huggingface-color.svg +1 -0
  34. package/gui/dist/provider-icons/kimi-color.svg +1 -0
  35. package/gui/dist/provider-icons/kiro-color.svg +15 -0
  36. package/gui/dist/provider-icons/lm-studio-color.svg +1 -0
  37. package/gui/dist/provider-icons/mistral-color.svg +1 -0
  38. package/gui/dist/provider-icons/moonshot-color.svg +1 -0
  39. package/gui/dist/provider-icons/nvidia-color.svg +1 -0
  40. package/gui/dist/provider-icons/ollama-color.svg +1 -0
  41. package/gui/dist/provider-icons/openai.svg +1 -0
  42. package/gui/dist/provider-icons/opencode.svg +2 -0
  43. package/gui/dist/provider-icons/openrouter-color.svg +1 -0
  44. package/gui/dist/provider-icons/pi.svg +21 -0
  45. package/gui/dist/provider-icons/qianfan-color.svg +1 -0
  46. package/gui/dist/provider-icons/qwen-portal-color.svg +1 -0
  47. package/gui/dist/provider-icons/telegram.svg +1 -0
  48. package/gui/dist/provider-icons/vercel-ai-gateway-color.svg +1 -0
  49. package/gui/dist/provider-icons/vllm-color.svg +1 -0
  50. package/gui/dist/provider-icons/xiaomi-color.svg +1 -0
  51. package/package.json +108 -0
  52. package/src/AGENTS.md +28 -0
  53. package/src/adapters/anthropic-image-guard.ts +251 -0
  54. package/src/adapters/anthropic-image-normalize.ts +518 -0
  55. package/src/adapters/anthropic-output-schema.ts +137 -0
  56. package/src/adapters/anthropic.ts +1327 -0
  57. package/src/adapters/azure.ts +36 -0
  58. package/src/adapters/base.ts +121 -0
  59. package/src/adapters/client-fingerprint.ts +65 -0
  60. package/src/adapters/cline-pass-deepseek-v4-tool-replay.ts +69 -0
  61. package/src/adapters/command-code.ts +601 -0
  62. package/src/adapters/cursor/arg-codec.ts +38 -0
  63. package/src/adapters/cursor/arg-normalize.ts +104 -0
  64. package/src/adapters/cursor/checkpoint-store.ts +303 -0
  65. package/src/adapters/cursor/cursor-errors.ts +288 -0
  66. package/src/adapters/cursor/discovery.ts +333 -0
  67. package/src/adapters/cursor/effort-map.ts +151 -0
  68. package/src/adapters/cursor/exec-policy.ts +88 -0
  69. package/src/adapters/cursor/framing.ts +250 -0
  70. package/src/adapters/cursor/gen/agent_pb.ts +15274 -0
  71. package/src/adapters/cursor/h2-pool.ts +123 -0
  72. package/src/adapters/cursor/http1-bidi.ts +361 -0
  73. package/src/adapters/cursor/images.ts +704 -0
  74. package/src/adapters/cursor/kv-store.ts +52 -0
  75. package/src/adapters/cursor/live-models.ts +269 -0
  76. package/src/adapters/cursor/live-smoke-gate.ts +41 -0
  77. package/src/adapters/cursor/live-transport.ts +1653 -0
  78. package/src/adapters/cursor/mcp-config.ts +42 -0
  79. package/src/adapters/cursor/mcp-manager.ts +333 -0
  80. package/src/adapters/cursor/message-mapper.ts +49 -0
  81. package/src/adapters/cursor/native-exec-common.ts +76 -0
  82. package/src/adapters/cursor/native-exec-desktop.ts +184 -0
  83. package/src/adapters/cursor/native-exec-fs.ts +332 -0
  84. package/src/adapters/cursor/native-exec-mcp.ts +153 -0
  85. package/src/adapters/cursor/native-exec-network.ts +43 -0
  86. package/src/adapters/cursor/native-exec-shell.ts +547 -0
  87. package/src/adapters/cursor/native-exec-tools.ts +118 -0
  88. package/src/adapters/cursor/native-exec.ts +663 -0
  89. package/src/adapters/cursor/protobuf-events.ts +1381 -0
  90. package/src/adapters/cursor/protobuf-request.ts +1032 -0
  91. package/src/adapters/cursor/request-builder.ts +461 -0
  92. package/src/adapters/cursor/thread-continuity.ts +67 -0
  93. package/src/adapters/cursor/tool-definitions.ts +735 -0
  94. package/src/adapters/cursor/tool-result-normalize.ts +92 -0
  95. package/src/adapters/cursor/transport-retry.ts +132 -0
  96. package/src/adapters/cursor/transport.ts +79 -0
  97. package/src/adapters/cursor/types.ts +79 -0
  98. package/src/adapters/cursor.ts +322 -0
  99. package/src/adapters/google-antigravity-hosts.ts +48 -0
  100. package/src/adapters/google-antigravity-replay.ts +827 -0
  101. package/src/adapters/google-antigravity-tools.ts +105 -0
  102. package/src/adapters/google-antigravity-wire.ts +141 -0
  103. package/src/adapters/google-errors.ts +92 -0
  104. package/src/adapters/google-http.ts +460 -0
  105. package/src/adapters/google-tool-schema.ts +238 -0
  106. package/src/adapters/google-truncation.ts +24 -0
  107. package/src/adapters/google-wire-compiler.ts +232 -0
  108. package/src/adapters/google.ts +1377 -0
  109. package/src/adapters/identity.ts +77 -0
  110. package/src/adapters/image.ts +23 -0
  111. package/src/adapters/kiro-constants.ts +16 -0
  112. package/src/adapters/kiro-errors.ts +208 -0
  113. package/src/adapters/kiro-events.ts +197 -0
  114. package/src/adapters/kiro-images.ts +129 -0
  115. package/src/adapters/kiro-retry.ts +312 -0
  116. package/src/adapters/kiro-thinking.ts +112 -0
  117. package/src/adapters/kiro-tool-fallback.ts +36 -0
  118. package/src/adapters/kiro-tools.ts +224 -0
  119. package/src/adapters/kiro-truncation.ts +33 -0
  120. package/src/adapters/kiro-wire.ts +129 -0
  121. package/src/adapters/kiro.ts +1936 -0
  122. package/src/adapters/mimo-free.ts +280 -0
  123. package/src/adapters/openai-chat-url.ts +11 -0
  124. package/src/adapters/openai-chat.ts +1980 -0
  125. package/src/adapters/openai-responses-url.ts +16 -0
  126. package/src/adapters/openai-responses.ts +1911 -0
  127. package/src/adapters/registry.ts +175 -0
  128. package/src/adapters/responses-tool-schema.ts +67 -0
  129. package/src/adapters/run-turn-queue.ts +114 -0
  130. package/src/adapters/tool-call-id.ts +119 -0
  131. package/src/adapters/tool-catalog-nudge.ts +154 -0
  132. package/src/adapters/upstream-http-error.ts +48 -0
  133. package/src/adapters/xai-web-search.ts +185 -0
  134. package/src/bridge.ts +1986 -0
  135. package/src/chat/inbound.ts +319 -0
  136. package/src/chat/outbound.ts +821 -0
  137. package/src/claude/agents-inject.ts +266 -0
  138. package/src/claude/alias.ts +149 -0
  139. package/src/claude/auth-detect.ts +229 -0
  140. package/src/claude/auth-mode-migration.ts +32 -0
  141. package/src/claude/auth-mode.ts +62 -0
  142. package/src/claude/context-windows.ts +205 -0
  143. package/src/claude/desktop-3p-guard.ts +35 -0
  144. package/src/claude/desktop-3p-paths.ts +84 -0
  145. package/src/claude/desktop-3p.ts +615 -0
  146. package/src/claude/desktop-health.ts +26 -0
  147. package/src/claude/desktop-profile.ts +263 -0
  148. package/src/claude/gateway-cache.ts +107 -0
  149. package/src/claude/inbound-debug.ts +163 -0
  150. package/src/claude/inbound.ts +578 -0
  151. package/src/claude/model-info.ts +174 -0
  152. package/src/claude/outbound.ts +926 -0
  153. package/src/cli/access.ts +108 -0
  154. package/src/cli/account-api.ts +302 -0
  155. package/src/cli/account-auth.ts +250 -0
  156. package/src/cli/account-catalog-refresh.ts +14 -0
  157. package/src/cli/account-extended.ts +737 -0
  158. package/src/cli/account-main.ts +317 -0
  159. package/src/cli/account.ts +299 -0
  160. package/src/cli/agent-driven.ts +70 -0
  161. package/src/cli/agent.ts +290 -0
  162. package/src/cli/catalog-prewarm.ts +27 -0
  163. package/src/cli/claude-agent-startup-sync.ts +73 -0
  164. package/src/cli/claude-desktop.ts +213 -0
  165. package/src/cli/claude.ts +355 -0
  166. package/src/cli/codex-log-guard-doctor.ts +103 -0
  167. package/src/cli/codex-shim-autorestore.ts +47 -0
  168. package/src/cli/codex-shim-readiness.ts +76 -0
  169. package/src/cli/combo.ts +127 -0
  170. package/src/cli/config-command.ts +209 -0
  171. package/src/cli/debug.ts +228 -0
  172. package/src/cli/dispatch.ts +585 -0
  173. package/src/cli/doctor.ts +1202 -0
  174. package/src/cli/ensure-desired-integrations.ts +152 -0
  175. package/src/cli/export-command.ts +213 -0
  176. package/src/cli/help.ts +101 -0
  177. package/src/cli/index.ts +973 -0
  178. package/src/cli/init.ts +211 -0
  179. package/src/cli/integrations.ts +260 -0
  180. package/src/cli/interactive-confirm.ts +133 -0
  181. package/src/cli/lab.ts +607 -0
  182. package/src/cli/launcher-context.ts +77 -0
  183. package/src/cli/minimax.ts +497 -0
  184. package/src/cli/models-runtime.ts +245 -0
  185. package/src/cli/models.ts +422 -0
  186. package/src/cli/observe.ts +206 -0
  187. package/src/cli/opencode.ts +588 -0
  188. package/src/cli/provider-replit.ts +232 -0
  189. package/src/cli/provider-runtime.ts +179 -0
  190. package/src/cli/provider.ts +492 -0
  191. package/src/cli/ready.ts +301 -0
  192. package/src/cli/registry.ts +422 -0
  193. package/src/cli/replit-gateway-key-input.ts +138 -0
  194. package/src/cli/root.ts +86 -0
  195. package/src/cli/route-policy.ts +92 -0
  196. package/src/cli/runtime-api.ts +328 -0
  197. package/src/cli/star-prompt.ts +211 -0
  198. package/src/cli/status-oauth.ts +78 -0
  199. package/src/cli/status.ts +328 -0
  200. package/src/cli/system-command.ts +112 -0
  201. package/src/cli/system-restart-client.ts +146 -0
  202. package/src/cli/tray-proxy.ts +199 -0
  203. package/src/cli/v2.ts +268 -0
  204. package/src/cli.ts +10 -0
  205. package/src/clients/config-export.ts +1704 -0
  206. package/src/codex/account-id.ts +34 -0
  207. package/src/codex/account-label.ts +47 -0
  208. package/src/codex/account-lifecycle.ts +172 -0
  209. package/src/codex/account-namespace-match.ts +63 -0
  210. package/src/codex/account-namespaces.ts +195 -0
  211. package/src/codex/account-pause.ts +20 -0
  212. package/src/codex/account-priority.ts +83 -0
  213. package/src/codex/account-runtime-state.ts +31 -0
  214. package/src/codex/account-store.ts +544 -0
  215. package/src/codex/account-usability.ts +43 -0
  216. package/src/codex/admission.ts +256 -0
  217. package/src/codex/affinity-debug.ts +162 -0
  218. package/src/codex/agent-roles-sync.ts +225 -0
  219. package/src/codex/agent-roles.ts +238 -0
  220. package/src/codex/app-server-processes.ts +1143 -0
  221. package/src/codex/app-server-restart-service.ts +232 -0
  222. package/src/codex/auth-api.ts +2147 -0
  223. package/src/codex/auth-collision.ts +109 -0
  224. package/src/codex/auth-context.ts +665 -0
  225. package/src/codex/autostart-health.ts +156 -0
  226. package/src/codex/catalog/account-models.ts +67 -0
  227. package/src/codex/catalog/aggregation.ts +436 -0
  228. package/src/codex/catalog/bundled.ts +549 -0
  229. package/src/codex/catalog/effort.ts +446 -0
  230. package/src/codex/catalog/filesystem-evidence.ts +302 -0
  231. package/src/codex/catalog/kinds.ts +2 -0
  232. package/src/codex/catalog/metadata.ts +664 -0
  233. package/src/codex/catalog/native-models.ts +72 -0
  234. package/src/codex/catalog/parsing.ts +650 -0
  235. package/src/codex/catalog/provider-fetch.ts +2064 -0
  236. package/src/codex/catalog/sync.ts +1883 -0
  237. package/src/codex/catalog-admission.ts +199 -0
  238. package/src/codex/catalog-refresh-status.ts +105 -0
  239. package/src/codex/catalog-write-serialization.ts +242 -0
  240. package/src/codex/catalog.ts +14 -0
  241. package/src/codex/codex-write-lock.ts +384 -0
  242. package/src/codex/convergence-types.ts +614 -0
  243. package/src/codex/convergence.ts +651 -0
  244. package/src/codex/coordinator-doctor.ts +332 -0
  245. package/src/codex/custom-model-catalog-migration.ts +176 -0
  246. package/src/codex/data/upstream-models.json +830 -0
  247. package/src/codex/desired-state.ts +230 -0
  248. package/src/codex/exec-invocation.ts +22 -0
  249. package/src/codex/features.ts +1566 -0
  250. package/src/codex/generation.ts +202 -0
  251. package/src/codex/history-job.ts +407 -0
  252. package/src/codex/history-lock.ts +242 -0
  253. package/src/codex/history-migration-guardian.ts +108 -0
  254. package/src/codex/history-provider.ts +979 -0
  255. package/src/codex/history-transition.ts +105 -0
  256. package/src/codex/history-worker.ts +220 -0
  257. package/src/codex/home.ts +206 -0
  258. package/src/codex/inject-coordination.ts +290 -0
  259. package/src/codex/inject.ts +1733 -0
  260. package/src/codex/injected-marker.ts +106 -0
  261. package/src/codex/integration-record.ts +266 -0
  262. package/src/codex/internal/catalog-writer.ts +203 -0
  263. package/src/codex/internal/history-writer.ts +80 -0
  264. package/src/codex/journal.ts +225 -0
  265. package/src/codex/log-guard/inspect.ts +506 -0
  266. package/src/codex/log-guard/lock.ts +150 -0
  267. package/src/codex/log-guard/maintenance.ts +403 -0
  268. package/src/codex/log-guard/path-safety.ts +88 -0
  269. package/src/codex/log-guard/policy.ts +44 -0
  270. package/src/codex/log-guard/processes.ts +205 -0
  271. package/src/codex/log-guard/protection.ts +489 -0
  272. package/src/codex/log-guard/sqlite-errors.ts +9 -0
  273. package/src/codex/main-account-cache.ts +56 -0
  274. package/src/codex/main-account.ts +68 -0
  275. package/src/codex/management-convergence.ts +167 -0
  276. package/src/codex/model-cache.ts +273 -0
  277. package/src/codex/model-entitlements.ts +353 -0
  278. package/src/codex/native-main-admission.ts +47 -0
  279. package/src/codex/native-main-auth-temp.ts +187 -0
  280. package/src/codex/native-main-claim.ts +178 -0
  281. package/src/codex/native-main-lock-file.ts +162 -0
  282. package/src/codex/native-main-owner.ts +329 -0
  283. package/src/codex/native-profile-api.ts +247 -0
  284. package/src/codex/native-profile-manager.ts +1531 -0
  285. package/src/codex/native-profile-processes.ts +121 -0
  286. package/src/codex/native-profile-recovery.ts +99 -0
  287. package/src/codex/native-profile-stage-store.ts +387 -0
  288. package/src/codex/native-profile-startup.ts +492 -0
  289. package/src/codex/native-profile-store.ts +855 -0
  290. package/src/codex/native-profile-types.ts +120 -0
  291. package/src/codex/native-residue.ts +682 -0
  292. package/src/codex/paths.ts +144 -0
  293. package/src/codex/plan-from-token.ts +140 -0
  294. package/src/codex/plan.ts +40 -0
  295. package/src/codex/plugins-doctor.ts +242 -0
  296. package/src/codex/pool-rotation.ts +295 -0
  297. package/src/codex/project-config-warnings.ts +425 -0
  298. package/src/codex/prompt-journal.ts +352 -0
  299. package/src/codex/prompt-layers.ts +967 -0
  300. package/src/codex/prompt-lock.ts +143 -0
  301. package/src/codex/quota-rejection.ts +298 -0
  302. package/src/codex/quota.ts +573 -0
  303. package/src/codex/refresh.ts +62 -0
  304. package/src/codex/reset-credit-recovery.ts +1044 -0
  305. package/src/codex/routing.ts +1888 -0
  306. package/src/codex/runtime.ts +659 -0
  307. package/src/codex/shim.ts +2170 -0
  308. package/src/codex/subagent-defaults.ts +550 -0
  309. package/src/codex/subagent-model-fallback.ts +784 -0
  310. package/src/codex/sync.ts +319 -0
  311. package/src/codex/transition-state.ts +612 -0
  312. package/src/codex/upstream-host-health.ts +368 -0
  313. package/src/codex/user-identity.ts +557 -0
  314. package/src/codex/warmup.ts +298 -0
  315. package/src/codex/websocket-registry.ts +100 -0
  316. package/src/codex/write-coordination.ts +114 -0
  317. package/src/combos/failover.ts +160 -0
  318. package/src/combos/index.ts +45 -0
  319. package/src/combos/request.ts +94 -0
  320. package/src/combos/resolve.ts +232 -0
  321. package/src/combos/types.ts +398 -0
  322. package/src/config/provider-name.ts +24 -0
  323. package/src/config.ts +4041 -0
  324. package/src/fork/register.ts +3 -0
  325. package/src/generated/compatibility-version.json +3116 -0
  326. package/src/generated/model-metadata.ts +106 -0
  327. package/src/github/star-state.ts +203 -0
  328. package/src/grok/inject.ts +530 -0
  329. package/src/grok/inspect.ts +45 -0
  330. package/src/grok/status.ts +121 -0
  331. package/src/grok/sync.ts +66 -0
  332. package/src/images/artifacts.ts +516 -0
  333. package/src/images/fulfill-video.ts +163 -0
  334. package/src/images/fulfill.ts +149 -0
  335. package/src/images/index.ts +4 -0
  336. package/src/images/loop.ts +955 -0
  337. package/src/images/plan.ts +143 -0
  338. package/src/images/synthetic-tool.ts +133 -0
  339. package/src/images/types.ts +41 -0
  340. package/src/images/xai-client.ts +141 -0
  341. package/src/images/xai-video-client.ts +163 -0
  342. package/src/index.ts +22 -0
  343. package/src/integrations/config-io.ts +269 -0
  344. package/src/integrations/journal.ts +315 -0
  345. package/src/integrations/merge.ts +135 -0
  346. package/src/integrations/mutation-flight.ts +71 -0
  347. package/src/integrations/native/ownership-preflight.ts +202 -0
  348. package/src/integrations/omp-yaml-source.ts +358 -0
  349. package/src/integrations/owned-refresh.ts +74 -0
  350. package/src/integrations/ownership.ts +111 -0
  351. package/src/integrations/registry.ts +159 -0
  352. package/src/integrations/serialize.ts +314 -0
  353. package/src/integrations/state.ts +361 -0
  354. package/src/integrations/store.ts +103 -0
  355. package/src/integrations/writer-lock.ts +98 -0
  356. package/src/integrations/writer.ts +691 -0
  357. package/src/lab/artifacts/sanitize.ts +586 -0
  358. package/src/lab/artifacts/secure-fs.ts +475 -0
  359. package/src/lab/artifacts/store.ts +310 -0
  360. package/src/lab/automation/budgets.ts +78 -0
  361. package/src/lab/automation/config-persistence.ts +256 -0
  362. package/src/lab/automation/constants.ts +39 -0
  363. package/src/lab/automation/cooldown.ts +103 -0
  364. package/src/lab/automation/dispatch.ts +211 -0
  365. package/src/lab/automation/index.ts +13 -0
  366. package/src/lab/automation/orchestrator.ts +499 -0
  367. package/src/lab/automation/persistence.ts +512 -0
  368. package/src/lab/automation/planner.ts +371 -0
  369. package/src/lab/automation/policy.ts +136 -0
  370. package/src/lab/automation/queue.ts +191 -0
  371. package/src/lab/automation/recovery.ts +24 -0
  372. package/src/lab/automation/route-context.ts +21 -0
  373. package/src/lab/automation/run-key.ts +44 -0
  374. package/src/lab/automation/runs-query.ts +34 -0
  375. package/src/lab/automation/types.ts +160 -0
  376. package/src/lab/conformance/assertion.ts +325 -0
  377. package/src/lab/conformance/digest.ts +22 -0
  378. package/src/lab/conformance/executor.ts +741 -0
  379. package/src/lab/conformance/fixture-provider.ts +27 -0
  380. package/src/lab/conformance/fixtures/live-v1-cases.json +175 -0
  381. package/src/lab/conformance/fixtures/protocol-v1-cases.json +461 -0
  382. package/src/lab/conformance/harness-budget.ts +47 -0
  383. package/src/lab/conformance/index.ts +5 -0
  384. package/src/lab/conformance/jcs.ts +64 -0
  385. package/src/lab/conformance/json-pointer.ts +39 -0
  386. package/src/lab/conformance/manifest.ts +180 -0
  387. package/src/lab/conformance/mcp-stub.ts +179 -0
  388. package/src/lab/conformance/negative-controls.ts +164 -0
  389. package/src/lab/conformance/observation.ts +355 -0
  390. package/src/lab/conformance/runner.ts +68 -0
  391. package/src/lab/conformance/sse-normalize.ts +59 -0
  392. package/src/lab/conformance/suite-manifest.ts +78 -0
  393. package/src/lab/conformance/types.ts +214 -0
  394. package/src/lab/constants.ts +126 -0
  395. package/src/lab/digest.ts +64 -0
  396. package/src/lab/events/errors.ts +9 -0
  397. package/src/lab/events/limits.ts +117 -0
  398. package/src/lab/events/types.ts +229 -0
  399. package/src/lab/events/validate.ts +781 -0
  400. package/src/lab/fabric/constants.ts +40 -0
  401. package/src/lab/fabric/executor.ts +492 -0
  402. package/src/lab/fabric/index.ts +80 -0
  403. package/src/lab/fabric/manifest.ts +222 -0
  404. package/src/lab/fabric/observe.ts +489 -0
  405. package/src/lab/fabric/patch.ts +79 -0
  406. package/src/lab/fabric/producer-child.ts +139 -0
  407. package/src/lab/fabric/producer-isolate.ts +276 -0
  408. package/src/lab/fabric/producer-protocol.ts +61 -0
  409. package/src/lab/fabric/scratch.ts +439 -0
  410. package/src/lab/fabric/subject.ts +106 -0
  411. package/src/lab/fabric/types.ts +134 -0
  412. package/src/lab/fabric/verifier.ts +98 -0
  413. package/src/lab/index.ts +54 -0
  414. package/src/lab/ledger/artifact-refs.ts +127 -0
  415. package/src/lab/ledger/invalidation.ts +136 -0
  416. package/src/lab/ledger/purge.ts +310 -0
  417. package/src/lab/ledger/store.ts +532 -0
  418. package/src/lab/live/credential-lease.ts +53 -0
  419. package/src/lab/live/destination.ts +155 -0
  420. package/src/lab/live/executor.ts +336 -0
  421. package/src/lab/live/inert-tools.ts +56 -0
  422. package/src/lab/live/manifest.ts +85 -0
  423. package/src/lab/live/mcp-loopback.ts +57 -0
  424. package/src/lab/live/runner.ts +19 -0
  425. package/src/lab/live/sandbox.ts +61 -0
  426. package/src/lab/live/suite-manifest.ts +41 -0
  427. package/src/lab/live/transport.ts +118 -0
  428. package/src/lab/live/types.ts +197 -0
  429. package/src/lab/observe/from-conformance.ts +301 -0
  430. package/src/lab/observe/from-live.ts +117 -0
  431. package/src/lab/paths.ts +153 -0
  432. package/src/lab/projection/rebuild.ts +495 -0
  433. package/src/lab/projection/schema.ts +135 -0
  434. package/src/lab/projection/verdicts.ts +474 -0
  435. package/src/lab/projection/verification.ts +412 -0
  436. package/src/lab/public/bundle.ts +217 -0
  437. package/src/lab/public/community-authority.ts +175 -0
  438. package/src/lab/public/community-files.ts +29 -0
  439. package/src/lab/public/community.ts +479 -0
  440. package/src/lab/public/file-safety.ts +155 -0
  441. package/src/lab/public/ids.ts +26 -0
  442. package/src/lab/public/index.ts +16 -0
  443. package/src/lab/public/mutation-lock.ts +424 -0
  444. package/src/lab/public/operator.ts +353 -0
  445. package/src/lab/public/origin-purge.ts +79 -0
  446. package/src/lab/public/origin.ts +203 -0
  447. package/src/lab/public/privacy.ts +143 -0
  448. package/src/lab/public/private-file.ts +261 -0
  449. package/src/lab/public/project.ts +124 -0
  450. package/src/lab/public/purge-test-fault.ts +21 -0
  451. package/src/lab/public/purge.ts +223 -0
  452. package/src/lab/public/registry.ts +44 -0
  453. package/src/lab/public/revocation.ts +252 -0
  454. package/src/lab/public/signature.ts +243 -0
  455. package/src/lab/public/storage.ts +105 -0
  456. package/src/lab/public/strict-json.ts +206 -0
  457. package/src/lab/public/time.ts +26 -0
  458. package/src/lab/public/types.ts +172 -0
  459. package/src/lab/public/validate.ts +391 -0
  460. package/src/lab/query/catalog.ts +101 -0
  461. package/src/lab/query/connection.ts +107 -0
  462. package/src/lab/query/constants.ts +4 -0
  463. package/src/lab/query/cursor.ts +132 -0
  464. package/src/lab/query/dto-map.ts +277 -0
  465. package/src/lab/query/errors.ts +22 -0
  466. package/src/lab/query/freshness.ts +53 -0
  467. package/src/lab/query/index.ts +45 -0
  468. package/src/lab/query/latest-observation.ts +59 -0
  469. package/src/lab/query/passive-production.ts +159 -0
  470. package/src/lab/query/queries.ts +444 -0
  471. package/src/lab/query/types.ts +266 -0
  472. package/src/lab/subject/behavior-fingerprint.ts +77 -0
  473. package/src/lab/subject/installation-salt.ts +112 -0
  474. package/src/lab/subject/protocol-subject.ts +80 -0
  475. package/src/lab/subject/route-subject.ts +74 -0
  476. package/src/lib/abort.ts +146 -0
  477. package/src/lib/admin-secrets.ts +25 -0
  478. package/src/lib/admission.ts +83 -0
  479. package/src/lib/app-owned-memory-stores.ts +195 -0
  480. package/src/lib/app-owned-memory.ts +265 -0
  481. package/src/lib/bounded-body.ts +346 -0
  482. package/src/lib/bun-binary-validator.d.mts +3 -0
  483. package/src/lib/bun-binary-validator.mjs +18 -0
  484. package/src/lib/bun-runtime.ts +184 -0
  485. package/src/lib/bun-stream-caps.ts +130 -0
  486. package/src/lib/codex-restart-contract.ts +120 -0
  487. package/src/lib/config-ownership.ts +364 -0
  488. package/src/lib/crash-guard.ts +344 -0
  489. package/src/lib/debug-log-buffer.ts +83 -0
  490. package/src/lib/debug-settings.ts +108 -0
  491. package/src/lib/debug.ts +31 -0
  492. package/src/lib/destination-policy.ts +380 -0
  493. package/src/lib/errors.ts +406 -0
  494. package/src/lib/eventstream-decoder.ts +253 -0
  495. package/src/lib/fabric-task-execution-authority.ts +7 -0
  496. package/src/lib/fabric-task-host.ts +29 -0
  497. package/src/lib/gcp-adc.ts +341 -0
  498. package/src/lib/injection-debug-log.ts +58 -0
  499. package/src/lib/lab-activation.ts +223 -0
  500. package/src/lib/lab-live-execution-authority.ts +13 -0
  501. package/src/lib/lab-live-host.ts +30 -0
  502. package/src/lib/lab-live-pinned-sender.ts +56 -0
  503. package/src/lib/lab-live-route-production.ts +130 -0
  504. package/src/lib/lab-passive-linker-registration.ts +26 -0
  505. package/src/lib/local-management-attestation.ts +51 -0
  506. package/src/lib/local-management-capability.ts +100 -0
  507. package/src/lib/local-provider-reload-contract.ts +100 -0
  508. package/src/lib/open-url.ts +25 -0
  509. package/src/lib/optional-shutdown-hooks.ts +57 -0
  510. package/src/lib/pinned-http.ts +270 -0
  511. package/src/lib/privacy.ts +20 -0
  512. package/src/lib/process-control.ts +168 -0
  513. package/src/lib/provider-outbound.ts +210 -0
  514. package/src/lib/provider-url.ts +14 -0
  515. package/src/lib/proxy-env.ts +18 -0
  516. package/src/lib/redact.ts +521 -0
  517. package/src/lib/retry-after.ts +55 -0
  518. package/src/lib/self-launch-argv.ts +15 -0
  519. package/src/lib/server-resource-ownership.ts +71 -0
  520. package/src/lib/service-secrets.ts +25 -0
  521. package/src/lib/shadow-call.ts +61 -0
  522. package/src/lib/sidecar-tracker.ts +52 -0
  523. package/src/lib/sse-decoder.ts +364 -0
  524. package/src/lib/state-store-registrations.ts +119 -0
  525. package/src/lib/state-store-sweeper.ts +184 -0
  526. package/src/lib/system-restart-contract.ts +73 -0
  527. package/src/lib/test-home-guard.ts +90 -0
  528. package/src/lib/token-estimate.ts +86 -0
  529. package/src/lib/tool-argument-integers.ts +202 -0
  530. package/src/lib/translator-budget.ts +400 -0
  531. package/src/lib/upstream-http-version.ts +57 -0
  532. package/src/lib/upstream-reachability.ts +95 -0
  533. package/src/lib/upstream-retry.ts +392 -0
  534. package/src/lib/win-exec.ts +115 -0
  535. package/src/lib/win-paths.ts +68 -0
  536. package/src/lib/windows-atomic-replace.ts +156 -0
  537. package/src/lib/windows-elevation.ts +773 -0
  538. package/src/lib/windows-secret-acl.ts +854 -0
  539. package/src/lib/windows-service-wrappers.ts +72 -0
  540. package/src/lib/windows-text.ts +106 -0
  541. package/src/lib/windows-user-principal.ts +341 -0
  542. package/src/lib/winsw.ts +403 -0
  543. package/src/oauth/account-import/google-antigravity-adapter.ts +74 -0
  544. package/src/oauth/account-import/index.ts +15 -0
  545. package/src/oauth/account-import/parser.ts +83 -0
  546. package/src/oauth/account-import/registry.ts +18 -0
  547. package/src/oauth/account-import/service.ts +75 -0
  548. package/src/oauth/account-import/types.ts +91 -0
  549. package/src/oauth/anthropic-routing.ts +594 -0
  550. package/src/oauth/anthropic.ts +188 -0
  551. package/src/oauth/antigravity-routing.ts +151 -0
  552. package/src/oauth/callback-server.ts +300 -0
  553. package/src/oauth/chatgpt.ts +161 -0
  554. package/src/oauth/command-code.ts +239 -0
  555. package/src/oauth/cursor.ts +252 -0
  556. package/src/oauth/github-copilot.ts +428 -0
  557. package/src/oauth/google-antigravity.ts +262 -0
  558. package/src/oauth/health.ts +407 -0
  559. package/src/oauth/index.ts +1504 -0
  560. package/src/oauth/key-providers.ts +124 -0
  561. package/src/oauth/kimi.ts +227 -0
  562. package/src/oauth/kiro-credentials.ts +726 -0
  563. package/src/oauth/kiro.ts +621 -0
  564. package/src/oauth/local-token-detect.ts +130 -0
  565. package/src/oauth/log.ts +50 -0
  566. package/src/oauth/login-cli.ts +223 -0
  567. package/src/oauth/nous.ts +798 -0
  568. package/src/oauth/pkce.ts +15 -0
  569. package/src/oauth/store.ts +728 -0
  570. package/src/oauth/token-guardian.ts +309 -0
  571. package/src/oauth/types.ts +62 -0
  572. package/src/oauth/xai.ts +241 -0
  573. package/src/providers/alibaba-region-backup.ts +75 -0
  574. package/src/providers/alibaba-region-migration.ts +156 -0
  575. package/src/providers/alibaba-region-startup.ts +36 -0
  576. package/src/providers/antigravity-models.ts +695 -0
  577. package/src/providers/antigravity-quota.ts +216 -0
  578. package/src/providers/api-keys.ts +140 -0
  579. package/src/providers/base-url-choices.ts +74 -0
  580. package/src/providers/codex-capacity.ts +292 -0
  581. package/src/providers/command-code-efforts.ts +144 -0
  582. package/src/providers/context-cap.ts +82 -0
  583. package/src/providers/cursor-pool.ts +72 -0
  584. package/src/providers/derive.ts +586 -0
  585. package/src/providers/fastwire.ts +501 -0
  586. package/src/providers/free-directory.ts +187 -0
  587. package/src/providers/github-copilot-transport.ts +56 -0
  588. package/src/providers/google-vertex-location.ts +14 -0
  589. package/src/providers/key-failover.ts +271 -0
  590. package/src/providers/kiro-models.ts +67 -0
  591. package/src/providers/label.ts +19 -0
  592. package/src/providers/model-discovery-limits.ts +16 -0
  593. package/src/providers/model-discovery.ts +449 -0
  594. package/src/providers/model-rename-migration.ts +255 -0
  595. package/src/providers/model-rename-startup.ts +28 -0
  596. package/src/providers/openai-sidecar.ts +243 -0
  597. package/src/providers/openai-tier-startup.ts +56 -0
  598. package/src/providers/openai-tiers.ts +423 -0
  599. package/src/providers/openai-virtual-models.ts +83 -0
  600. package/src/providers/opencode-zen-rate-limit.ts +102 -0
  601. package/src/providers/openrouter-routing.ts +102 -0
  602. package/src/providers/provider-id-rewrite.ts +185 -0
  603. package/src/providers/quota.ts +2345 -0
  604. package/src/providers/registry.ts +2918 -0
  605. package/src/providers/replit/constants.ts +27 -0
  606. package/src/providers/replit/derive.ts +85 -0
  607. package/src/providers/replit/headers.ts +28 -0
  608. package/src/providers/replit/origin.ts +55 -0
  609. package/src/providers/replit/pair-install-response.ts +72 -0
  610. package/src/providers/replit/probe.ts +199 -0
  611. package/src/providers/replit/setup.ts +350 -0
  612. package/src/providers/request-pacing.ts +310 -0
  613. package/src/providers/service-tier.ts +277 -0
  614. package/src/providers/slug-codec.ts +103 -0
  615. package/src/providers/static-model-discovery.ts +86 -0
  616. package/src/providers/xai-responses-opt-in.ts +15 -0
  617. package/src/providers/xai-transport.ts +148 -0
  618. package/src/reasoning-effort.ts +183 -0
  619. package/src/responses/compaction.ts +142 -0
  620. package/src/responses/custom-tool-compat.ts +266 -0
  621. package/src/responses/hosted-tool-policy.ts +9 -0
  622. package/src/responses/namespace-tool-compat.ts +355 -0
  623. package/src/responses/parser.ts +838 -0
  624. package/src/responses/provider-continuation.ts +98 -0
  625. package/src/responses/provider-opaque-metadata.ts +73 -0
  626. package/src/responses/reasoning-envelope.ts +60 -0
  627. package/src/responses/reasoning-replay-cache.ts +426 -0
  628. package/src/responses/schema.ts +165 -0
  629. package/src/responses/spill-store.ts +459 -0
  630. package/src/responses/state.ts +1433 -0
  631. package/src/responses/thought-signature-replay.ts +347 -0
  632. package/src/responses/tool-groups.ts +19 -0
  633. package/src/responses/tool-search-compat.ts +301 -0
  634. package/src/responses/truncated-stop-reason.ts +60 -0
  635. package/src/router.ts +761 -0
  636. package/src/routing/analytics.ts +378 -0
  637. package/src/routing/capability.ts +244 -0
  638. package/src/routing/compatibility/assemble.ts +73 -0
  639. package/src/routing/compatibility/behavior.ts +278 -0
  640. package/src/routing/compatibility/catalog.ts +99 -0
  641. package/src/routing/compatibility/endpoint.ts +52 -0
  642. package/src/routing/compatibility/lab-evidence-provider.ts +130 -0
  643. package/src/routing/compatibility/policy.ts +181 -0
  644. package/src/routing/compatibility/provider-slot.ts +56 -0
  645. package/src/routing/compatibility/reader.ts +110 -0
  646. package/src/routing/compatibility/subject.ts +191 -0
  647. package/src/routing/compatibility/types.ts +64 -0
  648. package/src/routing/compatibility/version.ts +104 -0
  649. package/src/routing/cost.ts +77 -0
  650. package/src/routing/evaluator.ts +495 -0
  651. package/src/routing/health.ts +412 -0
  652. package/src/routing/history/cursor.ts +43 -0
  653. package/src/routing/history/indexer.ts +605 -0
  654. package/src/routing/history/schema.ts +72 -0
  655. package/src/routing/profile-namespace.ts +15 -0
  656. package/src/routing/profile.ts +547 -0
  657. package/src/routing/quota.ts +145 -0
  658. package/src/routing/request-evidence.ts +45 -0
  659. package/src/routing/trace.ts +776 -0
  660. package/src/server/adapter-resolve.ts +53 -0
  661. package/src/server/auth-cors.ts +751 -0
  662. package/src/server/background-lifecycle.ts +182 -0
  663. package/src/server/chat-completions.ts +442 -0
  664. package/src/server/chat-native-sse.ts +331 -0
  665. package/src/server/chat-native.ts +426 -0
  666. package/src/server/claude-messages.ts +1030 -0
  667. package/src/server/direct-local-http.ts +347 -0
  668. package/src/server/effort-policy.ts +190 -0
  669. package/src/server/github-copilot-responses-repair.ts +338 -0
  670. package/src/server/gui-static.ts +152 -0
  671. package/src/server/image-retry.ts +42 -0
  672. package/src/server/images.ts +568 -0
  673. package/src/server/index.ts +1813 -0
  674. package/src/server/lifecycle.ts +498 -0
  675. package/src/server/live.ts +717 -0
  676. package/src/server/local-management-read-client.ts +90 -0
  677. package/src/server/local-provider-reload-client.ts +137 -0
  678. package/src/server/management/agent-settings-routes.ts +1433 -0
  679. package/src/server/management/api-access.ts +141 -0
  680. package/src/server/management/api-key-usage.ts +193 -0
  681. package/src/server/management/body.ts +41 -0
  682. package/src/server/management/combo-routes.ts +263 -0
  683. package/src/server/management/config-routes.ts +835 -0
  684. package/src/server/management/context.ts +113 -0
  685. package/src/server/management/integration-routes.ts +498 -0
  686. package/src/server/management/lab-automation-routes.ts +206 -0
  687. package/src/server/management/lab-routes.ts +563 -0
  688. package/src/server/management/logs-usage-routes.ts +586 -0
  689. package/src/server/management/model-routes.ts +560 -0
  690. package/src/server/management/model-rows.ts +163 -0
  691. package/src/server/management/native-integration-routes.ts +769 -0
  692. package/src/server/management/oauth-account-routes.ts +637 -0
  693. package/src/server/management/provider-capability-config.ts +48 -0
  694. package/src/server/management/provider-routes.ts +1033 -0
  695. package/src/server/management/replit-provider-routes.ts +86 -0
  696. package/src/server/management/request-history-routes.ts +191 -0
  697. package/src/server/management/routing-analytics-routes.ts +74 -0
  698. package/src/server/management/routing-profile-routes.ts +385 -0
  699. package/src/server/management/shared.ts +286 -0
  700. package/src/server/management/sidebar-routes.ts +106 -0
  701. package/src/server/management/storage-log-guard-routes.ts +186 -0
  702. package/src/server/management/sync-response.ts +69 -0
  703. package/src/server/management/system-restart.ts +435 -0
  704. package/src/server/management/system-routes.ts +194 -0
  705. package/src/server/management/usage-summary-cache.ts +94 -0
  706. package/src/server/management/vision-sidecar-options.ts +167 -0
  707. package/src/server/management/web-search-sidecar-options.ts +120 -0
  708. package/src/server/management-api.ts +314 -0
  709. package/src/server/management-auth.ts +482 -0
  710. package/src/server/memory-watchdog.ts +156 -0
  711. package/src/server/passive-route-linker.ts +66 -0
  712. package/src/server/port-reclaim.ts +307 -0
  713. package/src/server/ports.ts +156 -0
  714. package/src/server/proxy-liveness.ts +328 -0
  715. package/src/server/readiness.ts +99 -0
  716. package/src/server/relay-eager.ts +353 -0
  717. package/src/server/relay.ts +1209 -0
  718. package/src/server/request-decompress.ts +239 -0
  719. package/src/server/request-log-conversation.ts +168 -0
  720. package/src/server/request-log.ts +1259 -0
  721. package/src/server/responses/agent-task-recovery-cache.ts +143 -0
  722. package/src/server/responses/agent-task-recovery.ts +465 -0
  723. package/src/server/responses/collaboration.ts +551 -0
  724. package/src/server/responses/compact.ts +771 -0
  725. package/src/server/responses/core.ts +5389 -0
  726. package/src/server/responses/empty-completion-guard.ts +276 -0
  727. package/src/server/responses/encrypted-payload.ts +331 -0
  728. package/src/server/responses/fetch-helpers.ts +232 -0
  729. package/src/server/responses/input-admission.ts +185 -0
  730. package/src/server/responses/pacing-overload.ts +13 -0
  731. package/src/server/responses/passthrough-error.ts +78 -0
  732. package/src/server/responses/policy-fallback.ts +178 -0
  733. package/src/server/responses/responses-field-backfill.ts +251 -0
  734. package/src/server/responses/terminal-guard.ts +251 -0
  735. package/src/server/responses/upstream-error.ts +53 -0
  736. package/src/server/responses/ws-upstream.ts +308 -0
  737. package/src/server/responses-custom-tool-repair.ts +282 -0
  738. package/src/server/responses-image-gen-repair.ts +132 -0
  739. package/src/server/responses-item-id-repair.ts +272 -0
  740. package/src/server/responses-json-events.ts +90 -0
  741. package/src/server/responses-model-rewrite.ts +29 -0
  742. package/src/server/responses-reasoning-summary-rewrite.ts +178 -0
  743. package/src/server/responses-snapshot-repair.ts +621 -0
  744. package/src/server/responses-terminal-repair.ts +342 -0
  745. package/src/server/responses-tool-search-repair.ts +267 -0
  746. package/src/server/responses-undeclared-tool-guard.ts +153 -0
  747. package/src/server/responses.ts +25 -0
  748. package/src/server/search.ts +201 -0
  749. package/src/server/sse-frame-buffer.ts +292 -0
  750. package/src/server/sse-payload-rewrite.ts +263 -0
  751. package/src/server/startup-action-control.ts +315 -0
  752. package/src/server/startup-health-cache.ts +131 -0
  753. package/src/server/system-env.ts +484 -0
  754. package/src/server/windows-tcp-drop.ts +184 -0
  755. package/src/server/windows-tray-control.ts +41 -0
  756. package/src/server/ws-bridge.ts +472 -0
  757. package/src/service-manager-probe.ts +892 -0
  758. package/src/service.ts +3575 -0
  759. package/src/sidecar/auth.ts +92 -0
  760. package/src/sidecar/candidates.ts +83 -0
  761. package/src/stall-timeout.ts +20 -0
  762. package/src/storage/cleanup-job.ts +57 -0
  763. package/src/storage/cleanup.ts +3085 -0
  764. package/src/storage/policy-job.ts +457 -0
  765. package/src/storage/policy-scheduler.ts +40 -0
  766. package/src/storage/policy-worker.ts +59 -0
  767. package/src/storage/policy.ts +527 -0
  768. package/src/storage/restore-job.ts +299 -0
  769. package/src/storage/restore-worker.ts +58 -0
  770. package/src/storage/scanner.ts +238 -0
  771. package/src/storage/storage-mutation-coordinator.ts +139 -0
  772. package/src/storage/worker-lifecycle.ts +215 -0
  773. package/src/tray/assets/opencodex-tray-offline.ico +0 -0
  774. package/src/tray/assets/opencodex-tray-online.ico +0 -0
  775. package/src/tray/assets/opencodex-tray-warning.ico +0 -0
  776. package/src/tray/assets/opencodex-tray.png +0 -0
  777. package/src/tray/windows-tray.ps1 +364 -0
  778. package/src/tray/windows.ts +757 -0
  779. package/src/types/accounts.ts +37 -0
  780. package/src/types/config.ts +876 -0
  781. package/src/types/provider.ts +545 -0
  782. package/src/types/request.ts +384 -0
  783. package/src/types/tools.ts +131 -0
  784. package/src/types/wire.ts +80 -0
  785. package/src/types.ts +106 -0
  786. package/src/update/badge.ts +72 -0
  787. package/src/update/index.ts +415 -0
  788. package/src/update/job.ts +1887 -0
  789. package/src/update/notify.ts +263 -0
  790. package/src/update/npm-cache-preflight.d.mts +47 -0
  791. package/src/update/npm-cache-preflight.mjs +201 -0
  792. package/src/update/npm-invocation.d.mts +23 -0
  793. package/src/update/npm-invocation.mjs +94 -0
  794. package/src/update/transactional-install.d.mts +22 -0
  795. package/src/update/transactional-install.mjs +259 -0
  796. package/src/update/tray-update-plan.d.mts +18 -0
  797. package/src/update/tray-update-plan.mjs +38 -0
  798. package/src/usage/cost.ts +625 -0
  799. package/src/usage/debug.ts +97 -0
  800. package/src/usage/expected-prices.ts +416 -0
  801. package/src/usage/log.ts +1223 -0
  802. package/src/usage/summary.ts +753 -0
  803. package/src/usage/totals.ts +14 -0
  804. package/src/usage/user-cost-overlay-reconciler.ts +313 -0
  805. package/src/usage/user-cost-overlays.ts +314 -0
  806. package/src/vision/anthropic-describe.ts +189 -0
  807. package/src/vision/backends.ts +97 -0
  808. package/src/vision/describe.ts +131 -0
  809. package/src/vision/eligibility.ts +250 -0
  810. package/src/vision/index.ts +681 -0
  811. package/src/vision/reasoning.ts +55 -0
  812. package/src/vision/routed-describe.ts +175 -0
  813. package/src/vision/timeout-bounds.ts +9 -0
  814. package/src/web-search/anthropic-executor.ts +195 -0
  815. package/src/web-search/backends.ts +108 -0
  816. package/src/web-search/exa-executor.ts +88 -0
  817. package/src/web-search/executor.ts +113 -0
  818. package/src/web-search/format-result.ts +89 -0
  819. package/src/web-search/gemini-executor.ts +141 -0
  820. package/src/web-search/index.ts +331 -0
  821. package/src/web-search/loop.ts +896 -0
  822. package/src/web-search/parse.ts +315 -0
  823. package/src/web-search/progress-stream.ts +342 -0
  824. package/src/web-search/sources.ts +60 -0
  825. package/src/web-search/synthetic-tool.ts +47 -0
  826. package/src/web-search/xai-executor.ts +219 -0
@@ -0,0 +1,1030 @@
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 { sseFieldValue } from "../lib/sse-decoder";
11
+ import { enforceAnthropicImageLimits, sniffImageDimensions } from "../adapters/anthropic-image-guard";
12
+ import { normalizeAnthropicImages } from "../adapters/anthropic-image-normalize";
13
+ import { AnthropicRequestError, anthropicToResponsesTranslation, extractOcxEffortDirective, extractOcxRouteDirective, resolveInboundModel, type ClaudeCacheKeySource } from "../claude/inbound";
14
+ import { resolveDesktop3pAlias } from "../claude/desktop-3p";
15
+ import { recordDesktopRequest } from "../claude/desktop-health";
16
+ import { stripOneMillionMarker } from "../claude/context-windows";
17
+ import { captureClaudeInbound } from "../claude/inbound-debug";
18
+ import { isTransientUpstreamStatus } from "../lib/upstream-retry";
19
+ import { resolveClientRetryAfter } from "../lib/retry-after";
20
+ import {
21
+ anthropicErrorBody,
22
+ anthropicErrorResponse,
23
+ collectAnthropicMessage,
24
+ responsesJsonToAnthropicMessage,
25
+ responsesSseToAnthropicSse,
26
+ } from "../claude/outbound";
27
+ import { clearableDeadline, idleDeadline } from "../lib/abort";
28
+ import { estimateTokens } from "../lib/token-estimate";
29
+ import { NoEligiblePolicyCandidateError, routeModel } from "../router";
30
+ import { evidenceFromBody } from "../routing/request-evidence";
31
+ import { resolveWireProtocolOverride } from "./adapter-resolve";
32
+ import type { OcxConfig } from "../types";
33
+ import { readJsonRequestBody } from "./request-decompress";
34
+ import { addFinalRequestLog, httpStatusForRequestLogTerminal, recordFirstOutput, type RequestLogContext, type RequestLogEntry } from "./request-log";
35
+ import { conversationIdFromClaudeMetadata } from "./request-log-conversation";
36
+ import { responseWithDeferredRequestLog } from "./relay";
37
+ import { handleResponses } from "./responses";
38
+ import {
39
+ isApiAuthRequired,
40
+ isDataPlaneAdmissionSecret,
41
+ isProxyAdmissionSecret,
42
+ type RequestPolicyView,
43
+ } from "./auth-cors";
44
+ import type { AdmissionLease } from "../lib/admission";
45
+ import { tryClaimNativeMainProfileForTurn } from "../codex/native-main-admission";
46
+ import { CODEX_MAIN_PROFILE_MAINTENANCE_MESSAGE } from "../codex/auth-context";
47
+ import {
48
+ createTranslatorBudget,
49
+ finalizeTranslatorBudgetResponse,
50
+ isTranslatorBudgetExceededError,
51
+ type TranslatorBudget,
52
+ } from "../lib/translator-budget";
53
+
54
+ type Rec = Record<string, unknown>;
55
+
56
+ function isRec(v: unknown): v is Rec {
57
+ return !!v && typeof v === "object" && !Array.isArray(v);
58
+ }
59
+
60
+ /** Resolve Claude-only sidecar overrides without mutating the shared server config. */
61
+ export function buildClaudeReplayConfig(config: OcxConfig): OcxConfig {
62
+ return {
63
+ ...config,
64
+ webSearchSidecar: {
65
+ ...config.webSearchSidecar,
66
+ ...config.claudeCode?.webSearchSidecar,
67
+ },
68
+ visionSidecar: {
69
+ ...config.visionSidecar,
70
+ ...config.claudeCode?.visionSidecar,
71
+ },
72
+ };
73
+ }
74
+
75
+ function claudeInboundDisabled(config: OcxConfig): Response | null {
76
+ if (config.claudeCode?.enabled === false) {
77
+ return anthropicErrorResponse(403, "Claude inbound is disabled (GUI: Claude ON toggle / config.claudeCode.enabled)", "permission_error");
78
+ }
79
+ return null;
80
+ }
81
+
82
+ async function readAnthropicBody(req: Request, budget: TranslatorBudget): Promise<unknown> {
83
+ try {
84
+ return await readJsonRequestBody(req, budget);
85
+ } catch (err) {
86
+ if (isTranslatorBudgetExceededError(err)) throw err;
87
+ throw new AnthropicRequestError(err instanceof Error && err.message ? err.message : "Invalid JSON body");
88
+ }
89
+ }
90
+
91
+ // ── Native Anthropic passthrough (subscription OAuth pierce) ──────────────────────
92
+ // When Claude Code runs with ONLY ANTHROPIC_BASE_URL set (subscription mode — the
93
+ // connectors warning stays off), it sends its OWN claude.ai OAuth Bearer to us.
94
+ // Requests for genuine claude/anthropic models that no alias/modelMap claims are
95
+ // forwarded VERBATIM to api.anthropic.com with the caller's credential and all
96
+ // end-to-end headers, so betas/thinking signatures/billing identity stay native.
97
+ // (Evidence: teamclaude --no-mitm + Vercel gateway docs, devlog 003/060.)
98
+
99
+ const PASSTHROUGH_STRIP_HEADERS = new Set([
100
+ "connection", "keep-alive", "transfer-encoding", "upgrade", "te", "trailer",
101
+ "proxy-authenticate", "proxy-authorization", "host", "content-length",
102
+ "accept-encoding", "x-opencodex-api-key", "origin",
103
+ ]);
104
+
105
+ function singleCredentialToken(name: "authorization" | "x-api-key", value: string | null): string | null {
106
+ const raw = value?.trim() ?? "";
107
+ // Fetch Headers comma-joins duplicate fields. Neither Anthropic credential format permits a
108
+ // comma, so treating a joined value as one token could hide an admission secret behind a real
109
+ // provider credential. Ambiguous credential headers fail closed.
110
+ if (!raw || raw.includes(",")) return null;
111
+ if (name === "authorization") {
112
+ const match = /^Bearer\s+(.+)$/i.exec(raw);
113
+ return match?.[1]?.trim() || null;
114
+ }
115
+ return raw;
116
+ }
117
+
118
+ function hasAnthropicNativeCredential(req: Request, config: OcxConfig): boolean {
119
+ const bearer = singleCredentialToken("authorization", req.headers.get("authorization"));
120
+ const apiKey = singleCredentialToken("x-api-key", req.headers.get("x-api-key"));
121
+ return (!!bearer && bearer.startsWith("sk-ant-") && !isProxyAdmissionSecret(bearer, config))
122
+ || (!!apiKey && apiKey.startsWith("sk-ant-") && !isProxyAdmissionSecret(apiKey, config));
123
+ }
124
+
125
+ function wantsNativePassthrough(
126
+ req: Request,
127
+ config: OcxConfig,
128
+ requestPolicy: RequestPolicyView,
129
+ model: unknown,
130
+ ): model is string {
131
+ if (config.claudeCode?.nativePassthrough === false) return false;
132
+ if (typeof model !== "string" || !/^(claude|anthropic)/i.test(model)) return false;
133
+ // Authorization and x-api-key both belong to the upstream on this branch. An exposed listener
134
+ // therefore requires the dedicated admission header even though the routed Messages surface
135
+ // keeps accepting all three legacy admission forms.
136
+ if (isApiAuthRequired(requestPolicy)) {
137
+ const dedicated = req.headers.get("x-opencodex-api-key")?.trim() ?? "";
138
+ if (!isDataPlaneAdmissionSecret(dedicated, config)) return false;
139
+ }
140
+ if (!hasAnthropicNativeCredential(req, config)) return false;
141
+ // An alias or modelMap hit means the user asked for a ROUTED model: translate instead.
142
+ return resolveInboundModel(model, config.claudeCode) === model;
143
+ }
144
+
145
+ function shouldForwardNativeHeader(name: string, value: string, config: OcxConfig): boolean {
146
+ const lowerName = name.toLowerCase();
147
+ if (PASSTHROUGH_STRIP_HEADERS.has(lowerName)) return false;
148
+ if (lowerName !== "authorization" && lowerName !== "x-api-key") return true;
149
+ const token = singleCredentialToken(lowerName, value);
150
+ return !!token && !isProxyAdmissionSecret(token, config);
151
+ }
152
+
153
+ /** Format a 32-hex cache key as a uuid-shaped session id (version/variant nibbles forced). */
154
+ function uuidFromHex(hex32: string): string {
155
+ const h = (hex32 + "0".repeat(32)).slice(0, 32);
156
+ return `${h.slice(0, 8)}-${h.slice(8, 12)}-4${h.slice(13, 16)}-8${h.slice(17, 20)}-${h.slice(20, 32)}`;
157
+ }
158
+
159
+ function anthropicUsageToOcx(usage: Rec | undefined): { inputTokens: number; outputTokens: number; cachedInputTokens?: number; cacheReadInputTokens?: number; cacheCreationInputTokens?: number } | undefined {
160
+ if (!usage) return undefined;
161
+ const num = (v: unknown) => typeof v === "number" ? v : 0;
162
+ const hasCache = usage.cache_read_input_tokens !== undefined || usage.cache_creation_input_tokens !== undefined;
163
+ const read = num(usage.cache_read_input_tokens);
164
+ const write = num(usage.cache_creation_input_tokens);
165
+ // Anthropic input_tokens excludes cache read/write; normalize to the canonical
166
+ // inclusive convention (types.ts OcxUsage / devlog 070). cached = READS only.
167
+ return {
168
+ inputTokens: num(usage.input_tokens) + read + write,
169
+ outputTokens: num(usage.output_tokens),
170
+ ...(hasCache ? {
171
+ cachedInputTokens: read,
172
+ cacheReadInputTokens: read,
173
+ cacheCreationInputTokens: write,
174
+ } : {}),
175
+ };
176
+ }
177
+
178
+ /** Body-occupancy guard for the native passthrough (devlog 260716_passthrough_followups/010). */
179
+ export interface PassthroughBodyGuard {
180
+ /** Idle window in ms — raw upstream-byte inactivity while a read is pending. 0 disables. */
181
+ stallMs: number;
182
+ /** Cumulative body byte cap. 0 disables. */
183
+ maxBytes: number;
184
+ /** Client request signal for deterministic cancel classification. */
185
+ reqSignal?: AbortSignal;
186
+ }
187
+
188
+ type PassthroughCloseReason = "terminal" | "client_cancel" | "body_stall" | "body_overflow";
189
+
190
+ /**
191
+ * Tap an Anthropic-vocabulary SSE stream for the request log (usage + terminal),
192
+ * bounding body occupancy: idle (silence-only, timed ONLY while a reader.read() is
193
+ * pending so downstream backpressure never counts as upstream inactivity) and a
194
+ * cumulative byte cap. On stall/overflow it appends a protocol-compatible Anthropic
195
+ * `event: error` terminal frame after a blank-line boundary, closes, and cancels the
196
+ * upstream reader — never a total-wall-clock bound (slow-but-alive streams live).
197
+ * Exported for deterministic unit tests.
198
+ */
199
+ export function tapAnthropicSseForLog(
200
+ upstream: ReadableStream<Uint8Array>,
201
+ logCtx: RequestLogContext,
202
+ finalize: (status: number, meta: { closeReason: PassthroughCloseReason }) => void,
203
+ guard?: PassthroughBodyGuard,
204
+ ): ReadableStream<Uint8Array> {
205
+ const decoder = new TextDecoder();
206
+ const encoder = new TextEncoder();
207
+ let buffer = "";
208
+ let usageAcc: Rec = {};
209
+ const inspect = (chunk: Uint8Array) => {
210
+ buffer += decoder.decode(chunk, { stream: true });
211
+ let sep: number;
212
+ while ((sep = buffer.indexOf("\n\n")) !== -1) {
213
+ const frame = buffer.slice(0, sep);
214
+ buffer = buffer.slice(sep + 2);
215
+ const dataLine = frame
216
+ .split("\n")
217
+ .map(l => sseFieldValue(l, "data"))
218
+ .filter((v): v is string => v !== null)
219
+ .join("");
220
+ if (!dataLine) continue;
221
+ let data: unknown;
222
+ try { data = JSON.parse(dataLine); } catch { continue; }
223
+ if (!isRec(data)) continue;
224
+ if (data.type === "message_start" && isRec(data.message) && isRec(data.message.usage)) {
225
+ usageAcc = { ...usageAcc, ...data.message.usage };
226
+ } else if (data.type === "message_delta" && isRec(data.usage)) {
227
+ usageAcc = { ...usageAcc, ...data.usage };
228
+ }
229
+ }
230
+ };
231
+ const reader = upstream.getReader();
232
+ let settled = false;
233
+ let bodyBytes = 0;
234
+ let tapController: ReadableStreamDefaultController<Uint8Array> | undefined;
235
+
236
+ const recordUsage = () => {
237
+ logCtx.usage = anthropicUsageToOcx(Object.keys(usageAcc).length > 0 ? usageAcc : undefined);
238
+ };
239
+ const failBody = (closeReason: "body_stall" | "body_overflow", errType: string, message: string) => {
240
+ if (settled) return;
241
+ settled = true;
242
+ idle.cancel();
243
+ detachAbort();
244
+ recordUsage();
245
+ finalize(200, { closeReason });
246
+ const payload = JSON.stringify({ type: "error", error: { type: errType, message } });
247
+ try {
248
+ // Leading blank line terminates any partial SSE block so the frame parses cleanly
249
+ // (relaySseWithFailedTail policy, Anthropic wire shape).
250
+ tapController?.enqueue(encoder.encode(`\n\nevent: error\ndata: ${payload}\n\n`));
251
+ tapController?.close();
252
+ } catch { /* client already torn down */ }
253
+ reader.cancel(new DOMException(message, closeReason === "body_stall" ? "TimeoutError" : "QuotaExceededError")).catch(() => {});
254
+ };
255
+ const idle = idleDeadline(guard?.stallMs ?? 0, () => {
256
+ failBody(
257
+ "body_stall",
258
+ "timeout_error",
259
+ `anthropic passthrough body stalled: no upstream bytes for ${Math.round((guard?.stallMs ?? 0) / 1000)}s`,
260
+ );
261
+ });
262
+ // Deterministic client-cancel classification: Bun may surface a client abort as a
263
+ // reader.read() rejection OR a resolved done (src/lib/abort.ts cancelBodyOnAbort
264
+ // rationale), so the listener performs first-wins settlement itself instead of
265
+ // relying on which shape the read takes.
266
+ const onClientAbort = () => {
267
+ if (settled) return;
268
+ settled = true;
269
+ idle.cancel();
270
+ detachAbort();
271
+ finalize(499, { closeReason: "client_cancel" });
272
+ try { tapController?.close(); } catch { /* downstream already torn down */ }
273
+ reader.cancel(guard?.reqSignal?.reason).catch(() => {});
274
+ };
275
+ const detachAbort = (() => {
276
+ const signal = guard?.reqSignal;
277
+ if (!signal) return () => {};
278
+ if (signal.aborted) {
279
+ queueMicrotask(onClientAbort);
280
+ return () => {};
281
+ }
282
+ signal.addEventListener("abort", onClientAbort, { once: true });
283
+ return () => signal.removeEventListener("abort", onClientAbort);
284
+ })();
285
+
286
+ return new ReadableStream<Uint8Array>({
287
+ start(controller) {
288
+ tapController = controller;
289
+ },
290
+ async pull(controller) {
291
+ if (settled) return;
292
+ try {
293
+ idle.reset();
294
+ const { done, value } = await reader.read();
295
+ idle.pause();
296
+ if (settled) return; // stall/overflow/abort won the race while we awaited
297
+ if (done) {
298
+ settled = true;
299
+ idle.cancel();
300
+ detachAbort();
301
+ recordUsage();
302
+ finalize(200, { closeReason: "terminal" });
303
+ controller.close();
304
+ return;
305
+ }
306
+ if (value.byteLength > 0) {
307
+ bodyBytes += value.byteLength;
308
+ if (guard && guard.maxBytes > 0 && bodyBytes > guard.maxBytes) {
309
+ failBody(
310
+ "body_overflow",
311
+ "api_error",
312
+ `anthropic passthrough body exceeded ${guard.maxBytes} bytes`,
313
+ );
314
+ return;
315
+ }
316
+ }
317
+ inspect(value);
318
+ controller.enqueue(value);
319
+ } catch (err) {
320
+ if (settled) return;
321
+ settled = true;
322
+ idle.cancel();
323
+ detachAbort();
324
+ recordUsage();
325
+ finalize(200, { closeReason: "terminal" });
326
+ try { controller.error(err); } catch { /* torn down */ }
327
+ }
328
+ },
329
+ cancel(reason) {
330
+ if (!settled) {
331
+ settled = true;
332
+ idle.cancel();
333
+ detachAbort();
334
+ finalize(499, { closeReason: "client_cancel" });
335
+ }
336
+ reader.cancel(reason).catch(() => {});
337
+ },
338
+ });
339
+ }
340
+
341
+ async function anthropicNativePassthrough(
342
+ req: Request,
343
+ config: OcxConfig,
344
+ logCtx: RequestLogContext,
345
+ logIds: { requestId: string; start: number } | undefined,
346
+ body: Rec,
347
+ pathname: string,
348
+ ): Promise<Response> {
349
+ const model = typeof body.model === "string" ? body.model : "unknown";
350
+ logCtx.model = model;
351
+ logCtx.provider = "anthropic-native";
352
+ logCtx.requestedModel = model;
353
+ let logged = false;
354
+ const finalize = (status: number, meta: { closeReason: PassthroughCloseReason | "non_stream" }) => {
355
+ if (!logIds || logged) return;
356
+ logged = true;
357
+ addFinalRequestLog(logIds.requestId, logIds.start, logCtx, status, meta);
358
+ };
359
+
360
+ const base = (config.claudeCode?.anthropicBaseUrl ?? "https://api.anthropic.com").replace(/\/$/, "");
361
+ const search = new URL(req.url).search;
362
+ // Native passthrough bypasses the anthropic adapter, so the generous image pipeline
363
+ // (devlog/260714_image_normalization_pipeline/040) must run here: tier-normalize then
364
+ // guard the already-Anthropic-wire messages before serialization. Applies to
365
+ // count_tokens too — counts must match what the real send will contain, and the 32MB
366
+ // body cap applies to it equally. Non-message bodies pass through untouched.
367
+ if (Array.isArray(body.messages)) {
368
+ await normalizeAnthropicImages(body.messages);
369
+ enforceAnthropicImageLimits(body.messages);
370
+ }
371
+ const headers = new Headers();
372
+ req.headers.forEach((value, name) => {
373
+ if (shouldForwardNativeHeader(name, value, config)) headers.set(name, value);
374
+ });
375
+ headers.set("content-type", "application/json");
376
+
377
+ const result = await fetchWithHeaderDeadline(
378
+ `${base}${pathname}${search}`,
379
+ { method: "POST", headers, body: JSON.stringify(body) },
380
+ config.connectTimeoutMs ?? 200_000,
381
+ req.signal,
382
+ );
383
+ if (result.kind === "timeout") {
384
+ finalize(504, { closeReason: "non_stream" });
385
+ return anthropicErrorResponse(504, "anthropic passthrough timed out waiting for response headers", "timeout_error");
386
+ }
387
+ if (result.kind === "error") {
388
+ const err = result.error;
389
+ finalize(502, { closeReason: "non_stream" });
390
+ return anthropicErrorResponse(502, `anthropic passthrough failed: ${err instanceof Error ? err.message : String(err)}`, "api_error");
391
+ }
392
+ const upstream = result.upstream;
393
+
394
+ const contentType = upstream.headers.get("content-type") ?? "application/json";
395
+ const bodyGuard = resolvePassthroughBodyGuard(config, req.signal);
396
+ if (upstream.ok && contentType.includes("text/event-stream") && upstream.body) {
397
+ return new Response(tapAnthropicSseForLog(upstream.body, logCtx, finalize, bodyGuard), {
398
+ status: upstream.status,
399
+ headers: {
400
+ "Content-Type": contentType,
401
+ "Cache-Control": "no-cache",
402
+ "Connection": "keep-alive",
403
+ },
404
+ });
405
+ }
406
+ // Non-stream (count_tokens, errors, stream:false): relay verbatim under the same
407
+ // idle/size bounds — headers are NOT yet sent here, so real statuses are available.
408
+ const bodyResult = await readBoundedPassthroughBody(upstream, bodyGuard);
409
+ if (bodyResult.kind === "client_cancel") {
410
+ finalize(499, { closeReason: "client_cancel" });
411
+ return anthropicErrorResponse(499, "client closed request during anthropic passthrough", "api_error");
412
+ }
413
+ if (bodyResult.kind === "stall") {
414
+ finalize(504, { closeReason: "body_stall" });
415
+ return anthropicErrorResponse(504, `anthropic passthrough body stalled: no upstream bytes for ${Math.round(bodyGuard.stallMs / 1000)}s`, "timeout_error");
416
+ }
417
+ if (bodyResult.kind === "overflow") {
418
+ finalize(502, { closeReason: "body_overflow" });
419
+ return anthropicErrorResponse(502, `anthropic passthrough body exceeded ${bodyGuard.maxBytes} bytes`, "api_error");
420
+ }
421
+ const text = bodyResult.text;
422
+ if (upstream.ok) {
423
+ try {
424
+ const parsed = JSON.parse(text) as { usage?: Rec };
425
+ if (isRec(parsed?.usage)) logCtx.usage = anthropicUsageToOcx(parsed.usage);
426
+ } catch { /* count_tokens etc. */ }
427
+ }
428
+ finalize(upstream.status, { closeReason: "non_stream" });
429
+ const retryAfter = upstream.headers.get("retry-after");
430
+ return new Response(text, {
431
+ status: upstream.status,
432
+ headers: { "Content-Type": contentType, ...(retryAfter ? { "Retry-After": retryAfter } : {}) },
433
+ });
434
+ }
435
+
436
+ const DEFAULT_BODY_STALL_SEC = 90;
437
+ const DEFAULT_BODY_MAX_BYTES = 64 * 1024 * 1024;
438
+
439
+ /**
440
+ * Normalize the claudeCode body-guard config (devlog 260716_passthrough_followups/010).
441
+ * Policy: exactly 0 disables; finite positive values are honored (stall clamped to
442
+ * min 1s); negative/non-finite/absent values fall back to the defaults.
443
+ */
444
+ export function resolvePassthroughBodyGuard(config: OcxConfig, reqSignal?: AbortSignal): PassthroughBodyGuard {
445
+ const rawSec = config.claudeCode?.bodyStallSec;
446
+ const stallSec = rawSec === 0
447
+ ? 0
448
+ : typeof rawSec === "number" && Number.isFinite(rawSec) && rawSec > 0
449
+ ? Math.max(1, rawSec)
450
+ : DEFAULT_BODY_STALL_SEC;
451
+ const rawBytes = config.claudeCode?.bodyMaxBytes;
452
+ const maxBytes = rawBytes === 0
453
+ ? 0
454
+ : typeof rawBytes === "number" && Number.isFinite(rawBytes) && rawBytes > 0
455
+ ? Math.floor(rawBytes)
456
+ : DEFAULT_BODY_MAX_BYTES;
457
+ return { stallMs: stallSec * 1000, maxBytes, ...(reqSignal ? { reqSignal } : {}) };
458
+ }
459
+
460
+ type BoundedPassthroughBody =
461
+ | { kind: "ok"; text: string }
462
+ | { kind: "stall" }
463
+ | { kind: "overflow" }
464
+ | { kind: "client_cancel" };
465
+
466
+ /**
467
+ * Bounded replacement for `await upstream.text()` on the non-stream passthrough
468
+ * branch: same idle-only + size-cap semantics as the SSE tap. NOTE: reader.cancel()
469
+ * resolves a pending read as done rather than rejecting, so the stalled flag is
470
+ * re-checked after every read settlement (audit round 3).
471
+ */
472
+ export async function readBoundedPassthroughBody(
473
+ upstream: Response,
474
+ guard: PassthroughBodyGuard,
475
+ ): Promise<BoundedPassthroughBody> {
476
+ if (!upstream.body) return { kind: "ok", text: await upstream.text() };
477
+ const reader = upstream.body.getReader();
478
+ const decoder = new TextDecoder();
479
+ let text = "";
480
+ let bytes = 0;
481
+ let stalled = false;
482
+ let aborted = false;
483
+ const idle = idleDeadline(guard.stallMs, () => {
484
+ stalled = true;
485
+ reader.cancel(new DOMException("anthropic passthrough body stalled", "TimeoutError")).catch(() => {});
486
+ });
487
+ // Deterministic client-abort classification (audit round 4): Bun may surface the
488
+ // abort as a read rejection OR a resolved done, so we cancel the reader ourselves
489
+ // and classify via the flag rather than the read's settlement shape.
490
+ const signal = guard.reqSignal;
491
+ const onAbort = () => {
492
+ aborted = true;
493
+ reader.cancel(signal?.reason).catch(() => {});
494
+ };
495
+ if (signal?.aborted) onAbort();
496
+ else signal?.addEventListener("abort", onAbort, { once: true });
497
+ try {
498
+ while (true) {
499
+ idle.reset();
500
+ let result: Awaited<ReturnType<typeof reader.read>>;
501
+ try {
502
+ result = await reader.read();
503
+ } catch (err) {
504
+ if (aborted) return { kind: "client_cancel" };
505
+ if (stalled) return { kind: "stall" };
506
+ throw err;
507
+ } finally {
508
+ idle.pause();
509
+ }
510
+ if (aborted) return { kind: "client_cancel" };
511
+ if (stalled) return { kind: "stall" };
512
+ if (result.done) break;
513
+ if (result.value.byteLength === 0) continue;
514
+ bytes += result.value.byteLength;
515
+ if (guard.maxBytes > 0 && bytes > guard.maxBytes) {
516
+ reader.cancel(new DOMException("anthropic passthrough body exceeded byte cap", "QuotaExceededError")).catch(() => {});
517
+ return { kind: "overflow" };
518
+ }
519
+ text += decoder.decode(result.value, { stream: true });
520
+ }
521
+ text += decoder.decode();
522
+ return { kind: "ok", text };
523
+ } finally {
524
+ idle.cancel();
525
+ signal?.removeEventListener("abort", onAbort);
526
+ }
527
+ }
528
+
529
+ /**
530
+ * Header-phase fetch guarded by a clearable deadline (PR #136 follow-up hardening).
531
+ *
532
+ * The deadline covers ONLY the wait for response headers; once `fetch` settles —
533
+ * fulfilled OR rejected — the timer must die. The `finally` block guarantees
534
+ * `clear()` on every path (success, upstream reject, deadline expiry), fixing the
535
+ * timer leak where a rejected fetch left the deadline running until expiry.
536
+ * `didExpire()` stays truthful after `clear()` (see src/lib/abort.ts), so timeout
537
+ * classification inside the catch is unaffected by the finally cleanup.
538
+ *
539
+ * `makeDeadline`/`fetchImpl` are injectable for deterministic unit tests.
540
+ */
541
+ export type HeaderDeadlineFetchResult =
542
+ | { kind: "response"; upstream: Response }
543
+ | { kind: "timeout" }
544
+ | { kind: "error"; error: unknown };
545
+
546
+ export async function fetchWithHeaderDeadline(
547
+ input: string | URL,
548
+ init: RequestInit,
549
+ timeoutMs: number,
550
+ parent?: AbortSignal,
551
+ makeDeadline: typeof clearableDeadline = clearableDeadline,
552
+ fetchImpl: typeof fetch = fetch,
553
+ ): Promise<HeaderDeadlineFetchResult> {
554
+ const deadline = makeDeadline(timeoutMs, parent);
555
+ try {
556
+ const upstream = await fetchImpl(input, { ...init, signal: deadline.signal });
557
+ return { kind: "response", upstream };
558
+ } catch (error) {
559
+ if (deadline.didExpire()) return { kind: "timeout" };
560
+ return { kind: "error", error };
561
+ } finally {
562
+ deadline.clear();
563
+ }
564
+ }
565
+
566
+ export async function handleClaudeMessages(
567
+ req: Request,
568
+ config: OcxConfig,
569
+ logCtx: RequestLogContext,
570
+ logIds?: { requestId: string; start: number; turnAdmissionLease?: AdmissionLease },
571
+ requestPolicy: RequestPolicyView = config,
572
+ ): Promise<Response> {
573
+ const translatorBudget = createTranslatorBudget();
574
+ try {
575
+ return finalizeTranslatorBudgetResponse(
576
+ await handleClaudeMessagesWithBudget(req, config, logCtx, translatorBudget, logIds, requestPolicy),
577
+ translatorBudget,
578
+ );
579
+ } catch (error) {
580
+ translatorBudget.dispose();
581
+ throw error;
582
+ }
583
+ }
584
+
585
+ async function handleClaudeMessagesWithBudget(
586
+ req: Request,
587
+ config: OcxConfig,
588
+ logCtx: RequestLogContext,
589
+ translatorBudget: TranslatorBudget,
590
+ logIds?: { requestId: string; start: number; turnAdmissionLease?: AdmissionLease },
591
+ requestPolicy: RequestPolicyView = config,
592
+ ): Promise<Response> {
593
+ logCtx.surface = "claude";
594
+ const disabled = claudeInboundDisabled(config);
595
+ if (disabled) {
596
+ if (logIds) addFinalRequestLog(logIds.requestId, logIds.start, logCtx, 403, { closeReason: "non_stream" });
597
+ return disabled;
598
+ }
599
+
600
+ let anthropicBody: unknown;
601
+ let internalBody: Rec;
602
+ let cacheKeySource: ClaudeCacheKeySource = null;
603
+ let effortOverride: ReturnType<typeof extractOcxEffortDirective> = null;
604
+ try {
605
+ anthropicBody = await readAnthropicBody(req, translatorBudget);
606
+ // Defensive [1m] strip (devlog 138): clients normally remove the context-variant
607
+ // marker themselves; the 1M signal we act on is the anthropic-beta header.
608
+ // Case-insensitive — the CLI matches /\[1m\]/i (audit 021 #7).
609
+ if (isRec(anthropicBody) && typeof anthropicBody.model === "string") {
610
+ anthropicBody.model = stripOneMillionMarker(anthropicBody.model);
611
+ }
612
+ // ocx-route override (devlog 072): injected agent bodies pin their model via a
613
+ // system-prompt directive because 2.1.207 ignores custom ids in agent
614
+ // frontmatter. Must run BEFORE the native-passthrough branch — the CLI sends
615
+ // these subagent turns under a fallback claude model id.
616
+ if (isRec(anthropicBody)) {
617
+ const routeOverride = extractOcxRouteDirective(anthropicBody);
618
+ if (routeOverride && typeof anthropicBody.model === "string") {
619
+ anthropicBody.model = stripOneMillionMarker(routeOverride);
620
+ effortOverride = extractOcxEffortDirective(anthropicBody);
621
+ }
622
+ }
623
+ // Debug capture (opt-in allowlist scalars) BEFORE the passthrough branch so
624
+ // native, routed, and disabled-alias paths are all observable (devlog 130 B1).
625
+ captureClaudeInbound(
626
+ "messages",
627
+ anthropicBody,
628
+ isRec(anthropicBody) && typeof anthropicBody.model === "string"
629
+ ? resolveInboundModel(anthropicBody.model, config.claudeCode)
630
+ : undefined,
631
+ req.headers.get("anthropic-beta") ?? undefined,
632
+ );
633
+ // Client surface discrimination: Desktop 3P aliases resolve through the
634
+ // desktop registry; Code uses readable aliases or direct model names.
635
+ if (isRec(anthropicBody) && typeof anthropicBody.model === "string" && resolveDesktop3pAlias(anthropicBody.model)) {
636
+ logCtx.surface = "claude-desktop";
637
+ recordDesktopRequest();
638
+ }
639
+ // Correlate before native passthrough so Anthropic-credential turns still filter/total (#330 / #522).
640
+ if (isRec(anthropicBody)) {
641
+ const claudeConversationId = conversationIdFromClaudeMetadata(
642
+ isRec(anthropicBody.metadata) ? anthropicBody.metadata : undefined,
643
+ );
644
+ if (claudeConversationId) logCtx.conversationId = claudeConversationId;
645
+ }
646
+ if (isRec(anthropicBody) && wantsNativePassthrough(req, config, requestPolicy, anthropicBody.model)) {
647
+ return await anthropicNativePassthrough(req, config, logCtx, logIds, anthropicBody, "/v1/messages");
648
+ }
649
+ if (isRec(anthropicBody) && effortOverride) {
650
+ anthropicBody.output_config = {
651
+ ...(isRec(anthropicBody.output_config) ? anthropicBody.output_config : {}),
652
+ effort: effortOverride,
653
+ };
654
+ delete anthropicBody.thinking;
655
+ }
656
+ const translation = anthropicToResponsesTranslation(anthropicBody, config.claudeCode);
657
+ internalBody = translation.body;
658
+ translatorBudget.chargeRetained(new TextEncoder().encode(JSON.stringify(internalBody)).byteLength, { kind: "request_copies" });
659
+ cacheKeySource = translation.cacheKeySource;
660
+ } catch (err) {
661
+ const overflow = isTranslatorBudgetExceededError(err);
662
+ const status = overflow ? 413 : err instanceof AnthropicRequestError ? 400 : 500;
663
+ if (logIds) addFinalRequestLog(logIds.requestId, logIds.start, logCtx, status, { closeReason: "non_stream" });
664
+ return anthropicErrorResponse(
665
+ status,
666
+ overflow ? "request translation buffer exceeded the safe limit" : err instanceof Error ? err.message : String(err),
667
+ overflow ? "request_too_large" : undefined,
668
+ overflow ? "translation_buffer_limit" : undefined,
669
+ );
670
+ }
671
+
672
+ const requestedModel = (anthropicBody as Rec).model as string;
673
+ const stream = internalBody.stream === true;
674
+ // Routed adapters only support streamed turns; always stream internally and fold
675
+ // the translated Anthropic SSE into a message JSON for non-streaming clients.
676
+ internalBody.stream = true;
677
+
678
+ // Native ChatGPT passthrough (openai-responses forward) accepts only Codex-shaped
679
+ // bodies: it 400s on sampling params ("Unsupported parameter: max_output_tokens",
680
+ // verified live 2026-07-11). Strip them for that route; routed providers keep them.
681
+ let nativeRoute = false;
682
+ try {
683
+ const route = routeModel(config, internalBody.model as string, evidenceFromBody(internalBody));
684
+ // Settle the wire once so the sampling decision below reads the effective
685
+ // adapter rather than the provider-wide default (#404).
686
+ route.provider = resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, "anthropic");
687
+ logCtx.routeDecision = route.routeDecision;
688
+ if (route.provider.adapter === "openai-responses") {
689
+ nativeRoute = true;
690
+ delete internalBody.max_output_tokens;
691
+ delete internalBody.temperature;
692
+ delete internalBody.top_p;
693
+ delete internalBody.stop;
694
+ delete internalBody.user;
695
+ }
696
+ // Estimated-usage adapters (cursor/kiro) report no per-turn input tokens; stash a
697
+ // request-side estimate so the log's in:0 rows get a floor. NEVER set this for
698
+ // accurate-usage adapters — the request-log merge is max(reported, estimate) and
699
+ // would overwrite real usage (audit 133 R1#7).
700
+ if (route.provider.adapter === "cursor" || route.provider.adapter === "kiro") {
701
+ logCtx.usageLogInputTokens = estimateClaudeRequestTokens(anthropicBody as Rec, requestedModel);
702
+ }
703
+ // Effort safety valve (devlog 136 B6, audit 139 R2#2): opus-shaped aliases make
704
+ // every routed model look like a reasoning model to Claude clients, so a forced
705
+ // effort (CLAUDE_CODE_ALWAYS_ENABLE_EFFORT) would leak reasoning params to routes
706
+ // that affirmatively expose NO effort control. Strip only on a definitive [] from
707
+ // supportedLadderFor; unknown (undefined) passes through untouched.
708
+ if (internalBody.reasoning !== undefined) {
709
+ const { supportedLadderFor } = await import("./effort-policy");
710
+ const ladder = supportedLadderFor({ provider: route.provider, modelId: route.modelId });
711
+ if (ladder !== undefined && ladder.length === 0) delete internalBody.reasoning;
712
+ }
713
+ } catch (err) {
714
+ if (err instanceof NoEligiblePolicyCandidateError) {
715
+ logCtx.routeDecision = err.trace;
716
+ if (logIds) addFinalRequestLog(logIds.requestId, logIds.start, logCtx, 404, { closeReason: "non_stream" });
717
+ return anthropicErrorResponse(404, err.message, "invalid_request_error");
718
+ }
719
+ /* unknown model: let handleResponses shape the 404 */
720
+ }
721
+
722
+ const headers = new Headers({ "content-type": "application/json" });
723
+ for (const name of FORWARD_HEADERS) {
724
+ // The caller's bearer is the proxy admission token (ocx claude placeholder), never a
725
+ // ChatGPT credential — forwarding it upstream turns into {"detail":"Unauthorized"}.
726
+ if (name === "authorization") continue;
727
+ const value = req.headers.get(name);
728
+ if (value) headers.set(name, value);
729
+ }
730
+ // Routed replays need main ChatGPT auth so OpenAI-backed sidecars remain reachable;
731
+ // native replays have no caller ChatGPT credential. This enrichment is optional:
732
+ // auth-context later rejects a real physical-main selection, while routed/pool
733
+ // traffic continues without reading native credentials during a fence/recovery.
734
+ if (tryClaimNativeMainProfileForTurn(logIds?.turnAdmissionLease)) {
735
+ const { getMainAccountToken } = await import("../codex/main-account");
736
+ const token = getMainAccountToken();
737
+ if (token) {
738
+ headers.set("authorization", `Bearer ${token.accessToken}`);
739
+ headers.set("chatgpt-account-id", token.chatgptAccountId);
740
+ }
741
+ }
742
+ if (nativeRoute) {
743
+ // ChatGPT-backend prompt-cache affinity rides the session_id HEADER (codex
744
+ // clients always send their session uuid; devlog 090 follow-up: body-level
745
+ // prompt_cache_key alone still yielded cached_tokens:0). Claude Code never sends
746
+ // the header, so synthesize a stable per-session uuid from the same cache key —
747
+ // but ONLY for a real per-session key (metadata.user_id). The system-hash fallback
748
+ // key is shared across Desktop conversations, and a shared session_id's backend
749
+ // semantics are unproven (audit 133 R2#3): body prompt_cache_key only there.
750
+ if (cacheKeySource === "metadata" && !headers.has("session_id") && typeof internalBody.prompt_cache_key === "string") {
751
+ headers.set("session_id", uuidFromHex(internalBody.prompt_cache_key));
752
+ }
753
+ }
754
+ const internalBodyJson = JSON.stringify(internalBody);
755
+ translatorBudget.chargeRetained(new TextEncoder().encode(internalBodyJson).byteLength, { kind: "request_copies" });
756
+ const internalReq = new Request("http://localhost/v1/responses", {
757
+ method: "POST",
758
+ headers,
759
+ body: internalBodyJson,
760
+ });
761
+
762
+ // Request-log wiring mirrors the /v1/responses route: native passthrough finalizes
763
+ // via the terminal callbacks; routed streams get the Responses-vocabulary log tap
764
+ // BEFORE translation (the translated Anthropic stream has no response.completed
765
+ // frame, so tapping it records a bogus 502 with no usage/cache detail).
766
+ let nativeLogged = false;
767
+ const finalizeNativeLog = (status: number, meta: { terminalStatus?: RequestLogEntry["terminalStatus"]; closeReason: "terminal" | "client_cancel" }) => {
768
+ if (!logIds || nativeLogged) return;
769
+ nativeLogged = true;
770
+ addFinalRequestLog(logIds.requestId, logIds.start, logCtx, status, meta);
771
+ };
772
+ const upstream = await handleResponses(internalReq, buildClaudeReplayConfig(config), logCtx, {
773
+ ...(logIds?.turnAdmissionLease ? { turnAdmissionLease: logIds.turnAdmissionLease } : {}),
774
+ abortSignal: req.signal,
775
+ promptCacheKeyIsSharedCohort: cacheKeySource === "system",
776
+ // The body is Responses-shaped by now, but the client spoke Anthropic Messages.
777
+ // Without this the replay would look native and a Responses-scoped wire default
778
+ // would fire, disagreeing with the pre-flight decision above.
779
+ inboundWire: "anthropic",
780
+ stripClaudeMainAuthForNoncanonicalForward: true,
781
+ translatorBudget,
782
+ ...(logIds ? { onFirstOutput: () => recordFirstOutput(logCtx, logIds.start) } : {}),
783
+ onNativePassthroughTerminal: status => finalizeNativeLog(httpStatusForRequestLogTerminal(status, logCtx), { terminalStatus: status, closeReason: "terminal" }),
784
+ onNativePassthroughCancel: () => finalizeNativeLog(499, { closeReason: "client_cancel" }),
785
+ });
786
+ const response = logIds ? responseWithDeferredRequestLog(upstream, logIds.requestId, logIds.start, logCtx) : upstream;
787
+
788
+ if (!response.ok) {
789
+ // Re-shape the OpenAI-style error envelope into the Anthropic one, preserving status.
790
+ let message = `upstream error (${response.status})`;
791
+ try {
792
+ const text = await response.text();
793
+ try {
794
+ const parsed = JSON.parse(text) as { error?: { message?: string; type?: string } | string; message?: string };
795
+ const nested = typeof parsed?.error === "object" && parsed.error ? parsed.error.message : undefined;
796
+ const flat = typeof parsed?.error === "string" ? parsed.error : parsed?.message;
797
+ message = nested || flat || (text ? `upstream error (${response.status}): ${text.slice(0, 400)}` : message);
798
+ } catch {
799
+ if (text) message = `upstream error (${response.status}): ${text.slice(0, 400)}`;
800
+ }
801
+ } catch { /* keep fallback message */ }
802
+ const upstreamRetryAfter = response.headers.get("retry-after");
803
+ const retryAfter = resolveClientRetryAfter({
804
+ status: response.status,
805
+ message,
806
+ upstreamRetryAfter,
807
+ })
808
+ // Instant-retry "0" is a valid client directive but rejected by cooldown parsers.
809
+ // Preserve it so it still wins over the transient "2" fallback (claude-529 mapping).
810
+ ?? (upstreamRetryAfter?.trim() === "0" ? "0" : undefined);
811
+ // Transient upstream 5xx (already retried pre-stream, 010): reclassify as Anthropic
812
+ // 529 overloaded_error so the Claude Code client applies its built-in backoff retry
813
+ // instead of dying on a fatal api_error (260716 sol-builder incident). The request
814
+ // log keeps the upstream status (captured in the deferred-log closure before this
815
+ // rewrite): log = upstream truth, client = retry signal.
816
+ // Retryable 429s also get Retry-After (#507) so Codex-shaped clients and Claude Code
817
+ // share a backoff hint when the upstream omitted the header.
818
+ const nativeMainFence = response.status === 503
819
+ && upstreamRetryAfter?.trim() === "1"
820
+ && message === CODEX_MAIN_PROFILE_MAINTENANCE_MESSAGE;
821
+ const transient = !nativeMainFence && isTransientUpstreamStatus(response.status);
822
+ const outStatus = nativeMainFence ? 503 : transient ? 529 : response.status;
823
+ const out = new Response(JSON.stringify(anthropicErrorBody(outStatus, message)), {
824
+ status: outStatus,
825
+ headers: {
826
+ "Content-Type": "application/json",
827
+ ...(retryAfter ? { "Retry-After": retryAfter } : (transient ? { "Retry-After": "2" } : {})),
828
+ },
829
+ });
830
+ return out;
831
+ }
832
+
833
+ const contentType = response.headers.get("content-type") ?? "";
834
+ if (contentType.includes("text/event-stream") && response.body) {
835
+ const anthropicSse = responsesSseToAnthropicSse(response.body, requestedModel, { translatorBudget });
836
+ if (stream) {
837
+ return new Response(anthropicSse, {
838
+ status: 200,
839
+ headers: {
840
+ "Content-Type": "text/event-stream; charset=utf-8",
841
+ "Cache-Control": "no-cache",
842
+ "Connection": "keep-alive",
843
+ },
844
+ });
845
+ }
846
+ let message: Rec;
847
+ try {
848
+ message = await collectAnthropicMessage(anthropicSse, requestedModel, translatorBudget);
849
+ } catch (error) {
850
+ if (isTranslatorBudgetExceededError(error)) {
851
+ return anthropicErrorResponse(413, error.message, "request_too_large", error.code);
852
+ }
853
+ return anthropicErrorResponse(502, error instanceof Error ? error.message : String(error), "api_error");
854
+ }
855
+ const isError = (message as Rec).type === "error";
856
+ const translatedError = isError && typeof (message as Rec).error === "object"
857
+ ? (message as { error: { code?: unknown; message?: unknown } }).error
858
+ : undefined;
859
+ if (translatedError?.code === "translation_buffer_limit") {
860
+ return anthropicErrorResponse(
861
+ 413,
862
+ typeof translatedError.message === "string"
863
+ ? translatedError.message
864
+ : "upstream translation buffer exceeded the safe limit",
865
+ "request_too_large",
866
+ "translation_buffer_limit",
867
+ );
868
+ }
869
+ return new Response(JSON.stringify(message), {
870
+ status: isError ? 502 : 200,
871
+ headers: { "Content-Type": "application/json" },
872
+ });
873
+ }
874
+
875
+ // Defensive: some passthrough paths may answer JSON despite stream:true.
876
+ let json: unknown;
877
+ try {
878
+ json = await response.json();
879
+ } catch {
880
+ return anthropicErrorResponse(502, "internal replay returned a non-JSON response", "api_error");
881
+ }
882
+ const status = (json as Rec)?.status;
883
+ if (status === "failed") {
884
+ const error = (json as { error?: { message?: string; code?: string } }).error;
885
+ if (error?.code === "translation_buffer_limit") {
886
+ return anthropicErrorResponse(
887
+ 413,
888
+ error.message ?? "upstream translation buffer exceeded the safe limit",
889
+ "request_too_large",
890
+ "translation_buffer_limit",
891
+ );
892
+ }
893
+ return anthropicErrorResponse(502, error?.message ?? "upstream request failed", "api_error");
894
+ }
895
+ const message = responsesJsonToAnthropicMessage(json, requestedModel);
896
+ if ((message as Rec).type === "error") {
897
+ return new Response(JSON.stringify(message), {
898
+ status: 529,
899
+ headers: { "Content-Type": "application/json", "Retry-After": "2" },
900
+ });
901
+ }
902
+ if (!stream) {
903
+ return new Response(JSON.stringify(message), { status: 200, headers: { "Content-Type": "application/json" } });
904
+ }
905
+ // Streaming client + JSON upstream: synthesize a minimal valid Anthropic stream.
906
+ const encoder = new TextEncoder();
907
+ const frames: string[] = [];
908
+ const emit = (name: string, data: Rec) => frames.push(`event: ${name}\ndata: ${JSON.stringify(data)}\n\n`);
909
+ emit("message_start", { type: "message_start", message: { ...message, content: [], stop_reason: null, usage: { input_tokens: 0, output_tokens: 0 } } });
910
+ const blocks = Array.isArray((message as Rec).content) ? (message as Rec).content as Rec[] : [];
911
+ blocks.forEach((block, index) => {
912
+ emit("content_block_start", { type: "content_block_start", index, content_block: block });
913
+ emit("content_block_stop", { type: "content_block_stop", index });
914
+ });
915
+ emit("message_delta", { type: "message_delta", delta: { stop_reason: (message as Rec).stop_reason ?? "end_turn", stop_sequence: null }, usage: (message as Rec).usage ?? {} });
916
+ emit("message_stop", { type: "message_stop" });
917
+ return new Response(encoder.encode(frames.join("")), {
918
+ status: 200,
919
+ headers: { "Content-Type": "text/event-stream; charset=utf-8", "Cache-Control": "no-cache" },
920
+ });
921
+ }
922
+
923
+ /** Per-attachment token estimate for a base64 payload: real image dimensions when the
924
+ * header is sniffable (Anthropic prices images at ~pixels/750), else decoded bytes/512,
925
+ * min 256 — the same shape as the Kiro usage estimator (estimateKiroImageTokens). */
926
+ function estimateBase64AttachmentTokens(data: string): number {
927
+ const dims = sniffImageDimensions(data);
928
+ if (dims) return Math.max(256, Math.ceil((dims.width * dims.height) / 750));
929
+ const unpadded = data.endsWith("==") ? data.length - 2 : data.endsWith("=") ? data.length - 1 : data.length;
930
+ return Math.max(256, Math.ceil(Math.floor((unpadded * 3) / 4) / 512));
931
+ }
932
+
933
+ /**
934
+ * Char-based token estimate for an Anthropic-shaped request body. Base64 attachment
935
+ * payloads (image/document blocks in message content, including blocks nested in
936
+ * tool_result.content) are counted as a bounded per-attachment estimate instead of raw
937
+ * characters: one 2MB screenshot is ~2.7M base64 chars, which the plain chars/token
938
+ * divide reports as hundreds of thousands of tokens versus a real cost around 1.6k.
939
+ * That breaks the >2x drift bound the estimator is held to (devlog 260711_claude_inbound
940
+ * 040 §3). Text and url sources are left in place and counted as characters, as is
941
+ * anything outside protocol content positions (tool_use.input, tool schemas).
942
+ */
943
+ export function estimateClaudeRequestTokens(
944
+ raw: { system?: unknown; messages?: unknown; tools?: unknown },
945
+ modelId: string | undefined,
946
+ ): number {
947
+ let attachmentTokens = 0;
948
+ // Blank base64 payloads ONLY in protocol content positions: message content blocks and
949
+ // blocks nested in tool_result.content. tool_use.input and tool schemas can legitimately
950
+ // contain attachment-shaped JSON, and those bytes ARE serialized into function_call
951
+ // arguments / tool definitions for routed providers, so they must keep counting as text.
952
+ // system is text-only per the Anthropic protocol (no attachment sources), so it is
953
+ // stringified as-is.
954
+ const sanitizeBlock = (block: unknown): unknown => {
955
+ if (!block || typeof block !== "object") return block;
956
+ const b = block as Record<string, unknown>;
957
+ if (b.type === "image" || b.type === "document") {
958
+ const source = b.source as { type?: unknown; data?: unknown } | undefined;
959
+ if (source && typeof source === "object" && source.type === "base64" && typeof source.data === "string") {
960
+ attachmentTokens += estimateBase64AttachmentTokens(source.data);
961
+ return { ...b, source: { ...(source as Record<string, unknown>), data: "" } };
962
+ }
963
+ return block;
964
+ }
965
+ if (b.type === "tool_result" && Array.isArray(b.content)) {
966
+ return { ...b, content: (b.content as unknown[]).map(sanitizeBlock) };
967
+ }
968
+ return block;
969
+ };
970
+ const sanitizedMessages = (messages: unknown): unknown =>
971
+ Array.isArray(messages)
972
+ ? messages.map(message => {
973
+ if (!message || typeof message !== "object") return message;
974
+ const m = message as Record<string, unknown>;
975
+ return Array.isArray(m.content) ? { ...m, content: (m.content as unknown[]).map(sanitizeBlock) } : message;
976
+ })
977
+ : messages;
978
+ const parts: string[] = [];
979
+ if (raw.system !== undefined) parts.push(typeof raw.system === "string" ? raw.system : JSON.stringify(raw.system));
980
+ if (raw.messages !== undefined) parts.push(JSON.stringify(sanitizedMessages(raw.messages)));
981
+ if (raw.tools !== undefined) parts.push(JSON.stringify(raw.tools));
982
+ return Math.max(1, estimateTokens(parts.join("\n"), modelId) + attachmentTokens);
983
+ }
984
+
985
+ export async function handleClaudeCountTokens(
986
+ req: Request,
987
+ config: OcxConfig,
988
+ requestPolicy: RequestPolicyView = config,
989
+ ): Promise<Response> {
990
+ const disabled = claudeInboundDisabled(config);
991
+ if (disabled) return disabled;
992
+
993
+ let body: unknown;
994
+ const translatorBudget = createTranslatorBudget();
995
+ try {
996
+ body = await readAnthropicBody(req, translatorBudget);
997
+ } catch (err) {
998
+ if (err instanceof AnthropicRequestError) return anthropicErrorResponse(400, err.message);
999
+ return anthropicErrorResponse(500, err instanceof Error ? err.message : String(err));
1000
+ } finally { translatorBudget.dispose(); }
1001
+ if (!body || typeof body !== "object" || Array.isArray(body)) {
1002
+ return anthropicErrorResponse(400, "request body must be a JSON object");
1003
+ }
1004
+ const raw = body as Rec;
1005
+ if (typeof raw.model !== "string" || raw.model.length === 0) {
1006
+ return anthropicErrorResponse(400, "model is required");
1007
+ }
1008
+ let model = raw.model;
1009
+ // Case-insensitive [1m] strip (audit 021 #7 — the CLI matches /\[1m\]/i).
1010
+ const stripped = stripOneMillionMarker(model);
1011
+ if (stripped !== model) {
1012
+ model = stripped;
1013
+ raw.model = model;
1014
+ }
1015
+ // ocx-route override (devlog 072): keep count_tokens consistent with messages.
1016
+ const countRoute = extractOcxRouteDirective(raw);
1017
+ if (countRoute) {
1018
+ model = stripOneMillionMarker(countRoute);
1019
+ raw.model = model;
1020
+ }
1021
+ captureClaudeInbound("count_tokens", raw, resolveInboundModel(model, config.claudeCode), req.headers.get("anthropic-beta") ?? undefined);
1022
+ if (wantsNativePassthrough(req, config, requestPolicy, model)) {
1023
+ return await anthropicNativePassthrough(req, config, { model, provider: "anthropic-native", surface: "claude" }, undefined, raw, "/v1/messages/count_tokens");
1024
+ }
1025
+ const inputTokens = estimateClaudeRequestTokens(raw, model);
1026
+ return new Response(JSON.stringify({ input_tokens: inputTokens }), {
1027
+ status: 200,
1028
+ headers: { "Content-Type": "application/json" },
1029
+ });
1030
+ }