@bitkyc08/opencodex 2.84.0 → 2.87.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 (518) hide show
  1. package/README.md +0 -4
  2. package/bin/ocx.mjs +61 -3
  3. package/gui/dist/assets/App-ZfYbLHkB.js +58 -0
  4. package/gui/dist/assets/App-wT3mXqFz.css +1 -0
  5. package/gui/dist/assets/{Tray-C9TGtwad.js → Tray-DCd-A1az.js} +1 -1
  6. package/gui/dist/assets/index-CfYDMj34.js +94 -0
  7. package/gui/dist/assets/index-DPeB9HpV.css +1 -0
  8. package/gui/dist/assets/quota-summary-ChvmKGOk.js +1 -0
  9. package/gui/dist/index.html +2 -2
  10. package/package.json +1 -2
  11. package/src/adapters/anthropic/beta-allowlist.ts +91 -11
  12. package/src/adapters/anthropic/beta-body-pairs.ts +93 -0
  13. package/src/adapters/anthropic/client-identity.ts +1 -1
  14. package/src/adapters/anthropic/passthrough.ts +16 -3
  15. package/src/adapters/anthropic-progress.ts +84 -0
  16. package/src/adapters/anthropic.ts +108 -21
  17. package/src/adapters/azure.ts +19 -1
  18. package/src/adapters/base.ts +2 -0
  19. package/src/adapters/claude-cli/adapter.ts +111 -23
  20. package/src/adapters/claude-cli/profiles.ts +2 -1
  21. package/src/adapters/claude-cli/stable-replay.ts +108 -0
  22. package/src/adapters/claude-cli/usage-admission.ts +230 -0
  23. package/src/adapters/client-fingerprint.ts +24 -0
  24. package/src/adapters/coding-agent/protocol.ts +1 -1
  25. package/src/adapters/coding-agent/turn.ts +40 -11
  26. package/src/adapters/cursor/arg-codec.ts +17 -5
  27. package/src/adapters/cursor/catalog.ts +8 -0
  28. package/src/adapters/cursor/effort-map.ts +3 -0
  29. package/src/adapters/cursor/freeform-args.ts +75 -0
  30. package/src/adapters/cursor/protobuf-events.ts +36 -23
  31. package/src/adapters/dsh-account.ts +97 -0
  32. package/src/adapters/google-antigravity-wire.ts +22 -1
  33. package/src/adapters/google.ts +37 -10
  34. package/src/adapters/openai-chat/messages.ts +4 -3
  35. package/src/adapters/openai-chat/passthrough.ts +8 -2
  36. package/src/adapters/openai-chat/reasoning-replay.ts +52 -0
  37. package/src/adapters/openai-chat/response-events.ts +59 -1
  38. package/src/adapters/openai-chat.ts +8 -5
  39. package/src/adapters/openai-responses/compaction-search-history.ts +14 -7
  40. package/src/adapters/openai-responses/passthrough.ts +57 -125
  41. package/src/adapters/openai-responses/request-strips.ts +25 -3
  42. package/src/adapters/openai-responses/transport.ts +81 -0
  43. package/src/adapters/qoder/adapter.ts +44 -3
  44. package/src/adapters/registry.ts +5 -0
  45. package/src/adapters/zed.ts +26 -1
  46. package/src/advisor/activity.ts +55 -0
  47. package/src/advisor/budget.ts +62 -0
  48. package/src/advisor/consult.ts +223 -0
  49. package/src/advisor/context.ts +238 -0
  50. package/src/advisor/disclosure.ts +20 -0
  51. package/src/advisor/evidence-tool.ts +21 -0
  52. package/src/advisor/evidence.ts +117 -0
  53. package/src/advisor/runtime.ts +353 -0
  54. package/src/advisor/settings.ts +144 -0
  55. package/src/advisor/state.ts +369 -0
  56. package/src/advisor/synthetic-tool.ts +33 -0
  57. package/src/bridge/errors.ts +24 -1
  58. package/src/bridge/internal.ts +3 -1
  59. package/src/bridge/response-json.ts +18 -3
  60. package/src/bridge/sse.ts +58 -8
  61. package/src/bridge/terminal-refusal.ts +59 -0
  62. package/src/chat/inbound.ts +30 -6
  63. package/src/chat/outbound.ts +5 -0
  64. package/src/claude/desktop-3p-library.ts +72 -7
  65. package/src/claude/desktop-3p.ts +75 -19
  66. package/src/claude/desktop-first-party.ts +1 -1
  67. package/src/claude/desktop-picker-platform.ts +4 -0
  68. package/src/claude/desktop-picker-profile.ts +11 -4
  69. package/src/claude/desktop-picker.ts +15 -8
  70. package/src/claude/desktop-profile.ts +10 -1
  71. package/src/claude/desktop-remote-store-io.ts +5 -3
  72. package/src/claude/desktop-system-proxy.ts +50 -4
  73. package/src/claude/inbound-content-options.ts +11 -0
  74. package/src/claude/inbound.ts +154 -50
  75. package/src/claude/intercept/local-ca.ts +8 -1
  76. package/src/claude/intercept/picker-ca-startup.ts +2 -1
  77. package/src/claude/intercept/picker-runtime.ts +6 -5
  78. package/src/claude/intercept/picker-trust-windows.ts +338 -0
  79. package/src/claude/intercept/picker-trust.ts +19 -0
  80. package/src/claude/intercept/port-policy.ts +30 -0
  81. package/src/claude/intercept/runtime.ts +97 -94
  82. package/src/claude/model-default-settings.ts +154 -0
  83. package/src/claude/model-info.ts +7 -7
  84. package/src/claude/outbound.ts +51 -19
  85. package/src/cli/account-api.ts +1 -0
  86. package/src/cli/account-policy-dto.ts +6 -0
  87. package/src/cli/account-policy.ts +34 -5
  88. package/src/cli/account-preservation.ts +44 -0
  89. package/src/cli/account.ts +8 -2
  90. package/src/cli/advisor.ts +165 -0
  91. package/src/cli/auxiliary-listener-preflight.ts +37 -0
  92. package/src/cli/capabilities-accounts.ts +20 -1
  93. package/src/cli/capabilities-agents-routing.ts +23 -2
  94. package/src/cli/capabilities-base.ts +39 -11
  95. package/src/cli/capabilities-observe-system.ts +63 -3
  96. package/src/cli/capabilities-provider-models.ts +11 -0
  97. package/src/cli/chatgpt-command.ts +35 -14
  98. package/src/cli/claude.ts +46 -6
  99. package/src/cli/codex-cli-update.ts +151 -41
  100. package/src/cli/codex-provider.ts +44 -0
  101. package/src/cli/combo-input.ts +10 -1
  102. package/src/cli/combo-result.ts +1 -1
  103. package/src/cli/combo.ts +2 -1
  104. package/src/cli/cross-home-owner.ts +17 -14
  105. package/src/cli/dispatch.ts +30 -5
  106. package/src/cli/doctor.ts +15 -1
  107. package/src/cli/ensure-desktop-cold-start.ts +382 -0
  108. package/src/cli/export-command.ts +1 -1
  109. package/src/cli/help.ts +4 -2
  110. package/src/cli/index.ts +14 -5
  111. package/src/cli/integration-plan-dto.ts +3 -0
  112. package/src/cli/log-history.ts +172 -0
  113. package/src/cli/models-runtime-subcommands.ts +1 -0
  114. package/src/cli/models-runtime.ts +72 -8
  115. package/src/cli/observe-stream.ts +2 -0
  116. package/src/cli/observe.ts +16 -4
  117. package/src/cli/provider.ts +27 -5
  118. package/src/cli/registry.ts +41 -9
  119. package/src/cli/restart-scope.ts +3 -1
  120. package/src/cli/runtime-api.ts +5 -3
  121. package/src/cli/system-command.ts +40 -8
  122. package/src/cli/uninstall-client-state.ts +32 -0
  123. package/src/cli/usage-compare.ts +309 -0
  124. package/src/cli/usage-model-search.ts +4 -4
  125. package/src/cli/usage-report.ts +2 -2
  126. package/src/cli/v2.ts +1 -1
  127. package/src/client/machine-listener.ts +1 -3
  128. package/src/client/runtime.ts +1 -0
  129. package/src/clients/client-path.ts +51 -0
  130. package/src/clients/config-export/contracts.ts +3 -1
  131. package/src/clients/config-export/model-metadata.ts +3 -2
  132. package/src/clients/config-export/qoder.ts +93 -0
  133. package/src/clients/config-export.ts +35 -48
  134. package/src/clients/gentle-shell-path.ts +87 -0
  135. package/src/codex/account-auto-switch.ts +2 -0
  136. package/src/codex/account-identity.ts +29 -0
  137. package/src/codex/account-label.ts +1 -1
  138. package/src/codex/account-lifecycle.ts +21 -5
  139. package/src/codex/account-preservation.ts +28 -0
  140. package/src/codex/account-use-remaining.ts +23 -0
  141. package/src/codex/app-server-processes.ts +97 -7
  142. package/src/codex/app-server-restart-service.ts +55 -4
  143. package/src/codex/auth-api/account-list.ts +4 -0
  144. package/src/codex/auth-api/account-pause-group.ts +6 -50
  145. package/src/codex/auth-api/account-preservation.ts +57 -0
  146. package/src/codex/auth-api/pool-mode-gate.ts +5 -2
  147. package/src/codex/auth-api/routes.ts +8 -0
  148. package/src/codex/auth-api/use-remaining-route.ts +36 -0
  149. package/src/codex/auth-api.ts +0 -1
  150. package/src/codex/auth-context.ts +81 -7
  151. package/src/codex/autostart-health.ts +4 -0
  152. package/src/codex/catalog/auto-review.ts +20 -3
  153. package/src/codex/catalog/build-entries.ts +19 -2
  154. package/src/codex/catalog/combo-member.ts +14 -5
  155. package/src/codex/catalog/configured-tiers.ts +50 -0
  156. package/src/codex/catalog/effort.ts +4 -1
  157. package/src/codex/catalog/gather-capture.ts +2 -0
  158. package/src/codex/catalog/metadata.ts +11 -9
  159. package/src/codex/catalog/model-hints.ts +42 -19
  160. package/src/codex/catalog/model-visibility.ts +3 -1
  161. package/src/codex/catalog/native-ultrafast.ts +106 -0
  162. package/src/codex/catalog/parsing.ts +35 -86
  163. package/src/codex/catalog/provider-models.ts +40 -34
  164. package/src/codex/catalog/retained-sync.ts +2 -0
  165. package/src/codex/catalog/routed-gather.ts +33 -16
  166. package/src/codex/catalog/vendor-context.ts +82 -0
  167. package/src/codex/cli-install-provenance.ts +105 -17
  168. package/src/codex/cli-update-plan.ts +545 -0
  169. package/src/codex/config-write-lock.ts +63 -4
  170. package/src/codex/convergence.ts +2 -0
  171. package/src/codex/desktop-app/picker-freshness.ts +96 -0
  172. package/src/codex/desktop-app/types.ts +2 -0
  173. package/src/codex/desktop-app/windows.ts +29 -16
  174. package/src/codex/desktop-app-restart.ts +25 -2
  175. package/src/codex/desktop-compatibility/certificate-service.ts +99 -0
  176. package/src/codex/desktop-compatibility/certificate-store.ts +193 -0
  177. package/src/codex/desktop-compatibility/connection-store.ts +87 -0
  178. package/src/codex/desktop-compatibility/installed-build.ts +73 -0
  179. package/src/codex/desktop-compatibility/json-body.ts +29 -0
  180. package/src/codex/desktop-compatibility/native-identity.ts +62 -0
  181. package/src/codex/desktop-compatibility/relay-listener.ts +93 -0
  182. package/src/codex/desktop-compatibility/routing-binding.ts +10 -0
  183. package/src/codex/desktop-compatibility/routing-preflight.ts +69 -0
  184. package/src/codex/desktop-compatibility/runtime-ownership.ts +28 -0
  185. package/src/codex/desktop-compatibility/runtime.ts +182 -0
  186. package/src/codex/desktop-compatibility/service.ts +30 -0
  187. package/src/codex/desktop-compatibility/startup-settings.ts +50 -0
  188. package/src/codex/desktop-compatibility/usage-activation.ts +86 -0
  189. package/src/codex/desktop-compatibility/usage-controlled-fetch.ts +59 -0
  190. package/src/codex/desktop-compatibility/usage-controller.ts +107 -0
  191. package/src/codex/desktop-compatibility/usage-policy.ts +59 -0
  192. package/src/codex/desktop-compatibility/usage-refresh.ts +33 -0
  193. package/src/codex/desktop-compatibility/usage-sse-controller.ts +86 -0
  194. package/src/codex/desktop-compatibility/windows-activation-source.ts +44 -0
  195. package/src/codex/desktop-compatibility/windows-certificate-trust.ts +81 -0
  196. package/src/codex/desktop-compatibility/windows-key-protection.ts +165 -0
  197. package/src/codex/desktop-compatibility/windows-package-command.ts +86 -0
  198. package/src/codex/desktop-compatibility/windows-package-launch.ts +28 -0
  199. package/src/codex/features.ts +85 -45
  200. package/src/codex/history-provider.ts +1 -1
  201. package/src/codex/inject/config-write-section.ts +4 -1
  202. package/src/codex/inject/history-failure.ts +27 -0
  203. package/src/codex/inject/remove.ts +18 -6
  204. package/src/codex/inject/restore.ts +24 -2
  205. package/src/codex/inject/routing-classify.ts +4 -3
  206. package/src/codex/inject.ts +18 -27
  207. package/src/codex/journal.ts +16 -1
  208. package/src/codex/main-account-hard-lock.ts +30 -1
  209. package/src/codex/main-account.ts +99 -15
  210. package/src/codex/native-main-refresh-mode.ts +217 -0
  211. package/src/codex/native-profile-startup.ts +1 -1
  212. package/src/codex/ocx-compaction-history.ts +12 -10
  213. package/src/codex/paths.ts +86 -1
  214. package/src/codex/provider-routing.ts +174 -0
  215. package/src/codex/refresh.ts +2 -2
  216. package/src/codex/routing/health-store.ts +2 -2
  217. package/src/codex/routing/pool-recovery.ts +79 -0
  218. package/src/codex/routing/selection.ts +4 -3
  219. package/src/codex/routing/thread-affinity.ts +1 -1
  220. package/src/codex/shim-diagnostics.ts +6 -0
  221. package/src/codex/shim-overlay.ts +7 -2
  222. package/src/codex/signed-in-identity.ts +43 -0
  223. package/src/codex/subagent-model-fallback.ts +1 -1
  224. package/src/codex/sync.ts +6 -1
  225. package/src/codex/user-identity.ts +16 -5
  226. package/src/codex/windows-local-appdata.ts +84 -0
  227. package/src/combos/failover.ts +6 -1
  228. package/src/combos/index.ts +1 -0
  229. package/src/combos/resolve.ts +46 -10
  230. package/src/combos/types.ts +15 -0
  231. package/src/config/diagnostics.ts +35 -7
  232. package/src/config/live-reconcile.ts +51 -2
  233. package/src/config/load-degrade.ts +23 -1
  234. package/src/config/opencode-wire-upgrade.ts +54 -0
  235. package/src/config/opencode-zen-upgrade.ts +52 -0
  236. package/src/config/owner-registry.ts +22 -17
  237. package/src/config/paths.ts +10 -3
  238. package/src/config/provider-validation.ts +45 -2
  239. package/src/config/schema/config-schema.ts +39 -0
  240. package/src/config/schema/desktop-compatibility.ts +9 -0
  241. package/src/config/schema/leaf-validators.ts +39 -9
  242. package/src/config/schema/main-account-hard-lock-fallback.ts +26 -0
  243. package/src/config/schema/native-reasoning-retention.ts +26 -0
  244. package/src/config.ts +2 -0
  245. package/src/generated/compatibility-version.json +962 -350
  246. package/src/generated/model-metadata.ts +1 -1
  247. package/src/grok/sync.ts +1 -1
  248. package/src/images/loop.ts +46 -8
  249. package/src/integrations/catalog-refresh.ts +1 -1
  250. package/src/integrations/config-io.ts +20 -2
  251. package/src/integrations/journal.ts +15 -1
  252. package/src/integrations/mutation-plan.ts +2 -0
  253. package/src/integrations/registry.ts +15 -0
  254. package/src/integrations/store.ts +3 -0
  255. package/src/integrations/writer.ts +10 -1
  256. package/src/lib/advisor-activation.ts +16 -0
  257. package/src/lib/background-work.ts +65 -0
  258. package/src/lib/bounded-file-read.ts +58 -0
  259. package/src/lib/bun-path-runtime.d.mts +10 -1
  260. package/src/lib/bun-path-runtime.mjs +62 -3
  261. package/src/lib/codex-restart-contract.ts +15 -0
  262. package/src/lib/desktop-proxy-route.ts +30 -0
  263. package/src/lib/desktop-upstream-tunnel.ts +72 -0
  264. package/src/lib/empty-tool-catalog.ts +62 -0
  265. package/src/lib/errors.ts +26 -3
  266. package/src/lib/gcp-adc.ts +96 -9
  267. package/src/lib/gcp-credential-input.ts +12 -0
  268. package/src/lib/lab-live-route-production.ts +5 -0
  269. package/src/lib/local-internal-call-capability.ts +55 -0
  270. package/src/lib/opaque-proxy-tunnel.ts +217 -0
  271. package/src/lib/package-tree-integrity.ts +2 -2
  272. package/src/lib/redact.ts +2 -2
  273. package/src/lib/socks5-fetch.ts +48 -71
  274. package/src/lib/socks5-handshake.ts +112 -0
  275. package/src/lib/translator-budget.ts +7 -1
  276. package/src/oauth/account-quota-rank.ts +69 -2
  277. package/src/oauth/anthropic-account-refusal.ts +20 -2
  278. package/src/oauth/anthropic-oauth-definitions.ts +3 -1
  279. package/src/oauth/anthropic-routing.ts +1 -21
  280. package/src/oauth/anthropic.ts +38 -7
  281. package/src/oauth/callback-server.ts +27 -1
  282. package/src/oauth/chatgpt-device.ts +1 -1
  283. package/src/oauth/chatgpt-shared.ts +83 -0
  284. package/src/oauth/chatgpt.ts +8 -72
  285. package/src/oauth/dsh.ts +203 -0
  286. package/src/oauth/generic-account-failover.ts +62 -12
  287. package/src/oauth/generic-session-affinity.ts +125 -0
  288. package/src/oauth/index.ts +188 -111
  289. package/src/oauth/kiro-device-login.ts +2 -1
  290. package/src/oauth/kiro.ts +7 -1
  291. package/src/oauth/local-token-detect.ts +21 -1
  292. package/src/oauth/login-cli-errors.ts +44 -0
  293. package/src/oauth/login-cli.ts +69 -7
  294. package/src/oauth/login-flow-state.ts +371 -23
  295. package/src/oauth/meta-muse-errors.ts +41 -0
  296. package/src/oauth/meta-muse-keychain.ts +52 -0
  297. package/src/oauth/meta-muse.ts +51 -84
  298. package/src/oauth/model-discovery-auth.ts +3 -0
  299. package/src/oauth/pool-settings-capability.ts +4 -2
  300. package/src/oauth/refresh-flight-retirement.ts +48 -0
  301. package/src/oauth/session-affinity-key.ts +23 -0
  302. package/src/oauth/store-anthropic-instance.ts +9 -2
  303. package/src/oauth/store.ts +12 -5
  304. package/src/protocols/codecs/chat-ir.ts +467 -0
  305. package/src/protocols/codecs/chat.ts +10 -1
  306. package/src/protocols/codecs/messages-ir.ts +500 -0
  307. package/src/protocols/encoders/adapter-events.ts +5 -4
  308. package/src/protocols/encoders/messages.ts +9 -6
  309. package/src/protocols/features.ts +10 -2
  310. package/src/protocols/path.ts +11 -1
  311. package/src/protocols/plan-snapshot.ts +39 -16
  312. package/src/protocols/plan.ts +9 -1
  313. package/src/protocols/provider-summary.ts +2 -1
  314. package/src/protocols/settings.ts +8 -0
  315. package/src/providers/antigravity-account-roster.ts +102 -0
  316. package/src/providers/antigravity-models.ts +85 -7
  317. package/src/providers/api-key-cooldowns.ts +49 -0
  318. package/src/providers/api-key-resolve.ts +11 -6
  319. package/src/providers/api-key-response-ownership.ts +57 -0
  320. package/src/providers/api-key-selection.ts +31 -6
  321. package/src/providers/api-keys.ts +214 -3
  322. package/src/providers/claude-cli-usage.ts +96 -0
  323. package/src/providers/dsh-account-destination.ts +31 -0
  324. package/src/providers/dsh-account-models.ts +41 -0
  325. package/src/providers/gcp-credential-cleanup.ts +59 -0
  326. package/src/providers/key-failover.ts +54 -50
  327. package/src/providers/key-store.ts +7 -2
  328. package/src/providers/model-rename-fields.ts +5 -0
  329. package/src/providers/model-rename-migration.ts +7 -5
  330. package/src/providers/openai-tiers-destination.ts +11 -2
  331. package/src/providers/openai-tiers.ts +5 -12
  332. package/src/providers/opencode-go-transport.ts +3 -1
  333. package/src/providers/quota/account-cache.ts +6 -5
  334. package/src/providers/quota/account-row.ts +34 -0
  335. package/src/providers/quota/antigravity.ts +28 -7
  336. package/src/providers/quota/dsh-account.ts +70 -0
  337. package/src/providers/quota/vendor-probes-oauth.ts +1 -93
  338. package/src/providers/quota-types.ts +2 -0
  339. package/src/providers/quota.ts +14 -20
  340. package/src/providers/registry/entries-core.ts +44 -14
  341. package/src/providers/registry/entries-extended.ts +19 -0
  342. package/src/providers/registry/types.ts +1 -1
  343. package/src/providers/request-pacing.ts +77 -19
  344. package/src/providers/routed-config.ts +339 -0
  345. package/src/providers/service-tier.ts +2 -1
  346. package/src/quota/window-mapping.ts +5 -2
  347. package/src/responses/anthropic-progress.ts +34 -0
  348. package/src/responses/assistant-replay.ts +35 -0
  349. package/src/responses/compaction-history-framing.ts +65 -0
  350. package/src/responses/compaction.ts +6 -1
  351. package/src/responses/incomplete-reason.ts +31 -0
  352. package/src/responses/parser.ts +78 -22
  353. package/src/responses/reasoning-envelope.ts +4 -0
  354. package/src/responses/reasoning-replay-cache.ts +24 -2
  355. package/src/responses/reasoning-retention.ts +173 -0
  356. package/src/responses/retained-compaction.ts +46 -0
  357. package/src/responses/schema.ts +23 -0
  358. package/src/responses/state.ts +32 -29
  359. package/src/router.ts +8 -321
  360. package/src/routing/health.ts +37 -22
  361. package/src/routing/history/build-file.ts +92 -0
  362. package/src/routing/history/indexer.ts +254 -221
  363. package/src/routing/history/ingest.ts +242 -0
  364. package/src/routing/history/rebuild-job.ts +111 -0
  365. package/src/routing/history/rebuild-protocol.ts +15 -0
  366. package/src/routing/history/rebuild-worker.ts +34 -0
  367. package/src/routing/profile.ts +1 -1
  368. package/src/server/admission-model-scope.ts +21 -1
  369. package/src/server/antigravity-direct-eligibility.ts +208 -0
  370. package/src/server/auth-cors.ts +15 -0
  371. package/src/server/chat-completions.ts +82 -10
  372. package/src/server/chat-native-reasoning.ts +123 -0
  373. package/src/server/chat-native-sse.ts +110 -7
  374. package/src/server/chat-native.ts +76 -17
  375. package/src/server/claude-messages.ts +164 -433
  376. package/src/server/effort-policy.ts +1 -1
  377. package/src/server/images.ts +15 -2
  378. package/src/server/index/desktop-compatibility-startup.ts +56 -0
  379. package/src/server/index/model-alias-id.ts +36 -0
  380. package/src/server/index/optional-listeners.ts +2 -0
  381. package/src/server/index/serve-options.ts +11 -3
  382. package/src/server/index/startup-warnings.ts +11 -0
  383. package/src/server/index/websocket-handler.ts +9 -0
  384. package/src/server/index.ts +14 -11
  385. package/src/server/inference/advisor-diagnostics.ts +31 -0
  386. package/src/server/inference/client-encoder-delivery.ts +6 -5
  387. package/src/server/login-shell-path.ts +146 -0
  388. package/src/server/management/advisor-routes.ts +202 -0
  389. package/src/server/management/agent-settings-routes.ts +22 -4
  390. package/src/server/management/claude-desktop-picker-routes.ts +1 -1
  391. package/src/server/management/combo-routes.ts +176 -50
  392. package/src/server/management/companion-routes.ts +4 -0
  393. package/src/server/management/config-routes.ts +77 -6
  394. package/src/server/management/context.ts +4 -0
  395. package/src/server/management/cursor-integration-routes.ts +1 -1
  396. package/src/server/management/desktop-compatibility-routes.ts +46 -0
  397. package/src/server/management/desktop-compatibility-runtime-routes.ts +46 -0
  398. package/src/server/management/desktop-compatibility-settings-routes.ts +27 -0
  399. package/src/server/management/model-routes.ts +13 -8
  400. package/src/server/management/model-rows.ts +14 -0
  401. package/src/server/management/native-integration-routes.ts +2 -7
  402. package/src/server/management/native-reasoning-retention-routes.ts +41 -0
  403. package/src/server/management/oauth-account-routes.ts +87 -14
  404. package/src/server/management/protocol-settings-patch.ts +1 -0
  405. package/src/server/management/provider-gcp-credential-routes.ts +29 -0
  406. package/src/server/management/provider-key-pacing-routes.ts +31 -0
  407. package/src/server/management/provider-overwrite-carry.ts +7 -0
  408. package/src/server/management/provider-routes.ts +25 -30
  409. package/src/server/management/route-registry.ts +21 -1
  410. package/src/server/management/sibling-guard.ts +1 -0
  411. package/src/server/management-api.ts +9 -1
  412. package/src/server/messages-antigravity-direct.ts +195 -0
  413. package/src/server/messages-native-tool-guard.ts +185 -0
  414. package/src/server/messages-native.ts +106 -22
  415. package/src/server/messages-shared.ts +431 -0
  416. package/src/server/relay.ts +1 -1
  417. package/src/server/request-log-terminal-status.ts +1 -1
  418. package/src/server/request-log.ts +13 -1
  419. package/src/server/responses/account-change-state.ts +8 -2
  420. package/src/server/responses/adapter-continuation.ts +48 -19
  421. package/src/server/responses/adapter-delivery.ts +43 -8
  422. package/src/server/responses/adapter-dispatch.ts +48 -4
  423. package/src/server/responses/advisor-plan-slot.ts +27 -0
  424. package/src/server/responses/advisor-retention.ts +117 -0
  425. package/src/server/responses/advisor-slot.ts +368 -0
  426. package/src/server/responses/code-mode-collab-surface.ts +116 -8
  427. package/src/server/responses/codex-ws-wire.ts +15 -8
  428. package/src/server/responses/compact.ts +303 -125
  429. package/src/server/responses/compaction-recovery.ts +18 -10
  430. package/src/server/responses/compaction-retention.ts +61 -0
  431. package/src/server/responses/context-overflow.ts +11 -6
  432. package/src/server/responses/core-auth.ts +6 -0
  433. package/src/server/responses/core-codex-account.ts +11 -1
  434. package/src/server/responses/core-combo-failure.ts +12 -1
  435. package/src/server/responses/core-combo.ts +141 -36
  436. package/src/server/responses/core-errors.ts +3 -3
  437. package/src/server/responses/core-lifetime.ts +7 -2
  438. package/src/server/responses/core-normalize.ts +11 -0
  439. package/src/server/responses/core-opaque-recovery.ts +35 -0
  440. package/src/server/responses/core-options.ts +51 -0
  441. package/src/server/responses/core-pipeline.ts +141 -0
  442. package/src/server/responses/core-replay.ts +25 -6
  443. package/src/server/responses/core.ts +3 -116
  444. package/src/server/responses/fetch-helpers.ts +73 -19
  445. package/src/server/responses/main-hard-lock-routing.ts +109 -0
  446. package/src/server/responses/native-response-control.ts +4 -2
  447. package/src/server/responses/passthrough-delivery.ts +19 -11
  448. package/src/server/responses/passthrough-dispatch.ts +29 -16
  449. package/src/server/responses/request-prepare.ts +144 -17
  450. package/src/server/responses/request-sidecar-auth.ts +2 -2
  451. package/src/server/responses/request-transport.ts +120 -27
  452. package/src/server/responses/response-effects.ts +1 -0
  453. package/src/server/responses/run-turn-execution.ts +166 -38
  454. package/src/server/responses/sidecar-execution.ts +98 -5
  455. package/src/server/responses/terminal-guard.ts +4 -17
  456. package/src/server/responses-undeclared-tool-guard.ts +30 -9
  457. package/src/server/startup-action-control.ts +16 -2
  458. package/src/server/startup-health-cache.ts +1 -0
  459. package/src/service/auth-preflight.ts +71 -0
  460. package/src/service/cli.ts +4 -2
  461. package/src/service/definition.ts +97 -0
  462. package/src/service/desktop-startup.ts +14 -0
  463. package/src/service/guards.ts +1 -61
  464. package/src/service/health-sentinel-linux.ts +112 -0
  465. package/src/service/health-sentinel-main.ts +99 -61
  466. package/src/service/health-sentinel-probe.ts +24 -7
  467. package/src/service/health.ts +5 -89
  468. package/src/service/launchd.ts +27 -7
  469. package/src/service/logging.ts +135 -0
  470. package/src/service/repair.ts +2 -1
  471. package/src/service/state.ts +7 -8
  472. package/src/service/systemd.ts +1 -1
  473. package/src/service/windows-guarded-manager.ts +2 -1
  474. package/src/service/windows-ops.ts +1 -1
  475. package/src/service/windows-scheduler.ts +2 -7
  476. package/src/service/windows-taskxml.ts +2 -2
  477. package/src/service/windows-wscript.ts +14 -0
  478. package/src/service.ts +4 -2
  479. package/src/sidecar/auth.ts +2 -0
  480. package/src/tray/windows-tray.ps1 +43 -24
  481. package/src/types/config.ts +124 -6
  482. package/src/types/opencode-go.ts +61 -0
  483. package/src/types/opencode-zen.ts +29 -0
  484. package/src/types/provider.ts +39 -6
  485. package/src/types/request.ts +38 -2
  486. package/src/types/tools.ts +2 -0
  487. package/src/types/wire.ts +29 -37
  488. package/src/types.ts +1 -0
  489. package/src/update/async-check.ts +1 -1
  490. package/src/update/badge.ts +2 -2
  491. package/src/update/index.ts +88 -15
  492. package/src/update/job-supervised-restart.ts +107 -0
  493. package/src/update/job.ts +32 -32
  494. package/src/update/notify.ts +1 -1
  495. package/src/update/npm-invocation.mjs +12 -2
  496. package/src/update/refresh-scheduler.ts +2 -2
  497. package/src/update/supervised-recovery.d.mts +27 -0
  498. package/src/update/supervised-recovery.mjs +157 -0
  499. package/src/usage/cache-diagnostic.ts +2 -0
  500. package/src/usage/cost.ts +48 -12
  501. package/src/usage/expected-prices.ts +15 -0
  502. package/src/usage/ledger-retention.ts +2 -1
  503. package/src/usage/ledger-scanner.ts +6 -5
  504. package/src/usage/log.ts +17 -8
  505. package/src/usage/summary.ts +22 -9
  506. package/src/usage/user-cost-overlays.ts +14 -19
  507. package/src/usage/user-cost-validation.ts +38 -0
  508. package/src/vision/eligibility.ts +13 -0
  509. package/src/web-search/gemini-executor.ts +2 -2
  510. package/src/web-search/loop.ts +55 -5
  511. package/src/web-search/progress-stream.ts +4 -1
  512. package/src/web-search/run-turn-loop.ts +9 -1
  513. package/assets/sponsors/packycode.png +0 -0
  514. package/gui/dist/assets/App-CSJvmjNs.css +0 -1
  515. package/gui/dist/assets/App-CZtqiGcw.js +0 -56
  516. package/gui/dist/assets/index-BB6uK6tU.css +0 -1
  517. package/gui/dist/assets/index-CTm4l-HQ.js +0 -94
  518. package/gui/dist/assets/quota-summary-BbEeRfPY.js +0 -1
@@ -15,19 +15,25 @@ import {
15
15
  } from "./admission-model-scope";
16
16
  import { jsonUtf8Bytes } from "../lib/json-byte-size";
17
17
  import { redactSecretString } from "../lib/redact";
18
- import { sseFieldValue } from "../lib/sse-decoder";
19
- import { enforceAnthropicImageLimits, sniffImageDimensions } from "../adapters/anthropic-image-guard";
18
+ import { enforceAnthropicImageLimits } from "../adapters/anthropic-image-guard";
20
19
  import { normalizeAnthropicImages } from "../adapters/anthropic-image-normalize";
21
- import { createToolCallIdAllocator } from "../adapters/tool-call-id";
22
20
  import { openAIChatSerializesThinking } from "../adapters/openai-chat/messages";
23
21
  import { messagesToResponsesTranslation } from "../protocols/codecs/messages";
24
- import { AnthropicRequestError, DesktopModelMappingUnavailableError, extractOcxEffortDirective, extractOcxRouteDirective, nativeAnthropicProjection, resolveInboundModel, type ClaudeCacheKeySource } from "../claude/inbound";
22
+ import {
23
+ messagesDirectPlanHolds,
24
+ messagesDirectRequestOption,
25
+ messagesDirectTraceDecline,
26
+ planMessagesAntigravityDirect,
27
+ type MessagesDirectPlan,
28
+ } from "./messages-antigravity-direct";
29
+ import { AnthropicRequestError, callerCacheRetention, DesktopModelMappingUnavailableError, extractOcxEffortDirective, extractOcxRouteDirective, nativeAnthropicProjection, resolveInboundModel, type ClaudeCacheKeySource } from "../claude/inbound";
25
30
  import { nativeReasoningTag, type NativeReasoningOwner } from "../responses/reasoning-replay-cache";
26
31
  import { isKnownDesktop3pModelId, resolveDesktop3pAlias } from "../claude/desktop-3p";
27
32
  import { resolveAlias, claudeCodeNativeAlias, legacyAliasForNative } from "../claude/alias";
28
33
  import { recordDesktopRequest } from "../claude/desktop-health";
29
34
  import { stripOneMillionMarker } from "../claude/context-windows";
30
35
  import { captureClaudeInbound } from "../claude/inbound-debug";
36
+ import { declaredAnthropicServerTools } from "../claude/inbound-content-options";
31
37
  import { claudeCodeForIngress } from "../claude/intercept/model-bindings";
32
38
  import { classifyInterceptClient } from "../claude/intercept/client-class";
33
39
  import { analyzeClaudeCompatibility, isClaudeCompatibilityMode } from "../claude/compatibility";
@@ -40,6 +46,7 @@ import {
40
46
  REPLAY_REFUSED_STATUS,
41
47
  UPSTREAM_RESET_REPLAY_REFUSED_CODE,
42
48
  } from "../lib/upstream-retry";
49
+ import { isClientClosedMessage, recognizedUpstreamError, UNDECLARED_TOOL_CALL_ERROR_CODE } from "../lib/errors";
43
50
  import { resolveClientRetryAfter } from "../lib/retry-after";
44
51
  import { anthropicRateLimitHeaders } from "./anthropic-rate-limit-headers";
45
52
  import { upstreamMessagesRequestIdHeaders } from "./messages-response-headers";
@@ -51,10 +58,8 @@ import {
51
58
  responsesSseToAnthropicSse,
52
59
  } from "../claude/outbound";
53
60
  import { clearableDeadline, idleDeadline } from "../lib/abort";
54
- import { estimateTokens } from "../lib/token-estimate";
55
61
  import {
56
62
  CLAUDE_NATIVE_THINKING,
57
- projectClaudeRequest,
58
63
  type ClaudeThinkingProjection,
59
64
  } from "../lib/claude-request-projection";
60
65
  import { captureRouteStaticPolicy, NoEligiblePolicyCandidateError, previewRouteModel, routedProviderConfig, UnknownRoutingPolicyError, routeModel, type RouteResult } from "../router";
@@ -63,7 +68,6 @@ import { resolveWireProtocolOverride } from "./adapter-resolve";
63
68
  import type { OcxConfig } from "../types";
64
69
  import { readJsonRequestBody, resolveInboundBodyLimitBytes } from "./request-decompress";
65
70
  import { addFinalRequestLog, httpStatusForRequestLogTerminal, recordFirstOutput, type RequestLogContext } from "./request-log";
66
- import { recordGenerationEvent } from "./request-log-generation-window";
67
71
  import { createFinalRequestLog } from "./inference/final-log";
68
72
  import {
69
73
  conversationIdFromClaudeMetadata,
@@ -276,309 +280,39 @@ function uuidFromHex(hex32: string): string {
276
280
  return `${h.slice(0, 8)}-${h.slice(8, 12)}-4${h.slice(13, 16)}-8${h.slice(17, 20)}-${h.slice(20, 32)}`;
277
281
  }
278
282
 
279
- export function anthropicUsageToOcx(usage: Rec | undefined): { inputTokens: number; outputTokens: number; cachedInputTokens?: number; cacheReadInputTokens?: number; cacheCreationInputTokens?: number } | undefined {
280
- if (!usage) return undefined;
281
- const num = (v: unknown) => typeof v === "number" ? v : 0;
282
- const hasCache = usage.cache_read_input_tokens !== undefined || usage.cache_creation_input_tokens !== undefined;
283
- const read = num(usage.cache_read_input_tokens);
284
- const write = num(usage.cache_creation_input_tokens);
285
- // Anthropic input_tokens excludes cache read/write; normalize to the canonical
286
- // inclusive convention (types.ts OcxUsage / devlog 070). cached = READS only.
287
- return {
288
- inputTokens: num(usage.input_tokens) + read + write,
289
- outputTokens: num(usage.output_tokens),
290
- ...(hasCache ? {
291
- cachedInputTokens: read,
292
- cacheReadInputTokens: read,
293
- cacheCreationInputTokens: write,
294
- } : {}),
295
- };
296
- }
297
-
298
- /** Body-occupancy guard for the native passthrough (devlog 260716_passthrough_followups/010). */
299
- export interface PassthroughBodyGuard {
300
- /** Idle window in ms — raw upstream-byte inactivity while a read is pending. 0 disables. */
301
- stallMs: number;
302
- /** Cumulative body byte cap. 0 disables. */
303
- maxBytes: number;
304
- /** Client request signal for deterministic cancel classification. */
305
- reqSignal?: AbortSignal;
306
- }
307
-
308
- type PassthroughCloseReason = "terminal" | "client_cancel" | "body_stall" | "body_overflow";
309
- type PassthroughFinalizeMeta = { closeReason: PassthroughCloseReason; terminalStatus?: "failed" | "incomplete" };
310
-
311
283
  /**
312
- * Tap an Anthropic-vocabulary SSE stream for the request log (usage + terminal),
313
- * bounding body occupancy: idle (silence-only, timed ONLY while a reader.read() is
314
- * pending so downstream backpressure never counts as upstream inactivity) and a
315
- * cumulative byte cap. On stall/overflow, or when the upstream read fails after the
316
- * headers went out, it appends a protocol-compatible Anthropic `event: error` terminal
317
- * frame after a blank-line boundary, closes, and cancels the upstream reader — never a
318
- * total-wall-clock bound (slow-but-alive streams live).
319
- * Exported for deterministic unit tests.
284
+ * The passthrough accounting layer, shared with the managed native lane. It lives in
285
+ * messages-shared.ts so that lane can take it without importing this module, and this
286
+ * module without importing that lane. Re-exported here because this file was its only
287
+ * home and callers still resolve it from this module.
320
288
  */
321
- export function tapAnthropicSseForLog(
322
- upstream: ReadableStream<Uint8Array>,
323
- logCtx: RequestLogContext,
324
- finalize: (status: number, meta: PassthroughFinalizeMeta) => void,
325
- guard?: PassthroughBodyGuard,
326
- ): ReadableStream<Uint8Array> {
327
- const decoder = new TextDecoder();
328
- const encoder = new TextEncoder();
329
- let buffer = "";
330
- let usageAcc: Rec = {};
331
- // message_stop or an upstream error event: the turn's own terminal already went through.
332
- let terminalSeen = false;
333
- const inspectFrame = (frame: string) => {
334
- const dataLine = frame
335
- .split("\n")
336
- .map(l => sseFieldValue(l, "data"))
337
- .filter((v): v is string => v !== null)
338
- .join("");
339
- if (!dataLine) return;
340
- let data: unknown;
341
- try { data = JSON.parse(dataLine); } catch { return; }
342
- if (!isRec(data)) return;
343
- recordGenerationEvent(logCtx, data.type);
344
- if (data.type === "message_start" && isRec(data.message) && isRec(data.message.usage)) {
345
- usageAcc = { ...usageAcc, ...data.message.usage };
346
- } else if (data.type === "message_delta" && isRec(data.usage)) {
347
- usageAcc = { ...usageAcc, ...data.usage };
348
- } else if (data.type === "message_stop" || data.type === "error") {
349
- terminalSeen = true;
350
- }
351
- };
352
- const inspect = (chunk: Uint8Array) => {
353
- buffer += decoder.decode(chunk, { stream: true });
354
- // SSE lines may end in CRLF, LF or CR. Normalize the inspection copy to LF (the forwarded
355
- // bytes are untouched), holding a trailing CR until the next chunk shows whether an LF
356
- // follows it, so a CRLF split across chunks stays one line ending.
357
- const heldCr = buffer.endsWith("\r");
358
- buffer = (heldCr ? buffer.slice(0, -1) : buffer).replace(/\r\n?/g, "\n") + (heldCr ? "\r" : "");
359
- let sep: number;
360
- while ((sep = buffer.indexOf("\n\n")) !== -1) {
361
- const frame = buffer.slice(0, sep);
362
- buffer = buffer.slice(sep + 2);
363
- inspectFrame(frame);
364
- }
365
- };
366
- // The last block can sit in the buffer without its blank line: an upstream that stopped after
367
- // it, or a held trailing CR. Count it before deciding how the turn ended (the Responses relay
368
- // flushes the same candidate). Returns true when this flush found the terminal in a block the
369
- // client has not seen a blank line after, so the caller restores one.
370
- const flushTail = (): boolean => {
371
- const terminalBefore = terminalSeen;
372
- const tail = (buffer + decoder.decode()).replace(/\r\n?/g, "\n");
373
- buffer = "";
374
- if (tail) inspectFrame(tail);
375
- return terminalSeen && !terminalBefore && !tail.endsWith("\n\n");
376
- };
377
- const reader = upstream.getReader();
378
- let settled = false;
379
- let bodyBytes = 0;
380
- let tapController: ReadableStreamDefaultController<Uint8Array> | undefined;
381
-
382
- const recordUsage = () => {
383
- logCtx.usage = anthropicUsageToOcx(Object.keys(usageAcc).length > 0 ? usageAcc : undefined);
384
- };
385
- const closeWithErrorFrame = (errType: string, message: string) => {
386
- const payload = JSON.stringify({ type: "error", error: { type: errType, message } });
387
- try {
388
- // Leading blank line terminates any partial SSE block so the frame parses cleanly
389
- // (relaySseWithFailedTail policy, Anthropic wire shape).
390
- tapController?.enqueue(encoder.encode(`\n\nevent: error\ndata: ${payload}\n\n`));
391
- tapController?.close();
392
- } catch { /* client already torn down */ }
393
- };
394
- const failBody = (closeReason: "body_stall" | "body_overflow", errType: string, message: string) => {
395
- if (settled) return;
396
- settled = true;
397
- idle.cancel();
398
- detachAbort();
399
- const terminalInTail = flushTail();
400
- recordUsage();
401
- if (terminalSeen) {
402
- // The turn already ended (message_stop or an upstream error event): an upstream that then
403
- // idles or keeps sending did not cut it short. Same rule as the read-error branch,
404
- // including the restored blank line for a terminal found only in the tail.
405
- finalize(200, { closeReason: "terminal" });
406
- try {
407
- if (terminalInTail) tapController?.enqueue(encoder.encode("\n\n"));
408
- tapController?.close();
409
- } catch { /* client already torn down */ }
410
- } else {
411
- // A cut-short turn, logged as the Responses relay logs a stall-timeout incomplete
412
- // (httpStatusForRequestLogTerminal: only a max_output_tokens incomplete is a 200). A 200
413
- // row with no terminalStatus also lost its failure diagnostics in usage.jsonl.
414
- logCtx.upstreamError = message.slice(0, 500);
415
- finalize(502, { terminalStatus: "incomplete", closeReason });
416
- closeWithErrorFrame(errType, message);
417
- }
418
- reader.cancel(new DOMException(message, closeReason === "body_stall" ? "TimeoutError" : "QuotaExceededError")).catch(() => {});
419
- };
420
- const idle = idleDeadline(guard?.stallMs ?? 0, () => {
421
- failBody(
422
- "body_stall",
423
- "timeout_error",
424
- `anthropic passthrough body stalled: no upstream bytes for ${Math.round((guard?.stallMs ?? 0) / 1000)}s`,
425
- );
426
- });
427
- // Deterministic client-cancel classification: Bun may surface a client abort as a
428
- // reader.read() rejection OR a resolved done (src/lib/abort.ts cancelBodyOnAbort
429
- // rationale), so the listener performs first-wins settlement itself instead of
430
- // relying on which shape the read takes.
431
- const onClientAbort = () => {
432
- if (settled) return;
433
- settled = true;
434
- idle.cancel();
435
- detachAbort();
436
- finalize(499, { closeReason: "client_cancel" });
437
- try { tapController?.close(); } catch { /* downstream already torn down */ }
438
- reader.cancel(guard?.reqSignal?.reason).catch(() => {});
439
- };
440
- const detachAbort = (() => {
441
- const signal = guard?.reqSignal;
442
- if (!signal) return () => {};
443
- if (signal.aborted) {
444
- queueMicrotask(onClientAbort);
445
- return () => {};
446
- }
447
- signal.addEventListener("abort", onClientAbort, { once: true });
448
- return () => signal.removeEventListener("abort", onClientAbort);
449
- })();
289
+ import {
290
+ anthropicUsageToOcx,
291
+ estimateClaudeRequestTokens,
292
+ resolvePassthroughBodyGuard,
293
+ sanitizePassthroughToolCallIds,
294
+ tapAnthropicSseForLog,
295
+ type PassthroughBodyGuard,
296
+ type PassthroughCloseReason,
297
+ } from "./messages-shared";
450
298
 
451
- return new ReadableStream<Uint8Array>({
452
- start(controller) {
453
- tapController = controller;
454
- },
455
- async pull(controller) {
456
- if (settled) return;
457
- try {
458
- idle.reset();
459
- const { done, value } = await reader.read();
460
- idle.pause();
461
- if (settled) return; // stall/overflow/abort won the race while we awaited
462
- if (done) {
463
- settled = true;
464
- idle.cancel();
465
- detachAbort();
466
- recordUsage();
467
- finalize(200, { closeReason: "terminal" });
468
- controller.close();
469
- return;
470
- }
471
- if (value.byteLength > 0) {
472
- bodyBytes += value.byteLength;
473
- if (guard && guard.maxBytes > 0 && bodyBytes > guard.maxBytes) {
474
- failBody(
475
- "body_overflow",
476
- "api_error",
477
- `anthropic passthrough body exceeded ${guard.maxBytes} bytes`,
478
- );
479
- return;
480
- }
481
- }
482
- inspect(value);
483
- controller.enqueue(value);
484
- } catch (err) {
485
- if (settled) return;
486
- // Bun can settle a fetch body read before it dispatches the abort listeners
487
- // (consumeForInspection in relay.ts): read the signal itself before calling this
488
- // rejection an upstream failure.
489
- if (guard?.reqSignal?.aborted) {
490
- onClientAbort();
491
- return;
492
- }
493
- settled = true;
494
- idle.cancel();
495
- detachAbort();
496
- const terminalInTail = flushTail();
497
- recordUsage();
498
- if (isTranslatorBudgetExceededError(err)) {
499
- // A local cap, not an upstream failure: the non-streaming native Messages fold
500
- // maps this error to a 413 itself, so it still errors the stream.
501
- finalize(200, { closeReason: "terminal" });
502
- try { controller.error(err); } catch { /* torn down */ }
503
- return;
504
- }
505
- if (terminalSeen) {
506
- // Only the transport trailer was lost; the client already has the turn's terminal.
507
- // The Responses relay likewise reports a read error only without a seen terminal.
508
- finalize(200, { closeReason: "terminal" });
509
- try {
510
- // An SSE parser drops an event that EOF cuts off before its blank line, so restore
511
- // the delimiter when the terminal was only found in that unterminated tail.
512
- if (terminalInTail) controller.enqueue(encoder.encode("\n\n"));
513
- controller.close();
514
- } catch { /* torn down */ }
515
- reader.cancel(err).catch(() => {});
516
- return;
517
- }
518
- // The upstream read failed after the 200 went out (a mid-stream socket reset). Log it
519
- // the way the Responses relay does (onReadError): a truncated body is a failed turn,
520
- // not a completed one. The client gets the same Anthropic error terminal as a stall,
521
- // instead of a connection reset — or, on some Bun releases, a bare EOF that reads as
522
- // a finished message.
523
- const message = redactSecretString(`anthropic passthrough upstream stream failed: ${err instanceof Error ? err.message : String(err)}`);
524
- logCtx.transportPhase = "mid_stream";
525
- logCtx.terminalSource = "synthetic";
526
- logCtx.upstreamError = message.slice(0, 500);
527
- if (logCtx.activeAttempt) logCtx.activeAttempt.streamAborted = true;
528
- finalize(502, { terminalStatus: "failed", closeReason: "terminal" });
529
- closeWithErrorFrame("api_error", message);
530
- reader.cancel(err).catch(() => {});
531
- }
532
- },
533
- cancel(reason) {
534
- if (!settled) {
535
- settled = true;
536
- idle.cancel();
537
- detachAbort();
538
- finalize(499, { closeReason: "client_cancel" });
539
- }
540
- reader.cancel(reason).catch(() => {});
541
- },
542
- });
543
- }
299
+ export {
300
+ anthropicUsageToOcx,
301
+ estimateClaudeRequestTokens,
302
+ resolvePassthroughBodyGuard,
303
+ sanitizePassthroughToolCallIds,
304
+ tapAnthropicSseForLog,
305
+ type PassthroughBodyGuard,
306
+ type PassthroughCloseReason,
307
+ type PassthroughFinalizeMeta,
308
+ } from "./messages-shared";
544
309
 
545
- /**
546
- * `tool_use.id` / `tool_result.tool_use_id` must match Anthropic's wire contract
547
- * (`^[a-zA-Z0-9_-]+$`, <=64 chars). Third-party models mint other shapes — Devin's
548
- * swe-2 emits `Bash:0#<hex>` — and a session history carrying them 400s the moment it
549
- * is switched to a native Anthropic model ("messages.N.content.M.tool_use.id: String
550
- * should match pattern"). The adapter path normalizes these via
551
- * adapters/tool-call-id.ts (#1780); this passthrough bypasses that adapter, so the same
552
- * allocator runs here. Stateless per request: conforming ids pass through byte-identical
553
- * (prompt-cache keys untouched), rewritten ids keep call/result pairing stable.
554
- * An empty id has no representable wire form, and forwarding `""` is what Anthropic
555
- * rejects (#1767), so the request fails locally with a 400 before any upstream fetch.
556
- */
557
- export function sanitizePassthroughToolCallIds(messages: unknown[]): void {
558
- const blocks: Rec[] = [];
559
- for (const message of messages) {
560
- if (!isRec(message) || !Array.isArray(message.content)) continue;
561
- for (const block of message.content) if (isRec(block)) blocks.push(block);
562
- }
563
- const fieldOf = (block: Rec): "id" | "tool_use_id" | undefined => {
564
- if (typeof block.type !== "string") return undefined;
565
- if (block.type.endsWith("tool_use")) return "id";
566
- if (block.type.endsWith("tool_result")) return "tool_use_id";
567
- return undefined;
568
- };
569
- const callIds = createToolCallIdAllocator();
570
- for (const block of blocks) {
571
- const field = fieldOf(block);
572
- if (field && typeof block[field] === "string") callIds.reserve(block[field] as string);
573
- }
574
- for (const block of blocks) {
575
- const field = fieldOf(block);
576
- if (!field || typeof block[field] !== "string") continue;
577
- const wire = callIds.allocate(block[field] as string);
578
- if (wire === undefined) throw new AnthropicRequestError(`${block.type} block has an empty ${field}`);
579
- block[field] = wire;
580
- }
581
- }
310
+ /** Upstream error types a non-stream passthrough failure may report in history. */
311
+ const PASSTHROUGH_ERROR_TYPES = [
312
+ "invalid_request_error", "authentication_error", "permission_error", "not_found_error",
313
+ "rate_limit_error", "api_error", "overloaded_error", "request_too_large",
314
+ ] as const;
315
+ const PASSTHROUGH_ERROR_DETAIL_MAX_CHARS = 64 * 1024;
582
316
 
583
317
  async function anthropicNativePassthrough(
584
318
  req: Request,
@@ -694,35 +428,6 @@ async function anthropicNativePassthrough(
694
428
  });
695
429
  }
696
430
 
697
- const PASSTHROUGH_ERROR_TYPES = [
698
- "invalid_request_error", "authentication_error", "permission_error", "not_found_error",
699
- "rate_limit_error", "api_error", "overloaded_error", "request_too_large",
700
- ] as const;
701
- const PASSTHROUGH_ERROR_DETAIL_MAX_CHARS = 64 * 1024;
702
- const DEFAULT_BODY_STALL_SEC = 90;
703
- const DEFAULT_BODY_MAX_BYTES = 64 * 1024 * 1024;
704
-
705
- /**
706
- * Normalize the claudeCode body-guard config (devlog 260716_passthrough_followups/010).
707
- * Policy: exactly 0 disables; finite positive values are honored (stall clamped to
708
- * min 1s); negative/non-finite/absent values fall back to the defaults.
709
- */
710
- export function resolvePassthroughBodyGuard(config: OcxConfig, reqSignal?: AbortSignal): PassthroughBodyGuard {
711
- const rawSec = config.claudeCode?.bodyStallSec;
712
- const stallSec = rawSec === 0
713
- ? 0
714
- : typeof rawSec === "number" && Number.isFinite(rawSec) && rawSec > 0
715
- ? Math.max(1, rawSec)
716
- : DEFAULT_BODY_STALL_SEC;
717
- const rawBytes = config.claudeCode?.bodyMaxBytes;
718
- const maxBytes = rawBytes === 0
719
- ? 0
720
- : typeof rawBytes === "number" && Number.isFinite(rawBytes) && rawBytes > 0
721
- ? Math.floor(rawBytes)
722
- : DEFAULT_BODY_MAX_BYTES;
723
- return { stallMs: stallSec * 1000, maxBytes, ...(reqSignal ? { reqSignal } : {}) };
724
- }
725
-
726
431
  type BoundedPassthroughBody =
727
432
  | { kind: "ok"; text: string }
728
433
  | { kind: "stall" }
@@ -850,6 +555,21 @@ export async function handleClaudeMessages(
850
555
  }
851
556
  }
852
557
 
558
+ /** The client answer for a Messages body the translation (or the direct decode) refused. */
559
+ function translationFailure(err: unknown, logCtx: RequestLogContext, logIds: { requestId: string; start: number } | undefined): Response {
560
+ const overflow = isTranslatorBudgetExceededError(err);
561
+ const unavailable = err instanceof DesktopModelMappingUnavailableError;
562
+ const status = overflow ? 413 : unavailable ? 503 : err instanceof AnthropicRequestError ? 400 : 500;
563
+ if (logIds) addFinalRequestLog(logIds.requestId, logIds.start, logCtx, status, { closeReason: "non_stream" });
564
+ if (unavailable) return desktopMappingUnavailableResponse(err);
565
+ return anthropicErrorResponse(
566
+ status,
567
+ overflow ? "request translation buffer exceeded the safe limit" : err instanceof Error ? err.message : String(err),
568
+ overflow ? "request_too_large" : undefined,
569
+ overflow ? "translation_buffer_limit" : undefined,
570
+ );
571
+ }
572
+
853
573
  /**
854
574
  * Translate a Claude Messages request, route it through the Responses pipeline,
855
575
  * and translate the reply back. Runs under a translator budget owned by the
@@ -885,6 +605,11 @@ async function handleClaudeMessagesWithBudget(
885
605
  let effortRow: ParsedEffortRowId | null = null;
886
606
  let fastRow: ParsedFastRowId | null = null;
887
607
  let requestedModel = "";
608
+ // Antigravity direct lane (off by default): planned before translation, kept only if routing
609
+ // settles on the route the plan was made for.
610
+ let directPlan: MessagesDirectPlan | undefined;
611
+ /** The retained charge for the settings-only body, handed back if the plan is left. */
612
+ let directSettingsBodyBytes = 0;
888
613
  // Built only under the reject policy; the legacy default leaves this request untouched.
889
614
  let envelope: ProtocolEnvelope | undefined;
890
615
  let messagesFeatures: () => Iterable<ProtocolFeature> = () => [];
@@ -1034,9 +759,16 @@ async function handleClaudeMessagesWithBudget(
1034
759
  };
1035
760
  delete anthropicBody.thinking;
1036
761
  }
1037
- let translation;
762
+ // On the direct lane the core receives only the request settings; the conversation is
763
+ // already decoded into the IR. Without a plan the translation below runs as before.
764
+ directPlan = planMessagesAntigravityDirect(anthropicBody, cc, config, translatorBudget);
765
+ let translation = directPlan && {
766
+ body: directPlan.settingsBody,
767
+ cacheKeySource: directPlan.cacheKeySource,
768
+ nativeReasoningReplay: directPlan.conversation.nativeReasoningReplay,
769
+ };
1038
770
  try {
1039
- translation = messagesToResponsesTranslation(anthropicBody, cc, translatorBudget);
771
+ translation ??= messagesToResponsesTranslation(anthropicBody, cc, translatorBudget);
1040
772
  } catch (err) {
1041
773
  // A native Messages route can discard an undecodable proxy envelope. Keep the
1042
774
  // original error for any route that ultimately translates to Responses.
@@ -1052,21 +784,13 @@ async function handleClaudeMessagesWithBudget(
1052
784
  // fields only, so the caller intent is applied to the TRANSLATED body rather than the
1053
785
  // inbound one.
1054
786
  if (fastRow) internalBody.service_tier = "priority";
1055
- translatorBudget.chargeRetained(jsonUtf8Bytes(internalBody), { kind: "request_copies" });
787
+ const internalBodyBytes = jsonUtf8Bytes(internalBody);
788
+ translatorBudget.chargeRetained(internalBodyBytes, { kind: "request_copies" });
789
+ if (directPlan) directSettingsBodyBytes = internalBodyBytes;
1056
790
  cacheKeySource = translation.cacheKeySource;
1057
791
  nativeReasoningReplay = translation.nativeReasoningReplay;
1058
792
  } catch (err) {
1059
- const overflow = isTranslatorBudgetExceededError(err);
1060
- const unavailable = err instanceof DesktopModelMappingUnavailableError;
1061
- const status = overflow ? 413 : unavailable ? 503 : err instanceof AnthropicRequestError ? 400 : 500;
1062
- if (logIds) addFinalRequestLog(logIds.requestId, logIds.start, logCtx, status, { closeReason: "non_stream" });
1063
- if (unavailable) return desktopMappingUnavailableResponse(err);
1064
- return anthropicErrorResponse(
1065
- status,
1066
- overflow ? "request translation buffer exceeded the safe limit" : err instanceof Error ? err.message : String(err),
1067
- overflow ? "request_too_large" : undefined,
1068
- overflow ? "translation_buffer_limit" : undefined,
1069
- );
793
+ return translationFailure(err, logCtx, logIds);
1070
794
  }
1071
795
 
1072
796
  if (!requestedModel) requestedModel = (anthropicBody as Rec).model as string;
@@ -1098,8 +822,9 @@ async function handleClaudeMessagesWithBudget(
1098
822
 
1099
823
  let clientEncoder: ClientEncoderOption | undefined;
1100
824
  // Native ChatGPT passthrough (openai-responses forward) accepts only Codex-shaped
1101
- // bodies: it 400s on sampling params ("Unsupported parameter: max_output_tokens",
1102
- // verified live 2026-07-11). Strip them for that route; routed providers keep them.
825
+ // bodies: it 400s on sampling params (verified live 2026-07-11). Every openai-responses
826
+ // route drops temperature/top_p/stop/user below; max_output_tokens is left to
827
+ // stripUnsupportedForwardParams (canonical-forward.ts), which drops it only for that backend.
1103
828
  let settledRoute: RouteResult | undefined;
1104
829
  /**
1105
830
  * Which replayed thinking fields the send that actually happened will serialize.
@@ -1119,8 +844,30 @@ async function handleClaudeMessagesWithBudget(
1119
844
  }
1120
845
  return requestTokenFloor;
1121
846
  };
847
+ /** Leave the direct plan: the core gets the full bridge translation, as without the switch. */
848
+ const leaveDirectPlan = (): unknown => {
849
+ // Hand back everything the plan retained: its decode and the settings-only body it replaces.
850
+ directPlan?.releaseDecodeCharges();
851
+ directPlan = undefined;
852
+ nativeReasoningReplay = undefined;
853
+ translatorBudget.releaseRetained(directSettingsBodyBytes, { kind: "request_copies" });
854
+ directSettingsBodyBytes = 0;
855
+ try {
856
+ const translation = messagesToResponsesTranslation(anthropicBody, cc, translatorBudget);
857
+ internalBody = translation.body;
858
+ if (fastRow) internalBody.service_tier = "priority";
859
+ internalBody.stream = true;
860
+ translatorBudget.chargeRetained(jsonUtf8Bytes(internalBody), { kind: "request_copies" });
861
+ cacheKeySource = translation.cacheKeySource;
862
+ nativeReasoningReplay = translation.nativeReasoningReplay;
863
+ return undefined;
864
+ } catch (err) {
865
+ return err;
866
+ }
867
+ };
868
+ let directLeaveError: unknown;
1122
869
  try {
1123
- const route = routeModel(config, internalBody.model as string, evidenceFromBody(internalBody));
870
+ const route = routeModel(config, internalBody.model as string, directPlan ? directPlan.evidence : evidenceFromBody(internalBody));
1124
871
  // Settle the wire once so the sampling decision below reads the effective
1125
872
  // adapter rather than the provider-wide default (#404).
1126
873
  route.staticPolicy = captureRouteStaticPolicy(
@@ -1138,11 +885,14 @@ async function handleClaudeMessagesWithBudget(
1138
885
  route.provider = resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, "anthropic", route.staticPolicy);
1139
886
  logCtx.routeDecision = route.routeDecision;
1140
887
  settledRoute = route;
888
+ // Before any route-specific shaping, so a request that leaves the lane is shaped as the bridge's.
889
+ if (directPlan && !messagesDirectPlanHolds(directPlan, route, anthropicBody as Rec, config)) directLeaveError = leaveDirectPlan();
1141
890
  if (directEncodersApply(config, route)) {
1142
891
  clientEncoder = { protocol: "messages", stream, model: requestedModel, inputTokenFloor: claudeRequestTokenFloor() };
1143
892
  }
1144
893
  if (route.provider.adapter === "openai-responses") {
1145
- delete internalBody.max_output_tokens;
894
+ // max_output_tokens is the caller's spend cap: the adapter strips it only for the canonical
895
+ // ChatGPT backend that rejects it (#5753), so other Responses gateways keep the cap.
1146
896
  delete internalBody.temperature;
1147
897
  delete internalBody.top_p;
1148
898
  delete internalBody.stop;
@@ -1183,6 +933,14 @@ async function handleClaudeMessagesWithBudget(
1183
933
  }
1184
934
  /* unknown model: let handleResponses shape the 404 */
1185
935
  }
936
+ if (directPlan && !settledRoute) directLeaveError ??= leaveDirectPlan();
937
+ if (directLeaveError !== undefined) return translationFailure(directLeaveError, logCtx, logIds);
938
+ const directRoute = directPlan && settledRoute ? settledRoute : undefined;
939
+ // The direct lane answers through the direct encoder whatever `directEncoders` says: neither
940
+ // direction passes through the internal Responses body.
941
+ if (directRoute) {
942
+ clientEncoder = { protocol: "messages", stream, model: requestedModel, inputTokenFloor: claudeRequestTokenFloor(), requestLane: "direct" };
943
+ }
1186
944
 
1187
945
  // PF-08: a managed-key Anthropic route sends its Messages body natively. The caller-forward
1188
946
  // passthrough above was decided on the caller's own credential and never reaches this point.
@@ -1206,11 +964,20 @@ async function handleClaudeMessagesWithBudget(
1206
964
  return anthropicErrorResponse(400, nativeProjectionOnlyError.message, "invalid_request_error");
1207
965
  }
1208
966
  // With the switch on, a declined route says why on its bridge mark, as native Chat does.
1209
- if (nativeDecline && nativeDecline !== "rollout-disabled") {
967
+ const antigravityDecline = settledRoute && !nativeMessagesRoute && !directRoute
968
+ ? messagesDirectTraceDecline(settledRoute, anthropicBody, String(internalBody.model ?? ""), config)
969
+ : undefined;
970
+ if (directRoute) {
971
+ markProtocolEntry(logCtx, { inbound: "messages", lane: "direct", features: messagesFeatures });
972
+ } else if ((nativeDecline && nativeDecline !== "rollout-disabled") || antigravityDecline) {
1210
973
  markProtocolEntry(logCtx, {
1211
974
  inbound: "messages",
1212
975
  lane: "bridge",
1213
- reasonCodes: [...(effortRow ? ["effort-row" as const] : fastRow ? ["fast-row" as const] : []), nativeDecline],
976
+ reasonCodes: [
977
+ ...(effortRow ? ["effort-row" as const] : fastRow ? ["fast-row" as const] : []),
978
+ ...(nativeDecline && nativeDecline !== "rollout-disabled" ? [nativeDecline] : []),
979
+ ...(antigravityDecline ? [antigravityDecline] : []),
980
+ ],
1214
981
  features: messagesFeatures,
1215
982
  });
1216
983
  }
@@ -1218,7 +985,11 @@ async function handleClaudeMessagesWithBudget(
1218
985
  if (envelope && settledRoute && !settledRoute.combo && settledRoute.routeKind !== "policy") {
1219
986
  const verdict = checkRepresentable({
1220
987
  inbound: "messages",
1221
- requestPath: requestPathForLane("messages", nativeMessagesRoute ? "native" : "bridge", upstreamWireForAdapter(settledRoute.provider.adapter)),
988
+ requestPath: requestPathForLane(
989
+ "messages",
990
+ nativeMessagesRoute ? "native" : directRoute ? "direct" : "bridge",
991
+ upstreamWireForAdapter(settledRoute.provider.adapter),
992
+ ),
1222
993
  features: envelope.features(),
1223
994
  policy: "reject",
1224
995
  });
@@ -1337,7 +1108,11 @@ async function handleClaudeMessagesWithBudget(
1337
1108
  // BEFORE translation (the translated Anthropic stream has no response.completed
1338
1109
  // frame, so tapping it records a bogus 502 with no usage/cache detail).
1339
1110
  const finalizeNativeLog = createFinalRequestLog(logIds, logCtx).finish;
1340
- const upstream = await handleResponses(internalReq, buildClaudeReplayConfig(config), logCtx, {
1111
+ const upstream = await handleResponses(internalReq, {
1112
+ ...buildClaudeReplayConfig(config),
1113
+ // Operator config wins; otherwise the caller's own 1h breakpoints keep 1h here.
1114
+ cacheRetention: config.cacheRetention ?? callerCacheRetention(anthropicBody),
1115
+ }, logCtx, {
1341
1116
  // Routing keeps Claude-only sidecar overrides; admission policy must follow the live owner.
1342
1117
  codexAuthPolicy: config,
1343
1118
  ...(logIds?.admission ? { admission: logIds.admission } : {}),
@@ -1348,8 +1123,16 @@ async function handleClaudeMessagesWithBudget(
1348
1123
  // Without this the replay would look native and a Responses-scoped wire default
1349
1124
  // would fire, disagreeing with the pre-flight decision above.
1350
1125
  inboundWire: "anthropic",
1126
+ // An explicit empty `tools` array is a deny-all declaration the translation drops with
1127
+ // the rest of the catalog; carried separately so the undeclared-tool guard can still
1128
+ // enforce it while partial and absent catalogs stay deferred.
1129
+ clientDeclaredEmptyToolCatalog: isRec(anthropicBody)
1130
+ && Array.isArray(anthropicBody.tools)
1131
+ && anthropicBody.tools.length === 0,
1351
1132
  nativeReasoningReplay,
1352
1133
  nativeReasoningMint,
1134
+ // Out of body like the replay above: the core hands these to an Anthropic destination only.
1135
+ anthropicServerTools: declaredAnthropicServerTools(anthropicBody),
1353
1136
  claudeGoAffinity: { sessionLane: claudeGoSessionLane },
1354
1137
  claudeNativeSessionId,
1355
1138
  stripClaudeMainAuthForNoncanonicalForward: true,
@@ -1362,6 +1145,9 @@ async function handleClaudeMessagesWithBudget(
1362
1145
  onNativePassthroughTerminal: status => finalizeNativeLog(httpStatusForRequestLogTerminal(status, logCtx), { terminalStatus: status, closeReason: "terminal" }),
1363
1146
  onNativePassthroughCancel: () => finalizeNativeLog(499, { closeReason: "client_cancel" }),
1364
1147
  ...(clientEncoder ? { clientEncoder } : {}),
1148
+ ...(directRoute && directPlan
1149
+ ? { directRequest: messagesDirectRequestOption(directPlan, anthropicBody, cc, translatorBudget) }
1150
+ : {}),
1365
1151
  });
1366
1152
  const response = logIds ? responseWithDeferredRequestLog(upstream, logIds.requestId, logIds.start, logCtx) : upstream;
1367
1153
  // Already in the Messages wire (direct encoder): no conversion.
@@ -1374,11 +1160,13 @@ async function handleClaudeMessagesWithBudget(
1374
1160
  // Re-shape the OpenAI-style error envelope into the Anthropic one, preserving status.
1375
1161
  let message = `upstream error (${response.status})`;
1376
1162
  let contextError = false;
1163
+ let catalogRefusal = false;
1377
1164
  try {
1378
1165
  const text = await response.text();
1379
1166
  try {
1380
1167
  const parsed = JSON.parse(text) as { error?: { message?: string; type?: string; code?: unknown } | string; message?: string };
1381
1168
  contextError = typeof parsed?.error === "object" && parsed.error?.code === "context_length_exceeded";
1169
+ catalogRefusal = typeof parsed?.error === "object" && parsed.error?.code === UNDECLARED_TOOL_CALL_ERROR_CODE;
1382
1170
  const nested = typeof parsed?.error === "object" && parsed.error ? parsed.error.message : undefined;
1383
1171
  const flat = typeof parsed?.error === "string" ? parsed.error : parsed?.message;
1384
1172
  message = nested || flat || (text ? `upstream error (${response.status}): ${text.slice(0, 400)}` : message);
@@ -1407,7 +1195,10 @@ async function handleClaudeMessagesWithBudget(
1407
1195
  const nativeMainFence = response.status === 503
1408
1196
  && upstreamRetryAfter?.trim() === "1"
1409
1197
  && message === CODEX_MAIN_PROFILE_MAINTENANCE_MESSAGE;
1410
- const transient = !replayRefusal && !nativeMainFence && !contextError && isTransientUpstreamStatus(response.status);
1198
+ // The undeclared-tool guard's refusal is deterministic: the same request produces the
1199
+ // same verdict, so it must not arrive as a retryable overload like a transient 5xx.
1200
+ const transient = !replayRefusal && !nativeMainFence && !contextError && !catalogRefusal
1201
+ && isTransientUpstreamStatus(response.status);
1411
1202
  const outStatus = replayRefusal
1412
1203
  ? REPLAY_REFUSED_STATUS
1413
1204
  : nativeMainFence ? 503 : contextError ? 400 : transient ? 529 : response.status;
@@ -1419,7 +1210,11 @@ async function handleClaudeMessagesWithBudget(
1419
1210
  outStatus,
1420
1211
  message,
1421
1212
  undefined,
1422
- replayRefusal ? UPSTREAM_RESET_REPLAY_REFUSED_CODE : contextError ? "context_length_exceeded" : undefined,
1213
+ replayRefusal
1214
+ ? UPSTREAM_RESET_REPLAY_REFUSED_CODE
1215
+ : contextError
1216
+ ? "context_length_exceeded"
1217
+ : catalogRefusal ? UNDECLARED_TOOL_CALL_ERROR_CODE : undefined,
1423
1218
  )), {
1424
1219
  status: outStatus,
1425
1220
  headers: outHeaders,
@@ -1470,8 +1265,17 @@ async function handleClaudeMessagesWithBudget(
1470
1265
  "translation_buffer_limit",
1471
1266
  );
1472
1267
  }
1268
+ // A declared client refusal keeps its 4xx: as a 502 Claude Code retries a verdict that cannot
1269
+ // change. Server classes and rate limits keep the 502 this path has always answered. A 400
1270
+ // worded as a client close answers the 499 httpStatusFromTerminalError logs for it.
1271
+ const refusalStatus = recognizedUpstreamError(translatedError ?? {})?.httpStatus;
1473
1272
  return new Response(JSON.stringify(message), {
1474
- status: isError ? (translatedError?.code === "context_length_exceeded" ? 400 : 502) : 200,
1273
+ status: isError
1274
+ ? translatedError?.code === "context_length_exceeded" ? 400
1275
+ : refusalStatus !== undefined && refusalStatus < 429
1276
+ ? refusalStatus === 400 && isClientClosedMessage(String(translatedError?.message ?? "")) ? 499 : refusalStatus
1277
+ : 502
1278
+ : 200,
1475
1279
  headers: { "Content-Type": "application/json" },
1476
1280
  });
1477
1281
  }
@@ -1533,79 +1337,6 @@ async function handleClaudeMessagesWithBudget(
1533
1337
  });
1534
1338
  }
1535
1339
 
1536
- /** Per-attachment token estimate for a base64 payload: real image dimensions when the
1537
- * header is sniffable (Anthropic prices images at ~pixels/750), else decoded bytes/512,
1538
- * min 256 — the same shape as the Kiro usage estimator (estimateKiroImageTokens). */
1539
- function estimateBase64AttachmentTokens(data: string): number {
1540
- const dims = sniffImageDimensions(data);
1541
- if (dims) return Math.max(256, Math.ceil((dims.width * dims.height) / 750));
1542
- const unpadded = data.endsWith("==") ? data.length - 2 : data.endsWith("=") ? data.length - 1 : data.length;
1543
- return Math.max(256, Math.ceil(Math.floor((unpadded * 3) / 4) / 512));
1544
- }
1545
-
1546
- /**
1547
- * Char-based token estimate for an Anthropic-shaped request body. Base64 attachment
1548
- * payloads (image/document blocks in message content, including blocks nested in
1549
- * tool_result.content) are counted as a bounded per-attachment estimate instead of raw
1550
- * characters: one 2MB screenshot is ~2.7M base64 chars, which the plain chars/token
1551
- * divide reports as hundreds of thousands of tokens versus a real cost around 1.6k.
1552
- * That breaks the >2x drift bound the estimator is held to (devlog 260711_claude_inbound
1553
- * 040 §3); a live 260-message turn whose replayed thinking was 78.8% of the body breached
1554
- * it at 3.28x, which is why the estimate is projected onto the settled route. Text and url
1555
- * sources are left in place and counted as characters, as is
1556
- * anything outside protocol content positions (tool_use.input, tool schemas).
1557
- *
1558
- * `thinking` selects which replayed thinking fields the SETTLED route serializes, so the measure
1559
- * describes the prompt this proxy forwards rather than the one the caller typed. Omitted, the
1560
- * whole body counts — correct for the Anthropic-native wire, where nothing is projected away.
1561
- * See `claude-request-projection.ts` for why a routed wire must project it out.
1562
- */
1563
- export function estimateClaudeRequestTokens(
1564
- raw: { system?: unknown; messages?: unknown; tools?: unknown },
1565
- modelId: string | undefined,
1566
- thinking: ClaudeThinkingProjection = CLAUDE_NATIVE_THINKING,
1567
- ): number {
1568
- let attachmentTokens = 0;
1569
- // Blank base64 payloads ONLY in protocol content positions: message content blocks and
1570
- // blocks nested in tool_result.content. tool_use.input and tool schemas can legitimately
1571
- // contain attachment-shaped JSON, and those bytes ARE serialized into function_call
1572
- // arguments / tool definitions for routed providers, so they must keep counting as text.
1573
- // system is text-only per the Anthropic protocol (no attachment sources), so it is
1574
- // stringified as-is.
1575
- const sanitizeBlock = (block: unknown): unknown => {
1576
- if (!block || typeof block !== "object") return block;
1577
- const b = block as Record<string, unknown>;
1578
- if (b.type === "image" || b.type === "document") {
1579
- const source = b.source as { type?: unknown; data?: unknown } | undefined;
1580
- if (source && typeof source === "object" && source.type === "base64" && typeof source.data === "string") {
1581
- attachmentTokens += estimateBase64AttachmentTokens(source.data);
1582
- return { ...b, source: { ...(source as Record<string, unknown>), data: "" } };
1583
- }
1584
- return block;
1585
- }
1586
- if (b.type === "tool_result" && Array.isArray(b.content)) {
1587
- return { ...b, content: (b.content as unknown[]).map(sanitizeBlock) };
1588
- }
1589
- return block;
1590
- };
1591
- const sanitizedMessages = (messages: unknown): unknown =>
1592
- Array.isArray(messages)
1593
- ? messages.map(message => {
1594
- if (!message || typeof message !== "object") return message;
1595
- const m = message as Record<string, unknown>;
1596
- return Array.isArray(m.content) ? { ...m, content: (m.content as unknown[]).map(sanitizeBlock) } : message;
1597
- })
1598
- : messages;
1599
- const parts: string[] = [];
1600
- if (raw.system !== undefined) parts.push(typeof raw.system === "string" ? raw.system : JSON.stringify(raw.system));
1601
- if (raw.messages !== undefined) {
1602
- const projected = projectClaudeRequest(raw, thinking);
1603
- parts.push(JSON.stringify(sanitizedMessages(projected.messages)));
1604
- }
1605
- if (raw.tools !== undefined) parts.push(JSON.stringify(raw.tools));
1606
- return Math.max(1, estimateTokens(parts.join("\n"), modelId) + attachmentTokens);
1607
- }
1608
-
1609
1340
  /**
1610
1341
  * The projection for a route that has already settled.
1611
1342
  *