@bitkyc08/opencodex 2.59.0 → 2.61.0-preview.20260922

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 (374) hide show
  1. package/AGENTS_INSTALL.md +64 -0
  2. package/README.md +28 -1
  3. package/bin/ocx.mjs +382 -209
  4. package/gui/dist/assets/App-E64Rzjap.js +50 -0
  5. package/gui/dist/assets/Tray-_nfzD8k4.js +1 -0
  6. package/gui/dist/assets/index-DpdfZWMK.js +86 -0
  7. package/gui/dist/assets/index-_bpvxJu0.css +1 -0
  8. package/gui/dist/assets/usage-companion-chart-DtoK7T6h.js +1 -0
  9. package/gui/dist/favicon.png +0 -0
  10. package/gui/dist/index.html +2 -2
  11. package/gui/dist/provider-icons/stepfun-color.svg +1 -0
  12. package/package.json +8 -1
  13. package/src/adapters/anthropic-image-codec.ts +16 -2
  14. package/src/adapters/anthropic-image-normalize.ts +49 -2
  15. package/src/adapters/anthropic.ts +20 -1
  16. package/src/adapters/base.ts +23 -0
  17. package/src/adapters/coding-agent/protocol.ts +36 -6
  18. package/src/adapters/coding-agent/turn.ts +32 -4
  19. package/src/adapters/command-code.ts +52 -4
  20. package/src/adapters/cursor/catalog.ts +51 -7
  21. package/src/adapters/cursor/checkpoint-store.ts +3 -0
  22. package/src/adapters/cursor/discovery.ts +11 -8
  23. package/src/adapters/cursor/live-transport.ts +26 -9
  24. package/src/adapters/cursor/protobuf-request.ts +6 -3
  25. package/src/adapters/cursor/request-builder.ts +20 -4
  26. package/src/adapters/cursor/transport.ts +19 -0
  27. package/src/adapters/cursor.ts +14 -3
  28. package/src/adapters/declaration-carrier.ts +45 -0
  29. package/src/adapters/devin/cloud-direct/chat.ts +3 -1
  30. package/src/adapters/devin/cloud-direct/stated-reset-retry.ts +42 -5
  31. package/src/adapters/devin.ts +125 -36
  32. package/src/adapters/google-antigravity-replay.ts +1 -1
  33. package/src/adapters/google-antigravity-wire.ts +34 -7
  34. package/src/adapters/google-errors.ts +7 -1
  35. package/src/adapters/google-http.ts +49 -10
  36. package/src/adapters/google-tool-schema.ts +595 -31
  37. package/src/adapters/google-wire-compiler.ts +93 -10
  38. package/src/adapters/google-wire-shape.ts +461 -0
  39. package/src/adapters/google.ts +66 -11
  40. package/src/adapters/image.ts +4 -1
  41. package/src/adapters/input-media-guard.ts +21 -9
  42. package/src/adapters/kiro/usage.ts +3 -2
  43. package/src/adapters/kiro-tool-fallback.ts +1 -1
  44. package/src/adapters/ollama-native.ts +6 -0
  45. package/src/adapters/openai-chat/developer-role.ts +61 -0
  46. package/src/adapters/openai-chat/messages.ts +46 -27
  47. package/src/adapters/openai-chat/parallel-tool-calls.ts +32 -0
  48. package/src/adapters/openai-chat/passthrough.ts +33 -9
  49. package/src/adapters/openai-chat/reasoning-wire.ts +89 -0
  50. package/src/adapters/openai-chat-images.ts +3 -1
  51. package/src/adapters/openai-chat.ts +23 -58
  52. package/src/adapters/openai-responses/image-gen.ts +8 -6
  53. package/src/adapters/openai-responses/passthrough.ts +17 -3
  54. package/src/adapters/openai-responses/reasoning.ts +7 -0
  55. package/src/adapters/opencode-go-additional-tools.ts +12 -2
  56. package/src/adapters/registry.ts +3 -2
  57. package/src/adapters/run-turn-queue.ts +178 -29
  58. package/src/adapters/xai-web-search.ts +16 -1
  59. package/src/bridge/errors.ts +8 -2
  60. package/src/bridge/response-json.ts +9 -1
  61. package/src/bridge/sse.ts +13 -151
  62. package/src/chat/inbound.ts +141 -5
  63. package/src/claude/desktop-3p.ts +7 -1
  64. package/src/claude/desktop-first-party.ts +183 -0
  65. package/src/claude/desktop-gateway-state.ts +41 -0
  66. package/src/claude/inbound-content-options.ts +6 -0
  67. package/src/claude/inbound.ts +32 -6
  68. package/src/claude/intercept/connect-proxy.ts +179 -0
  69. package/src/claude/intercept/listener.ts +122 -0
  70. package/src/claude/intercept/local-ca.ts +298 -0
  71. package/src/claude/intercept/runtime.ts +98 -0
  72. package/src/claude/intercept/settings.ts +189 -0
  73. package/src/cli/access.ts +87 -0
  74. package/src/cli/account-auth.ts +19 -0
  75. package/src/cli/account-extended.ts +4 -4
  76. package/src/cli/capabilities.ts +31 -0
  77. package/src/cli/claude-desktop.ts +206 -16
  78. package/src/cli/codex-shim-autorestore.ts +3 -0
  79. package/src/cli/companion.ts +56 -0
  80. package/src/cli/dispatch.ts +46 -7
  81. package/src/cli/doctor.ts +28 -9
  82. package/src/cli/ensure-desired-integrations.ts +43 -5
  83. package/src/cli/help.ts +7 -9
  84. package/src/cli/hub.ts +3 -2
  85. package/src/cli/index.ts +203 -59
  86. package/src/cli/init.ts +8 -0
  87. package/src/cli/integrations.ts +7 -1
  88. package/src/cli/opencode.ts +2 -2
  89. package/src/cli/provider.ts +13 -1
  90. package/src/cli/registry.ts +41 -2
  91. package/src/cli/resolve.ts +230 -0
  92. package/src/cli/root.ts +24 -1
  93. package/src/cli/start-ownership-publication.ts +56 -0
  94. package/src/cli/status-probes.ts +2 -18
  95. package/src/cli/status.ts +62 -0
  96. package/src/cli/stop-report.ts +143 -0
  97. package/src/cli/uninstall-plan.ts +9 -0
  98. package/src/client/machine-api.ts +2 -2
  99. package/src/client/machine-listener.ts +4 -7
  100. package/src/client/runtime.ts +26 -2
  101. package/src/clients/aside-profiles.ts +4 -0
  102. package/src/clients/config-export/zcode-store.ts +157 -0
  103. package/src/clients/config-export.ts +36 -0
  104. package/src/codex/account-store.ts +65 -0
  105. package/src/codex/app-server-processes.ts +72 -40
  106. package/src/codex/auth-api/account-list.ts +19 -11
  107. package/src/codex/auth-api/login-flow.ts +6 -1
  108. package/src/codex/auth-api/pool-quota-probe.ts +30 -7
  109. package/src/codex/autostart-health.ts +28 -0
  110. package/src/codex/catalog/build-entries.ts +2 -2
  111. package/src/codex/catalog/effort.ts +3 -3
  112. package/src/codex/catalog/gather-capture.ts +21 -2
  113. package/src/codex/catalog/model-hints.ts +29 -28
  114. package/src/codex/catalog/parsing.ts +7 -0
  115. package/src/codex/catalog/provider-models.ts +43 -17
  116. package/src/codex/catalog/retained-sync.ts +24 -28
  117. package/src/codex/catalog/routed-gather.ts +19 -0
  118. package/src/codex/context-compat.ts +5 -2
  119. package/src/codex/convergence.ts +2 -2
  120. package/src/codex/desired-state.ts +4 -1
  121. package/src/codex/history-job.ts +6 -6
  122. package/src/codex/history-provider.ts +31 -166
  123. package/src/codex/history-rollout-read.ts +174 -0
  124. package/src/codex/inject/config-toml.ts +41 -6
  125. package/src/codex/inject/paginated-openai-compat.ts +90 -0
  126. package/src/codex/inject.ts +18 -15
  127. package/src/codex/injected-marker.ts +18 -0
  128. package/src/codex/internal/catalog-writer.ts +33 -1
  129. package/src/codex/main-account.ts +6 -0
  130. package/src/codex/model-cache.ts +99 -6
  131. package/src/codex/model-entitlement-admission.ts +59 -0
  132. package/src/codex/model-entitlements.ts +116 -54
  133. package/src/codex/native-main-admission.ts +83 -0
  134. package/src/codex/observed-model-denials.ts +101 -8
  135. package/src/codex/prompt-text-probe.ts +9 -6
  136. package/src/codex/routing/health-store.ts +39 -0
  137. package/src/codex/routing/selection.ts +37 -1
  138. package/src/codex/routing.ts +12 -42
  139. package/src/codex/shim-templates.ts +29 -3
  140. package/src/codex/shim.ts +1 -1
  141. package/src/codex/subagent-model-fallback.ts +22 -4
  142. package/src/combos/failover.ts +3 -0
  143. package/src/companion/settings.ts +132 -0
  144. package/src/config/admitted-identity.ts +222 -0
  145. package/src/config/atomic-write.ts +117 -5
  146. package/src/config/diagnostics.ts +22 -1
  147. package/src/config/feature-flags.ts +5 -0
  148. package/src/config/load-degrade.ts +52 -7
  149. package/src/config/process-state.ts +1 -1
  150. package/src/config/proxy-env.ts +8 -2
  151. package/src/config/schema/compaction-triggers.ts +11 -0
  152. package/src/config/schema/config-schema.ts +31 -1
  153. package/src/config/schema/leaf-validators.ts +59 -0
  154. package/src/config.ts +3 -3
  155. package/src/generated/compatibility-version.json +604 -280
  156. package/src/grok/reset-coupons.ts +38 -19
  157. package/src/images/loop.ts +6 -1
  158. package/src/integrations/aside-profile-context.ts +37 -3
  159. package/src/integrations/aside-profile-journal.ts +68 -3
  160. package/src/integrations/aside-profiles.ts +128 -3
  161. package/src/integrations/config-io.ts +44 -10
  162. package/src/integrations/merge.ts +120 -13
  163. package/src/integrations/mutation-plan.ts +921 -0
  164. package/src/integrations/registry.ts +38 -0
  165. package/src/integrations/state.ts +78 -45
  166. package/src/integrations/target.ts +208 -0
  167. package/src/integrations/writer.ts +134 -110
  168. package/src/lab/conformance/fixture-provider.ts +5 -0
  169. package/src/lab/live/transport.ts +4 -0
  170. package/src/lab/live/types.ts +5 -0
  171. package/src/lab/subject/behavior-fingerprint.ts +1 -1
  172. package/src/lib/admin-secrets.ts +9 -1
  173. package/src/lib/browser-launch-notice.ts +59 -0
  174. package/src/lib/bun-runtime.ts +6 -2
  175. package/src/lib/debug-log-buffer.ts +6 -1
  176. package/src/lib/debug.ts +63 -0
  177. package/src/lib/errors.ts +79 -0
  178. package/src/lib/http-response-semantics.ts +57 -0
  179. package/src/lib/lab-live-pinned-sender.ts +26 -12
  180. package/src/lib/open-url.ts +51 -7
  181. package/src/lib/package-tree-integrity.ts +2 -1
  182. package/src/lib/package-version.ts +8 -0
  183. package/src/lib/pinned-http.ts +142 -2
  184. package/src/lib/plain-data.ts +103 -0
  185. package/src/lib/process-control.ts +13 -5
  186. package/src/lib/provider-egress.ts +310 -0
  187. package/src/lib/provider-outbound.ts +109 -16
  188. package/src/lib/proxy-env.ts +82 -7
  189. package/src/lib/request-execution-budget.ts +72 -0
  190. package/src/lib/request-failure-attribution.ts +183 -0
  191. package/src/lib/request-failure-model.ts +236 -0
  192. package/src/lib/request-resend-gate.ts +138 -0
  193. package/src/lib/socks5-fetch.ts +136 -26
  194. package/src/lib/spend-ledger-owner.ts +364 -0
  195. package/src/lib/spend-reservation-ledger.ts +218 -27
  196. package/src/lib/standalone.ts +16 -0
  197. package/src/lib/upstream-retry.ts +167 -16
  198. package/src/lib/windows-system-proxy.ts +16 -11
  199. package/src/lib/winsw.ts +2 -2
  200. package/src/oauth/callback-server.ts +4 -3
  201. package/src/oauth/generic-account-failover.ts +1 -0
  202. package/src/oauth/health.ts +12 -1
  203. package/src/oauth/index.ts +27 -108
  204. package/src/oauth/login-cli.ts +80 -29
  205. package/src/oauth/login-flow-state.ts +127 -0
  206. package/src/providers/api-key-resolve.ts +133 -0
  207. package/src/providers/api-key-selection.ts +5 -1
  208. package/src/providers/derive.ts +34 -17
  209. package/src/providers/devin-cli-authmode-migration.ts +14 -10
  210. package/src/providers/key-failover.ts +97 -19
  211. package/src/providers/key-store.ts +34 -110
  212. package/src/providers/model-rename-fields.ts +147 -0
  213. package/src/providers/model-rename-migration.ts +179 -38
  214. package/src/providers/model-rename-startup.ts +7 -5
  215. package/src/providers/openai-virtual-models.ts +42 -2
  216. package/src/providers/quota/antigravity.ts +22 -2
  217. package/src/providers/quota/vendor-probes-key.ts +38 -23
  218. package/src/providers/reasoning-metadata.ts +43 -18
  219. package/src/providers/registry/entries-core.ts +47 -20
  220. package/src/providers/registry/entries-extended.ts +63 -4
  221. package/src/providers/registry/model-ids.ts +168 -0
  222. package/src/providers/registry/model-seeds.ts +56 -10
  223. package/src/providers/registry/types.ts +2 -0
  224. package/src/providers/resolved-model-policy-merge.ts +167 -0
  225. package/src/providers/resolved-model-policy.ts +406 -0
  226. package/src/providers/stale-vision-classification-migration.ts +137 -0
  227. package/src/providers/xai-transport.ts +12 -1
  228. package/src/reasoning-effort.ts +8 -0
  229. package/src/responses/apply-patch-envelope.ts +0 -12
  230. package/src/responses/freeform-wrapper-scan.ts +279 -0
  231. package/src/responses/function-call-compat.ts +38 -1
  232. package/src/responses/inline-document.ts +65 -0
  233. package/src/responses/input-media.ts +42 -8
  234. package/src/responses/legacy-dotted-tool-name-repair.ts +134 -0
  235. package/src/responses/muse-tool-name-alias.ts +19 -0
  236. package/src/responses/parser-content.ts +8 -2
  237. package/src/responses/parser-tools.ts +3 -0
  238. package/src/responses/parser.ts +3 -1
  239. package/src/responses/progressive-freeform-input.ts +130 -0
  240. package/src/responses/reasoning-envelope.ts +30 -0
  241. package/src/responses/schema.ts +3 -0
  242. package/src/responses/state.ts +5 -12
  243. package/src/responses/tool-name-aliases.ts +15 -1
  244. package/src/router.ts +108 -117
  245. package/src/routing/compatibility/behavior.ts +9 -0
  246. package/src/routing/compatibility/subject.ts +16 -1
  247. package/src/server/adapter-resolve.ts +9 -0
  248. package/src/server/admission-model-scope.ts +219 -0
  249. package/src/server/audio-live.ts +9 -3
  250. package/src/server/audio-upstream.ts +18 -0
  251. package/src/server/auth-cors.ts +29 -0
  252. package/src/server/chat-completions.ts +60 -4
  253. package/src/server/chat-native.ts +19 -4
  254. package/src/server/claude-messages.ts +61 -20
  255. package/src/server/effort-row.ts +11 -3
  256. package/src/server/grok-responses-control-frame.ts +160 -1
  257. package/src/server/grok-responses-snapshot-repair.ts +113 -11
  258. package/src/server/gui-freshness.ts +103 -0
  259. package/src/server/gui-static.ts +7 -9
  260. package/src/server/images.ts +59 -6
  261. package/src/server/index/claude-intercept-lifecycle.ts +49 -0
  262. package/src/server/index/serve-options.ts +142 -39
  263. package/src/server/index/spend-ledger-lifecycle.ts +92 -0
  264. package/src/server/index/startup-warnings.ts +24 -0
  265. package/src/server/index/websocket-handler.ts +6 -1
  266. package/src/server/index.ts +34 -38
  267. package/src/server/lifecycle.ts +4 -4
  268. package/src/server/live-call-bindings.ts +6 -0
  269. package/src/server/live.ts +88 -3
  270. package/src/server/management/agent-settings-routes.ts +121 -36
  271. package/src/server/management/aside-profile-routes.ts +266 -7
  272. package/src/server/management/companion-routes.ts +77 -0
  273. package/src/server/management/config-routes.ts +18 -2
  274. package/src/server/management/context.ts +3 -0
  275. package/src/server/management/integration-routes.ts +287 -5
  276. package/src/server/management/logs-usage-routes.ts +19 -0
  277. package/src/server/management/metrics-routes.ts +20 -0
  278. package/src/server/management/model-rows.ts +224 -12
  279. package/src/server/management/native-integration-routes.ts +103 -6
  280. package/src/server/management/oauth-account-routes.ts +45 -7
  281. package/src/server/management/route-registry.ts +20 -0
  282. package/src/server/management/shared.ts +28 -4
  283. package/src/server/management/system-restart.ts +7 -2
  284. package/src/server/management/system-routes.ts +2 -0
  285. package/src/server/management/usage-aggregate-cache.ts +4 -0
  286. package/src/server/management/usage-timeline-routes.ts +44 -0
  287. package/src/server/management-api.ts +10 -9
  288. package/src/server/management-auth.ts +15 -1
  289. package/src/server/proxy-liveness.ts +75 -0
  290. package/src/server/readiness.ts +29 -10
  291. package/src/server/relay-eager.ts +24 -2
  292. package/src/server/relay.ts +138 -12
  293. package/src/server/request-log-failure-attribution.ts +99 -0
  294. package/src/server/request-log.ts +169 -2
  295. package/src/server/request-metrics.ts +298 -0
  296. package/src/server/responses/adapter-continuation.ts +3 -3
  297. package/src/server/responses/adapter-dispatch.ts +11 -6
  298. package/src/server/responses/codex-ws-wire.ts +34 -8
  299. package/src/server/responses/combo-stream-preflight.ts +168 -6
  300. package/src/server/responses/compact.ts +43 -10
  301. package/src/server/responses/compaction-routing.ts +111 -0
  302. package/src/server/responses/core-codex-account.ts +8 -3
  303. package/src/server/responses/core-combo.ts +7 -7
  304. package/src/server/responses/core-normalize.ts +6 -12
  305. package/src/server/responses/core-opaque-recovery.ts +91 -0
  306. package/src/server/responses/core-options.ts +4 -0
  307. package/src/server/responses/encrypted-payload.ts +20 -2
  308. package/src/server/responses/fetch-helpers.ts +124 -8
  309. package/src/server/responses/input-admission.ts +10 -0
  310. package/src/server/responses/passthrough-delivery.ts +45 -15
  311. package/src/server/responses/passthrough-dispatch.ts +215 -44
  312. package/src/server/responses/passthrough-error.ts +27 -8
  313. package/src/server/responses/policy-fallback.ts +5 -13
  314. package/src/server/responses/request-prepare.ts +109 -18
  315. package/src/server/responses/request-send-budget.ts +17 -1
  316. package/src/server/responses/request-sidecar-auth.ts +1 -1
  317. package/src/server/responses/request-transport.ts +26 -6
  318. package/src/server/responses/reset-replay.ts +108 -0
  319. package/src/server/responses/run-turn-execution.ts +25 -3
  320. package/src/server/responses/sidecar-execution.ts +17 -2
  321. package/src/server/responses/ws-upstream.ts +14 -27
  322. package/src/server/responses-custom-tool-repair.ts +27 -54
  323. package/src/server/responses-request-tool-scope.ts +214 -0
  324. package/src/server/responses-undeclared-tool-guard.ts +35 -2
  325. package/src/server/search.ts +25 -1
  326. package/src/server/sse-payload-rewrite.ts +1 -1
  327. package/src/server/usage-ledger-retention.ts +73 -0
  328. package/src/service/cli.ts +48 -2
  329. package/src/service/health.ts +3 -2
  330. package/src/service/install-state-contract.d.mts +27 -0
  331. package/src/service/install-state-contract.mjs +34 -0
  332. package/src/service/launchd.ts +1 -1
  333. package/src/service/orchestration.ts +2 -4
  334. package/src/service/ownership-compatibility.ts +164 -0
  335. package/src/service/ownership-mutation-lease.d.mts +32 -0
  336. package/src/service/ownership-mutation-lease.mjs +211 -0
  337. package/src/service/repair.ts +45 -1
  338. package/src/service/state-lock.ts +269 -0
  339. package/src/service/state-record.d.mts +36 -0
  340. package/src/service/state-record.mjs +138 -0
  341. package/src/service/state.ts +582 -68
  342. package/src/service/windows-taskxml.ts +11 -10
  343. package/src/service.ts +7 -3
  344. package/src/tray/windows-tray.ps1 +156 -4
  345. package/src/types/config.ts +48 -3
  346. package/src/types/provider.ts +91 -0
  347. package/src/types/request.ts +30 -2
  348. package/src/types/tools.ts +33 -0
  349. package/src/types.ts +4 -0
  350. package/src/update/index.ts +207 -63
  351. package/src/update/job.ts +9 -5
  352. package/src/update/ownership-transaction.ts +47 -0
  353. package/src/update/restart-ownership.ts +54 -0
  354. package/src/update/runtime-ownership.d.mts +40 -0
  355. package/src/update/runtime-ownership.mjs +122 -0
  356. package/src/usage/attempt-delivery.ts +198 -0
  357. package/src/usage/cache-diagnostic.ts +305 -0
  358. package/src/usage/failure-fingerprint.ts +118 -0
  359. package/src/usage/failure-projection-cache.ts +174 -0
  360. package/src/usage/failure-projection.ts +174 -0
  361. package/src/usage/ledger-retention.ts +165 -0
  362. package/src/usage/log.ts +126 -79
  363. package/src/usage/request-outcome.ts +150 -0
  364. package/src/usage/retention-contract.ts +28 -0
  365. package/src/usage/summary.ts +2 -2
  366. package/src/usage/telemetry-contract.ts +237 -0
  367. package/src/usage/timeline.ts +236 -0
  368. package/src/vision/eligibility.ts +88 -9
  369. package/src/vision/plan.ts +34 -10
  370. package/src/web-search/alpha-search.ts +21 -1
  371. package/src/web-search/executor.ts +41 -2
  372. package/src/web-search/loop.ts +6 -1
  373. package/gui/dist/assets/index-C5IebErG.js +0 -136
  374. package/gui/dist/assets/index-OESInAjC.css +0 -1
@@ -6,11 +6,107 @@ export const PREFLIGHT_HEARTBEAT_RETAIN_LIMIT = 16;
6
6
 
7
7
  /**
8
8
  * Coalescing threshold for adjacent text/thinking deltas buffered with no
9
- * waiting reader (UTF-16 code units). This is a merge-size ceiling, not a
10
- * byte-memory cap: a single oversized incoming event stays one item.
9
+ * waiting reader (UTF-16 code units). This is a merge-size ceiling; the two
10
+ * retention budgets below are what bound memory.
11
11
  */
12
12
  export const COALESCE_MAX_CHUNK_LENGTH = 64 * 1024;
13
13
 
14
+ /**
15
+ * Retained-string budget for the WHOLE queue, in UTF-16 code units.
16
+ *
17
+ * This bounds what the queue is holding at one moment, not how much a turn
18
+ * streams: every dequeue gives its charge back, so a long healthy stream with a
19
+ * consumer attached never accumulates and is never capped by total length.
20
+ *
21
+ * 32 MiB is deliberately far above any single legitimate burst. A synchronous
22
+ * producer can legally fill the queue before its consumer is scheduled — the
23
+ * image loop does exactly that with over a million one-character deltas, which
24
+ * coalesce into roughly 1.2 MB of retained text — so a budget near that size
25
+ * aborts healthy turns rather than stalled ones. The previous effective bound
26
+ * was the 1024-event cap times the 64 KiB merge ceiling, so 64 MiB; this halves
27
+ * it while leaving that legitimate burst an order of magnitude of headroom.
28
+ */
29
+ export const DEFAULT_MAX_BACKLOG_CODE_UNITS = 32 * 1024 * 1024;
30
+
31
+ /**
32
+ * Retained-string budget for ONE queued event, in UTF-16 code units.
33
+ *
34
+ * Separate from the aggregate on purpose, because the two describe different
35
+ * failures. Passing the aggregate means the consumer is not keeping up. Passing
36
+ * this one means a single event is malformed or unbounded, which stays true
37
+ * however empty the queue is, so it must be refused even with the whole
38
+ * aggregate free. They also report different terminal messages, so an operator
39
+ * reading the turn's error learns which happened.
40
+ *
41
+ * Like the aggregate, this governs what the queue RETAINS. An event handed
42
+ * straight to a waiting consumer is never held here, so neither budget applies
43
+ * to it: refusing it would abort a turn over memory this queue does not own,
44
+ * and the consumer's own per-event bound governs that payload instead.
45
+ */
46
+ export const DEFAULT_MAX_EVENT_CODE_UNITS = 8 * 1024 * 1024;
47
+
48
+ const BACKLOG_EXCEEDED_MESSAGE = "consumer stalled: adapter event backlog exceeded — turn aborted";
49
+ const EVENT_TOO_LARGE_MESSAGE = "adapter event exceeds the single-event retained-string budget — turn aborted";
50
+
51
+ /**
52
+ * Bound on how far the retention measure walks into one event. AdapterEvent is
53
+ * a plain-data union, but two of its members carry open provider-shaped bags
54
+ * (`providerState`, `usage.rawUsage`) whose depth no type here controls. The
55
+ * ceilings keep a single push O(1)-ish rather than O(whatever an adapter
56
+ * attached), and under-counting a pathological object is the safe direction:
57
+ * the event-count cap still bounds how many of them can be retained.
58
+ */
59
+ const RETENTION_MAX_DEPTH = 8;
60
+ const RETENTION_MAX_NODES = 4096;
61
+
62
+ /**
63
+ * Retained UTF-16 code units carried by one event's string payload.
64
+ *
65
+ * Measured by walking own enumerable properties rather than by naming each
66
+ * variant's string fields: a hand-written per-variant table is exhaustive over
67
+ * a union, so adding an event type on one branch while a consumer lands on
68
+ * another produces a measure that silently stops counting the new payload.
69
+ * The walk is the derived answer and needs no update when the union grows.
70
+ *
71
+ * `type` is skipped because it is the discriminant, identical for every event
72
+ * of a kind and not payload anyone is buffering.
73
+ */
74
+ export function retainedEventCodeUnits(event: AdapterEvent): number {
75
+ // Defensive: this measures values an adapter produced. A malformed emission
76
+ // has to become a terminal event, not a TypeError thrown out of push() with
77
+ // the queue half-updated.
78
+ if (!event || typeof event !== "object") return 0;
79
+ let total = 0;
80
+ let nodes = 0;
81
+ const seen = new Set<object>();
82
+ const visit = (value: unknown, depth: number): void => {
83
+ if (typeof value === "string") {
84
+ total += value.length;
85
+ return;
86
+ }
87
+ if (!value || typeof value !== "object" || depth >= RETENTION_MAX_DEPTH || seen.has(value)) return;
88
+ seen.add(value);
89
+ for (const nested of Object.values(value)) {
90
+ if (nodes++ >= RETENTION_MAX_NODES) return;
91
+ visit(nested, depth + 1);
92
+ }
93
+ };
94
+ for (const [key, value] of Object.entries(event)) {
95
+ if (key === "type") continue;
96
+ if (nodes++ >= RETENTION_MAX_NODES) break;
97
+ visit(value, 1);
98
+ }
99
+ return total;
100
+ }
101
+
102
+ function positiveBudget(value: number | undefined, fallback: number, name: string): number {
103
+ const resolved = value ?? fallback;
104
+ if (!Number.isSafeInteger(resolved) || resolved <= 0) {
105
+ throw new RangeError(`${name} must be a positive safe integer`);
106
+ }
107
+ return resolved;
108
+ }
109
+
14
110
  export interface AdapterEventQueue {
15
111
  /**
16
112
  * Returns true when the event was merged into the buffered tail instead of
@@ -22,6 +118,13 @@ export interface AdapterEventQueue {
22
118
  close(): void;
23
119
  stream(): AsyncIterable<AdapterEvent>;
24
120
  collect(): Promise<AdapterEvent[]>;
121
+ /**
122
+ * Retained string payload the queue is currently holding, in UTF-16 code
123
+ * units. Exposed so a caller — and a regression — can assert the counter
124
+ * returns to zero on every terminal path instead of inferring it from an
125
+ * abort that happened to fire.
126
+ */
127
+ retainedCodeUnits(): number;
25
128
  }
26
129
 
27
130
  export interface AdapterEventPreflight {
@@ -73,47 +176,74 @@ export async function preflightAdapterEvents(
73
176
 
74
177
  export function createAdapterEventQueue(opts?: {
75
178
  maxBacklog?: number;
179
+ maxBacklogCodeUnits?: number;
180
+ maxEventCodeUnits?: number;
76
181
  onBacklogExceeded?: () => void;
77
182
  }): AdapterEventQueue {
78
183
  const queued: AdapterEvent[] = [];
184
+ /**
185
+ * What each queued item was charged, in lockstep with `queued`. Releasing the
186
+ * recorded charge rather than re-measuring is what makes the accounting exact
187
+ * on every path: a merge, a terminal record admitted past the budget and a
188
+ * plain event all give back precisely what they took, so the counter cannot
189
+ * drift positive (a leak) or negative (a budget the next turn gets for free).
190
+ */
191
+ const charged: number[] = [];
79
192
  const readers: QueueReader[] = [];
80
193
  const maxBacklog = opts?.maxBacklog ?? 1_024;
194
+ const maxBacklogCodeUnits = positiveBudget(opts?.maxBacklogCodeUnits, DEFAULT_MAX_BACKLOG_CODE_UNITS, "maxBacklogCodeUnits");
195
+ const maxEventCodeUnits = positiveBudget(opts?.maxEventCodeUnits, DEFAULT_MAX_EVENT_CODE_UNITS, "maxEventCodeUnits");
196
+ let retained = 0;
81
197
  let closed = false;
82
198
 
83
199
  // Merge an incoming delta into the buffered tail when no reader is waiting.
84
- // The backlog cap counts events, not tokens, so a detached or briefly
85
- // stalled consumer (e.g. a Codex app mid-reconnect whose disconnect Bun has
86
- // not yet delivered) used to hit the cap within seconds of token-granular
87
- // streaming and abort a healthy turn. Adjacent same-phase text deltas,
88
- // adjacent thinking deltas, and consecutive heartbeats carry no ordering
89
- // information between themselves, so merging them preserves every consumer
90
- // contract while making the cap approximate buffered items again.
200
+ // The event cap counts events, not tokens, so a detached or briefly stalled
201
+ // consumer (e.g. a Codex app mid-reconnect whose disconnect Bun has not yet
202
+ // delivered) used to hit it within seconds of token-granular streaming and
203
+ // abort a healthy turn. Adjacent same-phase text deltas, adjacent thinking
204
+ // deltas, and consecutive heartbeats carry no ordering information between
205
+ // themselves, so merging them preserves every consumer contract while making
206
+ // the cap approximate buffered items again.
91
207
  // Pushed objects may be retained by adapters, so the tail is REPLACED with
92
- // a fresh object — never mutated (alias safety).
93
- const coalesceIntoTail = (event: AdapterEvent): boolean => {
94
- const tail = queued[queued.length - 1];
95
- if (!tail) return false;
208
+ // a fresh object — never mutated (alias safety). Returning the replacement
209
+ // instead of installing it lets push price the merge before committing to it.
210
+ const planTailMerge = (tail: AdapterEvent, event: AdapterEvent): AdapterEvent | null => {
96
211
  if (event.type === "heartbeat") {
97
- if (tail.type !== "heartbeat") return false;
212
+ if (tail.type !== "heartbeat") return null;
98
213
  // Heartbeats carry no ordering between themselves, but the replay-unsafe
99
214
  // marker is not ordering — it is a latch. Dropping the incoming event
100
215
  // would discard the only record that Cursor already performed a local
101
216
  // side effect, and preflight would then permit an OAuth replay of it.
102
217
  if (event.replayUnsafe === true && tail.replayUnsafe !== true) {
103
- queued[queued.length - 1] = { type: "heartbeat", replayUnsafe: true };
218
+ return { type: "heartbeat", replayUnsafe: true };
104
219
  }
105
- return true;
220
+ return tail;
106
221
  }
107
222
  if (event.type === "text_delta" && tail.type === "text_delta" && tail.phase === event.phase) {
108
- if (tail.text.length + event.text.length > COALESCE_MAX_CHUNK_LENGTH) return false;
109
- queued[queued.length - 1] = { type: "text_delta", text: tail.text + event.text, phase: tail.phase };
110
- return true;
223
+ if (tail.text.length + event.text.length > COALESCE_MAX_CHUNK_LENGTH) return null;
224
+ return { type: "text_delta", text: tail.text + event.text, phase: tail.phase };
111
225
  }
112
226
  if (event.type === "thinking_delta" && tail.type === "thinking_delta") {
113
- if (tail.thinking.length + event.thinking.length > COALESCE_MAX_CHUNK_LENGTH) return false;
114
- queued[queued.length - 1] = { type: "thinking_delta", thinking: tail.thinking + event.thinking };
115
- return true;
227
+ if (tail.thinking.length + event.thinking.length > COALESCE_MAX_CHUNK_LENGTH) return null;
228
+ return { type: "thinking_delta", thinking: tail.thinking + event.thinking };
116
229
  }
230
+ return null;
231
+ };
232
+
233
+ /**
234
+ * Record why the turn is ending and close. The terminal error is admitted
235
+ * past both budgets — refusing to retain the explanation of a refusal would
236
+ * leave the consumer with a silent truncation — but it is charged like any
237
+ * other item so the counter stays exact through the final drain.
238
+ */
239
+ const abortWith = (message: string): false => {
240
+ opts?.onBacklogExceeded?.();
241
+ const terminal: AdapterEvent = { type: "error", message };
242
+ const cost = retainedEventCodeUnits(terminal);
243
+ queued.push(terminal);
244
+ charged.push(cost);
245
+ retained += cost;
246
+ close();
117
247
  return false;
118
248
  };
119
249
 
@@ -121,17 +251,35 @@ export function createAdapterEventQueue(opts?: {
121
251
  if (closed) return false;
122
252
  const reader = readers.shift();
123
253
  if (reader) {
254
+ // Handed straight to a waiting consumer, so the queue retains nothing and
255
+ // charges nothing. Neither budget applies to an event it never holds.
124
256
  reader({ done: false, value: event });
125
257
  return false;
126
258
  }
127
- if (coalesceIntoTail(event)) return true;
128
- if (queued.length >= maxBacklog) {
129
- opts?.onBacklogExceeded?.();
130
- queued.push({ type: "error", message: "consumer stalled: adapter event backlog exceeded — turn aborted" });
131
- close();
132
- return false;
259
+ const tail = queued[queued.length - 1];
260
+ const merged = tail ? planTailMerge(tail, event) : null;
261
+ if (merged && tail) {
262
+ const replacement = retainedEventCodeUnits(merged);
263
+ if (replacement > maxEventCodeUnits) return abortWith(EVENT_TOO_LARGE_MESSAGE);
264
+ // Charge only what the backlog actually gains. A merge keeps the tail's
265
+ // own fields, so the incoming event's duplicated phase is never retained
266
+ // twice and an unchanged tail costs nothing at all.
267
+ const delta = replacement - charged[charged.length - 1]!;
268
+ if (delta > maxBacklogCodeUnits - retained) return abortWith(BACKLOG_EXCEEDED_MESSAGE);
269
+ queued[queued.length - 1] = merged;
270
+ charged[charged.length - 1] = replacement;
271
+ retained += delta;
272
+ return true;
133
273
  }
274
+ const cost = retainedEventCodeUnits(event);
275
+ if (cost > maxEventCodeUnits) return abortWith(EVENT_TOO_LARGE_MESSAGE);
276
+ // Both refusals are priced before anything is retained, so an event that is
277
+ // turned away is never charged for.
278
+ if (queued.length >= maxBacklog) return abortWith(BACKLOG_EXCEEDED_MESSAGE);
279
+ if (cost > maxBacklogCodeUnits - retained) return abortWith(BACKLOG_EXCEEDED_MESSAGE);
134
280
  queued.push(event);
281
+ charged.push(cost);
282
+ retained += cost;
135
283
  return false;
136
284
  };
137
285
 
@@ -147,6 +295,7 @@ export function createAdapterEventQueue(opts?: {
147
295
  while (true) {
148
296
  const next = queued.shift();
149
297
  if (next) {
298
+ retained -= charged.shift() ?? 0;
150
299
  yield next;
151
300
  continue;
152
301
  }
@@ -165,5 +314,5 @@ export function createAdapterEventQueue(opts?: {
165
314
  return events;
166
315
  };
167
316
 
168
- return { push, close, stream, collect };
317
+ return { push, close, stream, collect, retainedCodeUnits: () => retained };
169
318
  }
@@ -1,4 +1,5 @@
1
1
  import type { OcxProviderConfig } from "../types";
2
+ import { debugProviderDiagnostic } from "../lib/debug";
2
3
  import { isXaiResponsesDestination } from "../providers/xai-transport";
3
4
 
4
5
  const CODEX_WEB_SEARCH_TOOL = "web_search";
@@ -192,7 +193,21 @@ export function normalizeXaiResponsesWebSearch(
192
193
  if (inputChanged) next = { ...next, input };
193
194
  }
194
195
 
195
- return normalizeToolChoice(next);
196
+ const normalized = normalizeToolChoice(next);
197
+ const choice = normalized.tool_choice;
198
+ if ((choice === "auto" || choice === "none") && !hasAnyDeclaredTool(normalized)) {
199
+ debugProviderDiagnostic("xai", "tool-choice-omitted", { choice });
200
+ const { tool_choice: _toolChoice, ...rest } = normalized;
201
+ // `auto` selects from the catalog, so a catalog with nothing in it makes it meaningless and
202
+ // the omission says nothing the request did not already say. `none` is the opposite: it is a
203
+ // prohibition, and on a request whose catalog this normalizer just emptied it is the only
204
+ // place the turn's client-call boundary is written down. Downstream repair reads that
205
+ // boundary off the final outbound body, so omitting the word alone would hand back a call the
206
+ // caller ruled out. Restate it as the explicit empty catalog, which carries the same deny-all
207
+ // and which this destination already receives whenever a caller sends one itself.
208
+ return choice === "none" && !Array.isArray(rest.tools) ? { ...rest, tools: [] } : rest;
209
+ }
210
+ return normalized;
196
211
  }
197
212
 
198
213
  function isLiveWebSearchTool(tool: unknown): boolean {
@@ -1,9 +1,10 @@
1
1
  import {
2
+ applyReplayRefusalClientHeaders,
2
3
  isNonReplayableUpstreamCode,
3
4
  isReplayRefusalCode,
4
- markReplayRefusalResponse,
5
5
  markResponseNonReplayable,
6
6
  REPLAY_REFUSED_STATUS,
7
+ retainReplayRefusal,
7
8
  } from "../lib/upstream-retry";
8
9
  import {
9
10
  adapterFailureFromMessage,
@@ -45,6 +46,11 @@ export function formatErrorResponse(
45
46
  && retryAfter.length <= 128) {
46
47
  headers.set("Retry-After", retryAfter);
47
48
  }
49
+ // The refusal's client policy, restated here for the same reason its status is: this
50
+ // formatter is the last thing several adapter and combo paths touch before the client,
51
+ // and no wait of its own does not stop a client that retries every 429 by default.
52
+ const refusal = isReplayRefusalCode(error.code) && replayBlocked;
53
+ if (refusal) applyReplayRefusalClientHeaders(headers);
48
54
  const response = new Response(JSON.stringify({ error }), {
49
55
  status: finalStatus,
50
56
  headers,
@@ -53,6 +59,6 @@ export function formatErrorResponse(
53
59
  // Re-wrapping is where the refusal loses its provenance: combo failure consumption parses
54
60
  // the JSON and builds a new Response, and the code alone does not tell a later quota
55
61
  // recorder that no upstream produced this status. Carry the narrower marker across too.
56
- if (replayBlocked && isReplayRefusalCode(error.code)) markReplayRefusalResponse(response);
62
+ if (refusal) retainReplayRefusal(response);
57
63
  return response;
58
64
  }
@@ -7,6 +7,7 @@ import type {
7
7
  OcxUsage,
8
8
  } from "../types";
9
9
  import { coerceIntegerToolArguments } from "../lib/tool-argument-integers";
10
+ import { attemptDeliveryRecorder } from "../usage/attempt-delivery";
10
11
  import {
11
12
  adapterFailureFromMessage,
12
13
  classifyError,
@@ -51,7 +52,14 @@ export function buildResponseJSON(
51
52
  ): Record<string, unknown> {
52
53
  // Default-budget safety net: a caller that omits the budget gets a bounded
53
54
  // default (disposed with the call), never the unbounded append path.
54
- if (options?.translatorBudget) return buildResponseJSONWithBudget(events, modelId, options);
55
+ if (options?.translatorBudget) {
56
+ const body = buildResponseJSONWithBudget(events, modelId, options);
57
+ // A buffered turn delivers its whole answer as one body, so nothing calls the per-frame
58
+ // recorder on the SSE bridge. Without this the attempt would persist adapter events with
59
+ // zero relayed ones, which is the loss signal -- raised on every non-streaming request.
60
+ attemptDeliveryRecorder(options.translatorBudget)?.noteBufferedDelivery(body);
61
+ return body;
62
+ }
55
63
  const budget = createTranslatorBudget();
56
64
  try {
57
65
  return buildResponseJSONWithBudget(events, modelId, { ...options, translatorBudget: budget });
package/src/bridge/sse.ts CHANGED
@@ -16,12 +16,12 @@ import {
16
16
  type OcxErrorPayload,
17
17
  } from "../lib/errors";
18
18
  import { redactSecretString } from "../lib/redact";
19
+ import { attemptDeliveryRecorder, classifyRelayedResponseEvent } from "../usage/attempt-delivery";
19
20
  import {
20
- freeformFallbackKeys,
21
21
  mayBecomePatchEnvelope,
22
22
  repairFreeformToolInput,
23
- unwrapFreeformToolInput,
24
23
  } from "../responses/apply-patch-envelope";
24
+ import { progressiveFreeformInput } from "../responses/progressive-freeform-input";
25
25
  import { encodeCompactionSummary } from "../responses/compaction";
26
26
  import { compileCodeModeHelperInput, resolveCodeModeHelperName } from "../responses/code-mode-helper-compat";
27
27
  import { isTruncatedStopReason, truncationReasonFor } from "../responses/truncated-stop-reason";
@@ -153,153 +153,6 @@ export function bridgeToResponsesSSE(
153
153
  ? compileCodeModeHelperInput(args, helper, codeModeHelperName ?? toolName)
154
154
  : repairFreeformToolInput(args, toolName, namespace);
155
155
  };
156
- // Best-effort unwrap of a PARTIAL freeform arg buffer for live input streaming
157
- // (`response.custom_tool_call_input.delta` — codex-rs uses it for UI preview only;
158
- // the completed custom_tool_call item stays authoritative). Compact `{"input":"...`
159
- // buffers get their string value progressively unescaped; anything else streams raw.
160
- const JSON_WHITESPACE = new Set([" ", "\t", "\n", "\r"]);
161
- type WrapperOpening =
162
- | { state: "none" }
163
- | { state: "prefix" }
164
- | { state: "open"; valueStart: number };
165
- /**
166
- * Where the string value of `{"<key>":"` begins, tolerating the insignificant whitespace
167
- * `JSON.parse` accepts.
168
- *
169
- * The earlier form of this compared the buffer against the compact literal `{"key":"`, so a
170
- * wrapper written with spaces or newlines matched no prefix at all, streamed as raw JSON
171
- * deltas and then completed as the unwrapped body. That is the same delta/completion
172
- * disagreement #5047 closed for compact wrappers, reached through a different spelling:
173
- * `unwrapFreeformToolInput` reads the completed text with `JSON.parse`, which does not care
174
- * how the object is laid out, so neither can the streaming side.
175
- */
176
- const wrapperOpening = (args: string, key: string): WrapperOpening => {
177
- let index = 0;
178
- for (const token of ["{", `"${key}"`, ":", '"']) {
179
- while (index < args.length && JSON_WHITESPACE.has(args[index]!)) index++;
180
- if (index >= args.length) return { state: "prefix" };
181
- for (const expected of token) {
182
- if (index >= args.length) return { state: "prefix" };
183
- if (args[index] !== expected) return { state: "none" };
184
- index++;
185
- }
186
- }
187
- return { state: "open", valueStart: index };
188
- };
189
- /**
190
- * Whether a body could still grow into one complete outer Markdown fence.
191
- *
192
- * `stripMarkdownCodeFence` removes such a fence at completion for exactly the two tools that
193
- * own the grammar, so a fenced body's streamed bytes and its completed input disagree unless
194
- * the stream holds. A buffer that does not open with a fence can never acquire one, so
195
- * ordinary bodies are unaffected; a buffer that does keeps its preview suppressed for the
196
- * whole call, because a closing fence can still be followed by more text that withdraws it.
197
- */
198
- const mayBecomeFencedBody = (text: string, toolName: string): boolean => {
199
- if (toolName !== "exec" && toolName !== "apply_patch") return false;
200
- const head = text.trimStart();
201
- if (head === "") return true;
202
- return head.startsWith("```") || "```".startsWith(head);
203
- };
204
- /** The two-character escapes JSON defines, and nothing else. */
205
- const JSON_ESCAPES = new Map<string, string>([
206
- ['"', '"'], ["\\", "\\"], ["/", "/"],
207
- ["b", "\b"], ["f", "\f"], ["n", "\n"], ["r", "\r"], ["t", "\t"],
208
- ]);
209
- const LOW_SURROGATE_ESCAPE = /^\\u[dD][c-fC-F][0-9a-fA-F]{2}$/;
210
- /**
211
- * The decoded prefix of a JSON string body, stopping at the first byte it cannot resolve.
212
- *
213
- * Every stop is a hold rather than a guess, because `JSON.parse` decides the completed value
214
- * and anything invented here would be retracted. Three of them are not obvious:
215
- *
216
- * An escape JSON does not define makes the whole wrapper unparseable no matter what arrives
217
- * next, so completion falls back to the raw text. Returning `null` for that case stops the
218
- * preview rather than continuing to decode a value the completed item will never carry.
219
- * `\\b` and `\\f` are defined, and were previously decoded to the letters b and f.
220
- *
221
- * A lone high surrogate is not a character. Emitting one alone puts an unpaired code unit in a
222
- * delta the client has to decode by itself, so the pair is emitted together or not at all.
223
- */
224
- const decodeJsonStringPrefix = (body: string): string | null => {
225
- let out = "";
226
- for (let i = 0; i < body.length; i++) {
227
- const c = body[i];
228
- if (c === '"') break; // unescaped closing quote: value complete
229
- if (c !== "\\") { out += c; continue; }
230
- const n = body[i + 1];
231
- if (n === undefined) break; // escape split across chunks: wait for more
232
- if (n === "u") {
233
- const hex = body.slice(i + 2, i + 6);
234
- if (hex.length < 4) break; // split across chunks: wait for more
235
- if (!/^[0-9a-fA-F]{4}$/.test(hex)) return null; // never parses
236
- const code = parseInt(hex, 16);
237
- if (code >= 0xd800 && code <= 0xdbff) {
238
- const low = body.slice(i + 6, i + 12);
239
- if (!LOW_SURROGATE_ESCAPE.test(low)) break;
240
- out += String.fromCharCode(code, parseInt(low.slice(2), 16));
241
- i += 11;
242
- continue;
243
- }
244
- out += String.fromCharCode(code);
245
- i += 5;
246
- continue;
247
- }
248
- const escaped = JSON_ESCAPES.get(n);
249
- if (escaped === undefined) return null; // never parses
250
- out += escaped;
251
- i += 1;
252
- }
253
- return out;
254
- };
255
- /**
256
- * The value to stream so far, or `null` to HOLD because nothing can be decided yet.
257
- *
258
- * `input` is decidable from its prefix: `unwrapFreeformToolInput` returns it whenever the
259
- * key is present, whatever else the object carries, so its value can be unescaped
260
- * progressively and never retracted.
261
- *
262
- * One case escapes that claim and is accepted rather than fixed: a duplicate `input` key.
263
- * `JSON.parse` keeps the last one, so `{"input":"a","input":"b"}` streams a and completes with
264
- * b. Closing it means holding every canonical wrapper until its object parses, which is the
265
- * progressive streaming this path exists to provide. The completed item stays authoritative,
266
- * and no model emits a duplicate key in practice.
267
- *
268
- * A wrapper that turns invalid AFTER streaming has committed has the same shape and the same
269
- * answer. `{"input":"a` followed by `\\qb"}` has already published a when the undefined escape
270
- * arrives, and completion returns the raw text because nothing parses. The preview stops
271
- * there: no rewind, and no decoded text the completed item does not contain. Bounding the
272
- * damage is what is available without giving up progressive streaming, and the args are
273
- * unusable in that case whichever representation wins.
274
- *
275
- * A fallback key is not. It only unwraps when it is the SINGLE string field, and a second
276
- * key can still arrive — so a value emitted early would have to be taken back. That is the
277
- * rewind this holds instead: stream nothing until the object closes, then publish the one
278
- * repaired body. The routed passthrough in `responses-custom-tool-repair.ts` already holds
279
- * any object prefix for the same reason (#5047).
280
- */
281
- const freeformPartialInput = (args: string, toolName: string): string | null => {
282
- const openings = ["input", ...freeformFallbackKeys(toolName)]
283
- .map(key => ({ key, opening: wrapperOpening(args, key) }));
284
- const canonical = openings[0]!.opening;
285
- if (canonical.state === "open") {
286
- const decoded = decodeJsonStringPrefix(args.slice(canonical.valueStart));
287
- if (decoded === null) return null;
288
- return mayBecomeFencedBody(decoded, toolName) ? null : decoded;
289
- }
290
- if (openings.some(entry => entry.opening.state === "open")) {
291
- // Committed to a fallback wrapper. Undecidable until the object is complete.
292
- try {
293
- JSON.parse(args);
294
- } catch {
295
- return null;
296
- }
297
- return unwrapFreeformToolInput(args, toolName);
298
- }
299
- // Still an ambiguous prefix of some wrapper: which wrapper, if any, is not known yet.
300
- if (openings.some(entry => entry.opening.state === "prefix")) return null;
301
- return mayBecomeFencedBody(args, toolName) ? null : args;
302
- };
303
156
  // tool_search_call carries arguments as a JSON object ({query, limit}); parse the model's arg string.
304
157
  const parseArgsObj = (args: string): Record<string, unknown> => {
305
158
  try { const o = JSON.parse(args); return o && typeof o === "object" ? o : {}; } catch { return {}; }
@@ -310,6 +163,10 @@ export function bridgeToResponsesSSE(
310
163
  // at terminal/cancel below.
311
164
  const ownsBudget = !options?.translatorBudget;
312
165
  const budget = options?.translatorBudget ?? createTranslatorBudget();
166
+ // Resolved from the CALLER's budget only. A bridge that owns its budget is not serving a
167
+ // logged request -- there is no attempt to count against, and a locally created scope would
168
+ // never have had a recorder bound to it.
169
+ const delivery = attemptDeliveryRecorder(options?.translatorBudget);
313
170
  // Idempotent: safe to call at every stream-death path; disposal must come
314
171
  // AFTER the final charges (emitDone), never inside reportTerminal.
315
172
  const disposeOwnedBudget = () => { if (ownsBudget) budget.dispose(); };
@@ -426,6 +283,11 @@ export function bridgeToResponsesSSE(
426
283
  controller.enqueue(frame);
427
284
  budget?.releaseRetained(frameBytes, { kind: "live_transient" });
428
285
  emittedFrames++;
286
+ // After a SUCCESSFUL enqueue, never before it. A frame that threw on the way to the
287
+ // transport did not reach the caller, and counting it here would make the relayed
288
+ // total equal the adapter total by construction -- erasing the one discrepancy these
289
+ // counters exist to expose (#3983).
290
+ delivery?.noteRelayedEvent(classifyRelayedResponseEvent(name, data));
429
291
  } catch (error) {
430
292
  if (isTranslatorBudgetExceededError(error)) {
431
293
  terminateForTranslatorOverflow?.(error);
@@ -1199,7 +1061,7 @@ export function bridgeToResponsesSSE(
1199
1061
  });
1200
1062
  }
1201
1063
  if (currentToolCall.freeform && !currentToolCall.codeModeHelperName) {
1202
- // `freeformPartialInput` holds while the buffer is still an ambiguous prefix
1064
+ // `progressiveFreeformInput` holds while the buffer is still an ambiguous prefix
1203
1065
  // of a JSON wrapper; otherwise stream only the unwrapped input suffix, never
1204
1066
  // rewinding on a mode flip.
1205
1067
  //
@@ -1209,7 +1071,7 @@ export function bridgeToResponsesSSE(
1209
1071
  // is the same disagreement in the other direction.
1210
1072
  const ownsFreeformGrammar = currentToolCall.namespace === undefined
1211
1073
  || currentToolCall.namespace === "functions";
1212
- const full = freeformPartialInput(
1074
+ const full = progressiveFreeformInput(
1213
1075
  currentToolCall.args,
1214
1076
  ownsFreeformGrammar ? currentToolCall.name : "",
1215
1077
  );