@bitkyc08/opencodex 2.80.0 → 2.82.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 (376) hide show
  1. package/README.md +3 -3
  2. package/bin/ocx.mjs +166 -73
  3. package/gui/dist/assets/App-Ds_qL7o8.js +54 -0
  4. package/gui/dist/assets/Tray-BqHF5Trd.js +1 -0
  5. package/gui/dist/assets/{index-Brk6F3fn.css → index-BAbcj51J.css} +1 -1
  6. package/gui/dist/assets/index-D3FBCNiP.js +94 -0
  7. package/gui/dist/assets/quota-summary-DTBC8n7-.js +1 -0
  8. package/gui/dist/index.html +2 -2
  9. package/gui/dist/provider-icons/claude-green.svg +1 -0
  10. package/package.json +3 -2
  11. package/src/adapters/anthropic-model-contract.ts +8 -2
  12. package/src/adapters/anthropic.ts +8 -3
  13. package/src/adapters/codebuddy/mcp-server.ts +2 -25
  14. package/src/adapters/codebuddy/tool-bridge.ts +1 -7
  15. package/src/adapters/coding-agent/mcp-server.ts +33 -0
  16. package/src/adapters/coding-agent/protocol.ts +122 -17
  17. package/src/adapters/coding-agent/tool-catalog.ts +75 -0
  18. package/src/adapters/coding-agent/turn.ts +101 -44
  19. package/src/adapters/cursor/catalog.ts +8 -0
  20. package/src/adapters/cursor/effort-map.ts +2 -0
  21. package/src/adapters/devin/live-models.ts +2 -0
  22. package/src/adapters/kiro-retry.ts +9 -4
  23. package/src/adapters/openai-chat/messages.ts +7 -6
  24. package/src/adapters/openai-responses/compaction-search-history.ts +109 -0
  25. package/src/adapters/openai-responses/passthrough.ts +9 -4
  26. package/src/adapters/openai-responses/tool-output-recovery.ts +20 -0
  27. package/src/adapters/physical-send.ts +8 -5
  28. package/src/adapters/qoder/adapter.ts +41 -6
  29. package/src/adapters/qoder/scaffold-guard.ts +2 -3
  30. package/src/adapters/zed.ts +16 -1
  31. package/src/claude/agents-inject.ts +14 -3
  32. package/src/claude/context-windows.ts +19 -17
  33. package/src/claude/desktop-3p.ts +88 -12
  34. package/src/claude/desktop-profile.ts +2 -1
  35. package/src/claude/gateway-cache.ts +18 -1
  36. package/src/claude/inbound-cache-stabilize.ts +4 -9
  37. package/src/claude/inbound.ts +78 -3
  38. package/src/claude/intercept/cli-catalog.ts +3 -0
  39. package/src/claude/intercept/model-bindings.ts +1 -0
  40. package/src/claude/intercept/picker-ca-persistence.ts +7 -6
  41. package/src/claude/intercept/picker-listener.ts +71 -4
  42. package/src/claude/intercept/picker-models.ts +14 -6
  43. package/src/claude/intercept/settings.ts +3 -0
  44. package/src/claude/long-context.ts +66 -0
  45. package/src/claude/model-info.ts +19 -16
  46. package/src/claude/outbound.ts +94 -9
  47. package/src/claude/subagent-model.ts +40 -11
  48. package/src/cli/account-anthropic-threshold.ts +14 -5
  49. package/src/cli/account-api.ts +3 -1
  50. package/src/cli/account-extended.ts +13 -9
  51. package/src/cli/account-policy-dto.ts +1 -1
  52. package/src/cli/account-policy.ts +13 -4
  53. package/src/cli/account.ts +3 -3
  54. package/src/cli/agent.ts +9 -5
  55. package/src/cli/aside-profile-recovery.ts +33 -0
  56. package/src/cli/capabilities-accounts.ts +2 -2
  57. package/src/cli/capabilities-agents-routing.ts +24 -0
  58. package/src/cli/capabilities-base.ts +5 -5
  59. package/src/cli/capabilities-integrations.ts +18 -2
  60. package/src/cli/cli-path-diagnostics.ts +132 -0
  61. package/src/cli/codex-shim-autorestore.ts +3 -0
  62. package/src/cli/desktop-runtime-guidance.ts +65 -0
  63. package/src/cli/dispatch.ts +15 -1
  64. package/src/cli/doctor.ts +16 -3
  65. package/src/cli/help.ts +4 -1
  66. package/src/cli/index.ts +25 -14
  67. package/src/cli/integration-aside-sync.ts +2 -1
  68. package/src/cli/integration-plan-dto.ts +1 -0
  69. package/src/cli/integration-preview.ts +8 -1
  70. package/src/cli/integrations.ts +48 -6
  71. package/src/cli/message-args.ts +34 -0
  72. package/src/cli/message-command.ts +43 -0
  73. package/src/cli/message-runtime.ts +6 -0
  74. package/src/cli/observe.ts +3 -1
  75. package/src/cli/provider.ts +29 -6
  76. package/src/cli/registry.ts +24 -2
  77. package/src/cli/resolve.ts +25 -2
  78. package/src/cli/root.ts +4 -2
  79. package/src/cli/status.ts +64 -12
  80. package/src/cli/update-restart-child.ts +4 -2
  81. package/src/cli/update-restart-home.ts +28 -10
  82. package/src/cli/update-restart-service-record.ts +116 -0
  83. package/src/cli/update-restart-supervision.ts +96 -0
  84. package/src/cli/update-restart.ts +59 -13
  85. package/src/cli/v2.ts +27 -20
  86. package/src/cli/version-skew-notice.ts +77 -0
  87. package/src/clients/config-export/commandcode.ts +131 -0
  88. package/src/clients/config-export/contracts.ts +12 -0
  89. package/src/clients/config-export.ts +64 -1
  90. package/src/clients/lazycodex.ts +1 -1
  91. package/src/clients/omo-role-models.ts +32 -7
  92. package/src/codex/auth-api/account-list.ts +11 -3
  93. package/src/codex/auth-api/account-selection.ts +107 -0
  94. package/src/codex/auth-api/main-account-probe.ts +25 -8
  95. package/src/codex/auth-api/pool-mode-gate.ts +33 -9
  96. package/src/codex/auth-api/pool-quota-probe.ts +15 -1
  97. package/src/codex/auth-api/routes.ts +11 -49
  98. package/src/codex/auth-context.ts +20 -1
  99. package/src/codex/autostart-health.ts +12 -3
  100. package/src/codex/catalog/auto-review.ts +13 -6
  101. package/src/codex/catalog/build-entries.ts +33 -7
  102. package/src/codex/catalog/bundled.ts +155 -26
  103. package/src/codex/catalog/combo-member.ts +3 -2
  104. package/src/codex/catalog/control-plane.ts +45 -0
  105. package/src/codex/catalog/effort.ts +37 -6
  106. package/src/codex/catalog/gather-capture.ts +10 -3
  107. package/src/codex/catalog/model-hints.ts +15 -8
  108. package/src/codex/catalog/model-visibility.ts +3 -1
  109. package/src/codex/catalog/operator-tiers.ts +38 -0
  110. package/src/codex/catalog/parsing.ts +2 -1
  111. package/src/codex/catalog/provider-models.ts +58 -44
  112. package/src/codex/catalog/retained-sync.ts +7 -5
  113. package/src/codex/catalog/routed-gather.ts +15 -1
  114. package/src/codex/catalog-auto-refresh-sources.ts +3 -4
  115. package/src/codex/catalog-auto-refresh.ts +19 -0
  116. package/src/codex/catalog.ts +1 -1
  117. package/src/codex/codex-home-owner.ts +3 -0
  118. package/src/codex/config-write-lock.ts +333 -0
  119. package/src/codex/control-plane-models.ts +6 -0
  120. package/src/codex/convergence.ts +2 -1
  121. package/src/codex/features.ts +257 -200
  122. package/src/codex/inject/config-write-section.ts +34 -0
  123. package/src/codex/inject/multi-agent-v2.ts +14 -6
  124. package/src/codex/inject/remove.ts +36 -11
  125. package/src/codex/inject/restore.ts +85 -20
  126. package/src/codex/inject-coordination.ts +15 -6
  127. package/src/codex/inject.ts +62 -20
  128. package/src/codex/journal.ts +62 -17
  129. package/src/codex/main-account-cache.ts +43 -0
  130. package/src/codex/paths.ts +2 -0
  131. package/src/codex/prepared-home.ts +18 -0
  132. package/src/codex/prompt-journal.ts +25 -12
  133. package/src/codex/prompt-layers.ts +49 -26
  134. package/src/codex/prompt-lock-claim.ts +134 -0
  135. package/src/codex/prompt-lock-evidence.ts +26 -0
  136. package/src/codex/prompt-lock-io.ts +16 -0
  137. package/src/codex/prompt-lock-owner.ts +130 -0
  138. package/src/codex/prompt-lock.ts +84 -53
  139. package/src/codex/routing/failure-window.ts +146 -0
  140. package/src/codex/routing/selection.ts +4 -1
  141. package/src/codex/routing.ts +13 -17
  142. package/src/codex/runtime.ts +106 -18
  143. package/src/codex/shim.ts +45 -19
  144. package/src/combos/jev-decision-contract.ts +25 -2
  145. package/src/combos/jev-service-exchange.ts +281 -0
  146. package/src/combos/jev.ts +34 -219
  147. package/src/combos/types.ts +1 -1
  148. package/src/config/atomic-write.ts +1 -1
  149. package/src/config/codex-ws-reuse-setting.ts +10 -0
  150. package/src/config/diagnostics.ts +27 -16
  151. package/src/config/live-reconcile.ts +17 -0
  152. package/src/config/load-degrade.ts +42 -0
  153. package/src/config/rebase-provenance.ts +11 -0
  154. package/src/config/schema/anthropic-account-pool.ts +143 -0
  155. package/src/config/schema/config-schema.ts +11 -1
  156. package/src/config/schema/leaf-validators.ts +4 -0
  157. package/src/config.ts +2 -0
  158. package/src/generated/compatibility-version.json +566 -298
  159. package/src/generated/model-metadata.ts +7 -6
  160. package/src/grok/inject.ts +50 -4
  161. package/src/grok/sync.ts +2 -1
  162. package/src/images/loop.ts +30 -11
  163. package/src/integrations/aside-profiles.ts +10 -3
  164. package/src/integrations/catalog-refresh.ts +19 -3
  165. package/src/integrations/mutation-plan.ts +7 -0
  166. package/src/integrations/registry.ts +8 -0
  167. package/src/integrations/state.ts +9 -2
  168. package/src/integrations/writer.ts +17 -5
  169. package/src/lib/bun-path-runtime.d.mts +32 -0
  170. package/src/lib/bun-path-runtime.mjs +95 -0
  171. package/src/lib/bun-runtime-preflight.ts +140 -0
  172. package/src/lib/desktop-cli-handoff.d.mts +20 -0
  173. package/src/lib/desktop-cli-handoff.mjs +92 -0
  174. package/src/lib/desktop-cli-record.d.mts +29 -0
  175. package/src/lib/desktop-cli-record.mjs +102 -0
  176. package/src/lib/lab-live-route-production.ts +9 -1
  177. package/src/lib/request-execution-budget.ts +244 -58
  178. package/src/lib/spend-pool-alias-validation.ts +79 -0
  179. package/src/lib/spend-pool-continuity.ts +34 -0
  180. package/src/lib/spend-reservation-ledger.ts +412 -50
  181. package/src/lib/state-store-registrations.ts +20 -3
  182. package/src/lib/synced-state-location.ts +46 -6
  183. package/src/lib/test-home-guard.ts +87 -1
  184. package/src/lib/translator-budget.ts +17 -4
  185. package/src/lib/upstream-retry.ts +17 -5
  186. package/src/lib/windows-atomic-replace.ts +38 -16
  187. package/src/lib/windows-elevation.ts +6 -0
  188. package/src/lib/windows-secret-acl.ts +92 -25
  189. package/src/lib/winsw.ts +2 -3
  190. package/src/lib/workflow-budget.ts +53 -11
  191. package/src/messaging/budget.ts +45 -0
  192. package/src/messaging/discovery.ts +57 -0
  193. package/src/messaging/envelope.ts +49 -0
  194. package/src/messaging/input.ts +37 -0
  195. package/src/messaging/rpc.ts +196 -0
  196. package/src/messaging/send.ts +58 -0
  197. package/src/messaging/socket.ts +72 -0
  198. package/src/messaging/types.ts +29 -0
  199. package/src/oauth/anthropic-account-refusal.ts +90 -18
  200. package/src/oauth/anthropic-account-threshold.ts +8 -2
  201. package/src/oauth/anthropic-continuity.ts +15 -1
  202. package/src/oauth/anthropic-model-quota.ts +120 -48
  203. package/src/oauth/anthropic-model-routes.ts +11 -3
  204. package/src/oauth/anthropic-oauth-definitions.ts +98 -0
  205. package/src/oauth/anthropic-pool-config.ts +30 -0
  206. package/src/oauth/anthropic-rate-limit-policy.ts +64 -9
  207. package/src/oauth/anthropic-routing.ts +1202 -866
  208. package/src/oauth/anthropic-send-ownership.ts +41 -0
  209. package/src/oauth/anthropic.ts +4 -1
  210. package/src/oauth/chatgpt.ts +113 -41
  211. package/src/oauth/generic-account-failover.ts +3 -1
  212. package/src/oauth/health.ts +8 -6
  213. package/src/oauth/index.ts +96 -175
  214. package/src/oauth/kiro.ts +22 -9
  215. package/src/oauth/login-cli.ts +2 -0
  216. package/src/oauth/model-discovery-auth.ts +43 -0
  217. package/src/oauth/pool-kernel.ts +11 -5
  218. package/src/oauth/pool-settings-capability.ts +5 -3
  219. package/src/oauth/store-anthropic-instance.ts +87 -0
  220. package/src/oauth/store.ts +68 -10
  221. package/src/oauth/token-guardian.ts +2 -0
  222. package/src/oauth/types.ts +2 -0
  223. package/src/protocols/plan-snapshot.ts +5 -0
  224. package/src/protocols/settings.ts +20 -5
  225. package/src/providers/anthropic-instance-id.ts +47 -0
  226. package/src/providers/anthropic-instance.ts +48 -0
  227. package/src/providers/anthropic-reset-grant-ledger.ts +11 -1
  228. package/src/providers/azure-model-metadata.ts +115 -0
  229. package/src/providers/derive.ts +23 -0
  230. package/src/providers/kiro-models.ts +3 -0
  231. package/src/providers/label.ts +1 -1
  232. package/src/providers/model-rename-fields.ts +2 -0
  233. package/src/providers/quota/account-cache.ts +37 -11
  234. package/src/providers/quota/anthropic-account-quota.ts +93 -0
  235. package/src/providers/quota/anthropic-cooldown-recovery.ts +222 -111
  236. package/src/providers/quota/report-cache.ts +24 -1
  237. package/src/providers/quota/vendor-probes-key.ts +127 -17
  238. package/src/providers/quota/vendor-probes-oauth.ts +46 -14
  239. package/src/providers/quota.ts +23 -33
  240. package/src/providers/registry/entries-core.ts +35 -30
  241. package/src/providers/registry/model-ids.ts +1 -0
  242. package/src/providers/registry/model-seeds.ts +7 -4
  243. package/src/providers/registry/types.ts +3 -0
  244. package/src/providers/registry.ts +3 -3
  245. package/src/responses/reasoning-envelope.ts +29 -1
  246. package/src/responses/reasoning-replay-cache.ts +17 -1
  247. package/src/responses/spill-store.ts +20 -3
  248. package/src/responses/state/metrics.ts +95 -0
  249. package/src/responses/state/snapshot-codec.ts +6 -0
  250. package/src/responses/state/spill-failure.ts +12 -0
  251. package/src/responses/state/spill-queue.ts +13 -5
  252. package/src/responses/state/unforced-store-false.ts +55 -0
  253. package/src/responses/state.ts +68 -76
  254. package/src/responses/task-input.ts +17 -0
  255. package/src/router.ts +28 -4
  256. package/src/routing/compatibility/assemble.ts +1 -1
  257. package/src/routing/quota.ts +7 -3
  258. package/src/server/auth-cors.ts +5 -0
  259. package/src/server/background-lifecycle.ts +27 -0
  260. package/src/server/chat-completions.ts +7 -1
  261. package/src/server/chat-native.ts +64 -28
  262. package/src/server/claude-messages.ts +59 -8
  263. package/src/server/gui-static.ts +38 -4
  264. package/src/server/inbound-body-admission.ts +1 -0
  265. package/src/server/index/claude-intercept-lifecycle.ts +3 -1
  266. package/src/server/index/serve-options.ts +10 -2
  267. package/src/server/index/spend-ledger-lifecycle.ts +12 -5
  268. package/src/server/index/startup-warnings.ts +19 -0
  269. package/src/server/index.ts +5 -2
  270. package/src/server/inference/context.ts +1 -1
  271. package/src/server/lifecycle.ts +1 -1
  272. package/src/server/management/agent-settings-routes.ts +123 -38
  273. package/src/server/management/anthropic-account-threshold.ts +12 -9
  274. package/src/server/management/anthropic-pool-settings.ts +72 -0
  275. package/src/server/management/anthropic-reset-grant-routes.ts +98 -39
  276. package/src/server/management/codex-agent-role-routes.ts +4 -2
  277. package/src/server/management/codex-prompt-routes.ts +1 -0
  278. package/src/server/management/codex-role-auto-assign.ts +13 -2
  279. package/src/server/management/config-routes.ts +85 -14
  280. package/src/server/management/context.ts +2 -2
  281. package/src/server/management/decision-routes.ts +2 -1
  282. package/src/server/management/integration-routes.ts +25 -2
  283. package/src/server/management/oauth-account-routes.ts +79 -98
  284. package/src/server/management/provider-instance-ownership.ts +44 -0
  285. package/src/server/management/provider-routes.ts +21 -28
  286. package/src/server/management/route-registry.ts +3 -1
  287. package/src/server/management/routing-profile-routes.ts +4 -4
  288. package/src/server/management/shared.ts +3 -3
  289. package/src/server/management/system-restart.ts +44 -14
  290. package/src/server/management/system-routes.ts +24 -2
  291. package/src/server/management/vision-sidecar-options.ts +9 -6
  292. package/src/server/management/web-search-sidecar-options.ts +51 -2
  293. package/src/server/management-auth.ts +6 -0
  294. package/src/server/messages-native-eligibility.ts +3 -2
  295. package/src/server/messages-native-oauth.ts +91 -45
  296. package/src/server/messages-native-selector.ts +39 -0
  297. package/src/server/messages-native.ts +133 -38
  298. package/src/server/messages-response-headers.ts +14 -0
  299. package/src/server/request-log-conversation.ts +48 -5
  300. package/src/server/request-log.ts +36 -2
  301. package/src/server/responses/adapter-continuation.ts +31 -19
  302. package/src/server/responses/adapter-dispatch.ts +75 -24
  303. package/src/server/responses/antigravity-validation-refusal.ts +20 -6
  304. package/src/server/responses/codex-ws-exchange.ts +17 -2
  305. package/src/server/responses/codex-ws-pool.ts +100 -13
  306. package/src/server/responses/codex-ws-wire.ts +9 -0
  307. package/src/server/responses/combo-requested-effort.ts +3 -1
  308. package/src/server/responses/compact.ts +25 -8
  309. package/src/server/responses/compaction-recovery.ts +1 -1
  310. package/src/server/responses/core-codex-account.ts +22 -1
  311. package/src/server/responses/core-combo-native.ts +24 -4
  312. package/src/server/responses/core-combo.ts +38 -25
  313. package/src/server/responses/core-lifetime.ts +3 -36
  314. package/src/server/responses/core-normalize.ts +1 -0
  315. package/src/server/responses/core-options.ts +8 -1
  316. package/src/server/responses/core-replay.ts +78 -0
  317. package/src/server/responses/core.ts +3 -3
  318. package/src/server/responses/fetch-helpers.ts +26 -3
  319. package/src/server/responses/passthrough-delivery.ts +30 -4
  320. package/src/server/responses/passthrough-dispatch.ts +39 -9
  321. package/src/server/responses/request-prepare.ts +41 -5
  322. package/src/server/responses/request-send-budget.ts +150 -50
  323. package/src/server/responses/request-spend.ts +325 -33
  324. package/src/server/responses/request-transport.ts +132 -52
  325. package/src/server/responses/response-effects.ts +1 -0
  326. package/src/server/responses/run-turn-execution.ts +13 -3
  327. package/src/server/responses/sidecar-execution.ts +113 -22
  328. package/src/server/responses/sidecar-send-budget.ts +58 -0
  329. package/src/server/responses/ws-upstream.ts +19 -6
  330. package/src/server/responses-undeclared-tool-guard.ts +5 -1
  331. package/src/server/startup-health-cache.ts +2 -1
  332. package/src/server/workflow-refusal.ts +33 -0
  333. package/src/service/cli.ts +20 -4
  334. package/src/service/desktop-command-guard.ts +32 -0
  335. package/src/service/desktop-startup.ts +66 -40
  336. package/src/service/desktop-supervision.d.mts +26 -0
  337. package/src/service/desktop-supervision.mjs +140 -0
  338. package/src/service/managing-cli.ts +1 -0
  339. package/src/service/orchestration.ts +46 -20
  340. package/src/service/ownership-mutation-lease.d.mts +1 -1
  341. package/src/service/ownership-mutation-lease.mjs +5 -7
  342. package/src/service/repair.ts +25 -9
  343. package/src/service/state.ts +4 -3
  344. package/src/service/systemd.ts +17 -8
  345. package/src/service/windows-ops.ts +61 -22
  346. package/src/sidecar/anthropic-binding.ts +79 -0
  347. package/src/sidecar/auth.ts +76 -2
  348. package/src/storage/policy-job.ts +5 -1
  349. package/src/storage/policy-worker.ts +3 -1
  350. package/src/storage/policy.ts +9 -0
  351. package/src/types/anthropic-account-pool.ts +33 -0
  352. package/src/types/config.ts +33 -28
  353. package/src/types/provider.ts +5 -0
  354. package/src/types/request.ts +5 -0
  355. package/src/types/tools.ts +7 -0
  356. package/src/types/wire.ts +1 -0
  357. package/src/types.ts +4 -0
  358. package/src/update/index.ts +57 -9
  359. package/src/update/restart-ownership.ts +10 -3
  360. package/src/update/runtime-ownership.d.mts +6 -0
  361. package/src/update/runtime-ownership.mjs +23 -7
  362. package/src/usage/cost.ts +3 -2
  363. package/src/usage/expected-prices.ts +66 -14
  364. package/src/vision/anthropic-describe.ts +16 -3
  365. package/src/vision/index.ts +3 -0
  366. package/src/vision/plan.ts +19 -6
  367. package/src/web-search/alpha-search.ts +7 -1
  368. package/src/web-search/anthropic-executor.ts +16 -4
  369. package/src/web-search/index.ts +7 -3
  370. package/src/web-search/loop.ts +32 -7
  371. package/src/web-search/passthrough-bridge.ts +4 -1
  372. package/src/web-search/sidecar-providers.ts +4 -3
  373. package/gui/dist/assets/App-MbDVfUeC.js +0 -54
  374. package/gui/dist/assets/Tray-CzkxK82A.js +0 -1
  375. package/gui/dist/assets/index-UJt6npXk.js +0 -94
  376. package/gui/dist/assets/quota-summary-DU3sD6Z-.js +0 -1
@@ -1,4 +1,5 @@
1
1
  import { createHmac, randomBytes } from "node:crypto";
2
+ import { codexWsReuseAcrossTurnsEnabled } from "../../config/codex-ws-reuse-setting";
2
3
  import { registerOptionalShutdownHook } from "../../lib/optional-shutdown-hooks";
3
4
  import { CODEX_RESPONSES_HTTP_URL, CODEX_WS_FRAME_HEADERS } from "./codex-ws-request";
4
5
  import { CODEX_WS_ID_MAX_BYTES } from "./codex-ws-correlation";
@@ -7,11 +8,15 @@ import { CodexWsSession } from "./codex-ws-session";
7
8
  export const CODEX_WS_POOL_MAX_SESSIONS = 32;
8
9
  export const CODEX_WS_POOL_IDLE_MS = 30_000;
9
10
  export const CODEX_WS_POOL_MAX_AGE_MS = 5 * 60_000;
11
+ export const CODEX_WS_POOL_WAIT_MS = 750;
12
+ /** Retained sockets per account+thread while cross-turn reuse is on. */
13
+ export const CODEX_WS_POOL_SCOPE_LIMIT = 2;
14
+ const CODEX_WS_POOL_WAIT_DEPTH = 2;
10
15
  const MUTABLE_HEADERS = new Set<string>(CODEX_WS_FRAME_HEADERS);
11
16
  let processKey: Buffer | undefined;
12
17
  let poolSequence = 0;
13
18
 
14
- export interface CodexWsReuseIdentity { key: string; scope: string }
19
+ export interface CodexWsReuseIdentity { key: string; scope: string; routeTag: string }
15
20
  function value(value: unknown): value is string {
16
21
  return typeof value === "string" && value.trim().length > 0
17
22
  && !/[\u0000-\u001f\u007f]/.test(value) && Buffer.byteLength(value) <= CODEX_WS_ID_MAX_BYTES;
@@ -49,19 +54,23 @@ export function codexWsReuseIdentity(url: string, headers: Record<string, string
49
54
  const immutable = Object.entries(headers).filter(([name]) => !MUTABLE_HEADERS.has(name)).sort(([a], [b]) => a.localeCompare(b));
50
55
  if (immutable.length > 128 || immutable.some(([, field]) => !value(field))
51
56
  || immutable.reduce((bytes, [name, field]) => bytes + Buffer.byteLength(name) + Buffer.byteLength(field), 0) > 32 * 1024) return null;
52
- const scope = digest([url, account, thread, turn]);
57
+ const reuseAcrossTurns = codexWsReuseAcrossTurnsEnabled();
58
+ const scope = digest(reuseAcrossTurns ? [url, account, thread] : [url, account, thread, turn]);
53
59
  const lite = metadata.ws_request_header_x_openai_internal_codex_responses_lite;
54
60
  if (lite !== undefined && lite !== "true" && lite !== "false") return null;
55
61
  // `dialUrl` is the socket destination after plugin rewrites; absent means the canonical one.
56
- return { scope, key: digest([scope, authorization, body.model, body.service_tier ?? null, lite ?? null, immutable, proxy ?? null, dialUrl ?? null]) };
62
+ const routeTag = digest(["route", authorization, proxy ?? null, dialUrl ?? null]);
63
+ return { scope, routeTag, key: digest([scope, authorization, body.model, body.service_tier ?? null, lite ?? null, immutable, proxy ?? null, dialUrl ?? null]) };
57
64
  }
58
65
 
59
66
  interface Entry { identity: CodexWsReuseIdentity; session: CodexWsSession; createdAt: number; idleAt: number; retired: boolean }
60
- interface PoolOptions { now?: () => number; maxSessions?: number; idleMs?: number; maxAgeMs?: number }
67
+ interface Waiter { key: string; resolve: (session: CodexWsSession | null) => void; timer: ReturnType<typeof setTimeout>; onAbort: () => void; signal?: AbortSignal }
68
+ interface PoolOptions { now?: () => number; maxSessions?: number; idleMs?: number; maxAgeMs?: number; waitMs?: number }
61
69
 
62
70
  /** Bounded retained sockets only. Busy/capacity misses keep the existing one-shot path. */
63
71
  export class CodexWsPool {
64
72
  private readonly entries = new Map<string, Entry>();
73
+ private readonly waiters: Waiter[] = [];
65
74
  private timer?: ReturnType<typeof setTimeout>;
66
75
  private detachShutdown?: () => void;
67
76
  private readonly hookKey = `codex-upstream-ws-pool-${++poolSequence}`;
@@ -69,29 +78,74 @@ export class CodexWsPool {
69
78
  private readonly maxSessions: number;
70
79
  private readonly idleMs: number;
71
80
  private readonly maxAgeMs: number;
81
+ private readonly waitMs: number;
72
82
  constructor(options: PoolOptions = {}) {
73
83
  this.now = options.now ?? Date.now;
74
84
  this.maxSessions = options.maxSessions ?? CODEX_WS_POOL_MAX_SESSIONS;
75
85
  this.idleMs = options.idleMs ?? CODEX_WS_POOL_IDLE_MS;
76
86
  this.maxAgeMs = options.maxAgeMs ?? CODEX_WS_POOL_MAX_AGE_MS;
87
+ this.waitMs = options.waitMs ?? CODEX_WS_POOL_WAIT_MS;
77
88
  }
78
89
 
79
90
  acquire(identity: CodexWsReuseIdentity, url: string, headers: Record<string, string>, proxy?: string): CodexWsSession | null {
80
- this.sweep();
81
- for (const entry of this.entries.values()) {
82
- if (entry.identity.scope !== identity.scope || entry.identity.key === identity.key) continue;
83
- entry.retired = true;
84
- if (!entry.session.busy) this.remove(entry);
91
+ return this.claim(identity, url, headers, proxy).session;
92
+ }
93
+
94
+ /**
95
+ * Before any frame is sent, wait for the matching busy socket instead of dialing a throwaway.
96
+ * The queue is FIFO and at most two deep. An abort or the deadline returns null.
97
+ */
98
+ acquireWaiting(
99
+ identity: CodexWsReuseIdentity,
100
+ url: string,
101
+ headers: Record<string, string>,
102
+ proxy: string | undefined,
103
+ signal?: AbortSignal,
104
+ ): Promise<CodexWsSession | null> {
105
+ const claimed = this.claim(identity, url, headers, proxy);
106
+ if (claimed.session || !claimed.busy) return Promise.resolve(claimed.session);
107
+ if (signal?.aborted) return Promise.resolve(null);
108
+ if (this.waiters.filter(waiter => waiter.key === identity.key).length >= CODEX_WS_POOL_WAIT_DEPTH) {
109
+ return Promise.resolve(null);
85
110
  }
111
+ return new Promise(resolve => {
112
+ const waiter: Waiter = {
113
+ key: identity.key,
114
+ resolve,
115
+ signal,
116
+ timer: setTimeout(() => this.finishWaiter(waiter, null), this.waitMs),
117
+ onAbort: () => this.finishWaiter(waiter, null),
118
+ };
119
+ waiter.timer.unref?.();
120
+ signal?.addEventListener("abort", waiter.onAbort, { once: true });
121
+ this.waiters.push(waiter);
122
+ });
123
+ }
124
+
125
+ private claim(
126
+ identity: CodexWsReuseIdentity,
127
+ url: string,
128
+ headers: Record<string, string>,
129
+ proxy?: string,
130
+ ): { session: CodexWsSession | null; busy: boolean } {
131
+ this.sweep();
132
+ this.retireSiblings(identity);
86
133
  const existing = this.entries.get(identity.key);
87
134
  if (existing) {
88
- if (existing.retired || existing.session.busy) return null;
89
- if (existing.session.reserve()) { this.arm(); return existing.session; }
135
+ if (existing.retired) return { session: null, busy: false };
136
+ if (existing.session.busy) return { session: null, busy: true };
137
+ if (existing.session.reserve()) { this.arm(); return { session: existing.session, busy: false }; }
90
138
  this.remove(existing);
139
+ } else if (codexWsReuseAcrossTurnsEnabled() && this.scopeCount(identity.scope) >= CODEX_WS_POOL_SCOPE_LIMIT) {
140
+ const oldest = [...this.entries.values()]
141
+ .filter(entry => entry.identity.scope === identity.scope && !entry.session.busy)
142
+ .sort((left, right) => left.idleAt - right.idleAt)[0];
143
+ if (!oldest) return { session: null, busy: false };
144
+ this.remove(oldest);
91
145
  }
92
146
  if (this.entries.size >= this.maxSessions) {
93
147
  const oldest = [...this.entries.values()].filter(entry => !entry.session.busy).sort((a, b) => a.idleAt - b.idleAt)[0];
94
- if (!oldest) return null;
148
+ if (!oldest) return { session: null, busy: false };
95
149
  this.remove(oldest);
96
150
  }
97
151
  const createdAt = this.now();
@@ -100,13 +154,45 @@ export class CodexWsPool {
100
154
  session.reserve();
101
155
  this.entries.set(identity.key, entry);
102
156
  this.detachShutdown ??= registerOptionalShutdownHook(this.hookKey, () => this.dispose());
103
- return session;
157
+ return { session, busy: false };
158
+ }
159
+
160
+ private retireSiblings(identity: CodexWsReuseIdentity): void {
161
+ const reuseAcrossTurns = codexWsReuseAcrossTurnsEnabled();
162
+ for (const entry of this.entries.values()) {
163
+ if (entry.identity.scope !== identity.scope || entry.identity.key === identity.key) continue;
164
+ const routeChanged = entry.identity.routeTag !== identity.routeTag;
165
+ if (reuseAcrossTurns && !routeChanged) continue;
166
+ entry.retired = true;
167
+ if (!entry.session.busy) this.remove(entry);
168
+ }
169
+ }
170
+
171
+ private scopeCount(scope: string): number {
172
+ let count = 0;
173
+ for (const entry of this.entries.values()) if (entry.identity.scope === scope && !entry.retired) count += 1;
174
+ return count;
175
+ }
176
+
177
+ private finishWaiter(waiter: Waiter, session: CodexWsSession | null): void {
178
+ const index = this.waiters.indexOf(waiter);
179
+ if (index < 0) return;
180
+ this.waiters.splice(index, 1);
181
+ clearTimeout(waiter.timer);
182
+ waiter.signal?.removeEventListener("abort", waiter.onAbort);
183
+ waiter.resolve(session);
104
184
  }
105
185
 
106
186
  private changed(entry: Entry): void {
107
187
  if (this.entries.get(entry.identity.key) !== entry) return;
108
188
  if (entry.session.closed) this.entries.delete(entry.identity.key);
109
189
  else if (!entry.session.busy) {
190
+ const waiter = this.waiters.find(candidate => candidate.key === entry.identity.key);
191
+ if (waiter && !entry.retired && entry.session.reserve()) {
192
+ this.finishWaiter(waiter, entry.session);
193
+ this.arm();
194
+ return;
195
+ }
110
196
  entry.idleAt = this.now();
111
197
  if (entry.retired || entry.idleAt - entry.createdAt >= this.maxAgeMs) this.remove(entry);
112
198
  }
@@ -152,6 +238,7 @@ export class CodexWsPool {
152
238
  this.detachShutdown = undefined;
153
239
  const entries = [...this.entries.values()];
154
240
  this.entries.clear();
241
+ for (const waiter of [...this.waiters]) this.finishWaiter(waiter, null);
155
242
  for (const entry of entries) entry.session.dispose(new DOMException("codex websocket pool shutdown", "AbortError"));
156
243
  }
157
244
 
@@ -49,6 +49,7 @@ const WS_CLOSE_MESSAGE_TOO_BIG = 1009;
49
49
 
50
50
  const codexWsUpstreamResponses = new WeakSet<Response>();
51
51
  const quotaObservedResponses = new WeakSet<Response>();
52
+ const codexWsPreludeProjections = new WeakSet<Response>();
52
53
 
53
54
  /** Quota arrived directly at its captured account; do not replay old HTTP prelude headers. */
54
55
  export function isCodexWsQuotaObservedResponse(response: Response): boolean {
@@ -60,6 +61,14 @@ export function isCodexWsUpstreamResponse(response: Response): boolean {
60
61
  return codexWsUpstreamResponses.has(response);
61
62
  }
62
63
 
64
+ /** Pre-response projection; its headers retain the exchange's prelude snapshot. */
65
+ export function markCodexWsPreludeProjection(response: Response): void {
66
+ codexWsPreludeProjections.add(response);
67
+ }
68
+
69
+ export function isCodexWsPreludeProjection(response: Response): boolean {
70
+ return codexWsPreludeProjections.has(response);
71
+ }
63
72
 
64
73
  export function markCodexWsResponse(response: Response, observed: boolean): void {
65
74
  codexWsUpstreamResponses.add(response);
@@ -10,5 +10,7 @@ export function comboRequestedEffortLabel(
10
10
  const forcedTransition = forcedEffort != null && forcedEffort !== originalRequestedEffort
11
11
  ? `->${forcedEffort}`
12
12
  : "";
13
- return `${originalRequestedEffort}${forcedTransition}${laterTransitions}`;
13
+ const segments = `${originalRequestedEffort}${forcedTransition}${laterTransitions}`.split("->");
14
+ // A pin that kept the effort it was given is not a transition; drop the repeated segment.
15
+ return segments.filter((segment, index) => index === 0 || segment !== segments[index - 1]).join("->");
14
16
  }
@@ -1,3 +1,6 @@
1
+ import { createPhysicalSendReporter } from "../../lib/request-execution-budget";
2
+ import { createInferenceSendBudget } from "../inference/context";
3
+ import { unboundPoolSpendRefusalResponse } from "../workflow-refusal";
1
4
  import { capturePoolQuotaWriter } from "../../codex/account-store";
2
5
  import { previewXaiOauthWireModel } from "./core-normalize";
3
6
  import {
@@ -722,6 +725,7 @@ export async function handleResponsesCompact(
722
725
  logCtx.provider = route.codexAccountNamespace
723
726
  ? `${route.providerName}-${route.codexAccountNamespace}`
724
727
  : route.providerName;
728
+ logCtx.spendPoolId = route.providerName;
725
729
  logCtx.providerAdapter = route.provider.adapter;
726
730
  // #4940, and the same refusal the ordinary Responses path makes in request-prepare.ts. Compact has
727
731
  // to repeat it rather than inherit it: the native branch below dispatches straight to
@@ -804,7 +808,7 @@ export async function handleResponsesCompact(
804
808
  // failure that hands off, continues here. The routed turn used to call handleResponses with
805
809
  // no budget at all, so `handleResponsesInner` minted a fresh four after the native attempt
806
810
  // had already spent some of the first one.
807
- const sendBudget: RequestExecutionBudget = options.sendBudget ?? createRequestExecutionBudget();
811
+ const sendBudget: RequestExecutionBudget = options.sendBudget ?? createInferenceSendBudget(req, logCtx);
808
812
  // A manual override onto another backend must not mint ciphertext the conversation model cannot replay.
809
813
  const manualOverrideCrossesProvider = options.compactionRoutingOverride
810
814
  ? !compactionRoutingKeepsProviderIdentity(config, options.compactionRoutingOverride, route)
@@ -1022,12 +1026,21 @@ export async function handleResponsesCompact(
1022
1026
  // `kind === "pool"` and a main-pool credential left it false -- so 401 then 429 really did
1023
1027
  // reach five. The single sends spend the base allowance first and then the one shared
1024
1028
  // final-recovery reserve, which is the same rule the Responses path follows.
1025
- const sendSingleCompactAttempt = (
1029
+ const sendSingleCompactAttempt = async (
1026
1030
  doFetch: () => Promise<Response>,
1027
1031
  ): Promise<Response> => {
1028
1032
  if (sendBudget.remainingBaseSends(TRANSIENT_RETRY_MAX_ATTEMPTS) > 0) {
1029
- sendBudget.used += 1;
1030
- return doFetch();
1033
+ const report = createPhysicalSendReporter(sendBudget, () => ({ poolId: logCtx.spendPoolId ?? route.providerName,
1034
+ identityId: logCtx.accountLogLabel }));
1035
+ let started = false;
1036
+ try {
1037
+ if (report.beforeSend?.() === false) throw new SendBudgetExhaustedError(safeHostLabel(compactUrl));
1038
+ started = true;
1039
+ return await doFetch();
1040
+ } finally {
1041
+ try { if (started) report(1); }
1042
+ finally { report.close?.(); }
1043
+ }
1031
1044
  }
1032
1045
  const decision = sendBudget.reserveDispatch({
1033
1046
  sendClass: "auth-recovery",
@@ -1076,7 +1089,8 @@ export async function handleResponsesCompact(
1076
1089
  // Draws the shared remainder instead of a fresh three. Compact is a native endpoint
1077
1090
  // of the same logical turn, so its sends belong to the same cap.
1078
1091
  attempts: sendBudget.remainingBaseSends(TRANSIENT_RETRY_MAX_ATTEMPTS),
1079
- onSendsConsumed: (used: number) => { sendBudget.used += Math.max(0, used); },
1092
+ onSendsConsumed: createPhysicalSendReporter(sendBudget, () => ({ poolId: logCtx.spendPoolId ?? route.providerName,
1093
+ identityId: logCtx.accountLogLabel })),
1080
1094
  });
1081
1095
  };
1082
1096
 
@@ -1084,9 +1098,12 @@ export async function handleResponsesCompact(
1084
1098
  // actually happens, so every recorder call names the context that produced it.
1085
1099
  let outcomeCtx = authCtx;
1086
1100
  const localDispatchRefusal = (error: unknown): Response | undefined => {
1087
- const response = mapCodexAuthContextErrorToResponse(unwrapUpstreamRetryEvidenceError(error), {
1088
- now: Date.now(), accountSelector: route.codexAccountNamespace,
1089
- });
1101
+ const cause = unwrapUpstreamRetryEvidenceError(error);
1102
+ const response = cause instanceof SendBudgetExhaustedError
1103
+ ? unboundPoolSpendRefusalResponse(logCtx) ?? formatErrorResponse(429, "request_send_budget_exhausted", cause.message)
1104
+ : mapCodexAuthContextErrorToResponse(cause, {
1105
+ now: Date.now(), accountSelector: route.codexAccountNamespace,
1106
+ });
1090
1107
  if (response) {
1091
1108
  releaseUpstreamHostAdmission(compactHostAdmissionLease);
1092
1109
  compactHostAdmissionLease = null;
@@ -47,7 +47,7 @@ function settlePhysicalSends(log: RequestLogContext, budget: RequestExecutionBud
47
47
  // An external report may settle a prepaid booking without changing used. Its explicit
48
48
  // receipt outranks the numeric delta, or the same source send would be charged twice.
49
49
  const unreported = Math.max(0, sent - Math.max(reported, Math.max(0, budget.used - beforeUsed)));
50
- if (unreported > 0) budget.used += unreported;
50
+ if (unreported > 0 && !budget.spendEnforced) budget.used += unreported;
51
51
  return sent;
52
52
  }
53
53
 
@@ -6,6 +6,7 @@ import {
6
6
  computeQuotaCooldown,
7
7
  formatCodexProviderForLog,
8
8
  } from "../../codex/routing";
9
+ import { claimMainQuotaDispatchForWs, isMainQuotaDispatchLive, renewMainQuotaDispatchForAttempt, type MainQuotaDispatch } from "../../codex/main-account-cache";
9
10
  import type { CodexWsQuotaObserver } from "./codex-ws-metadata";
10
11
  import { isCanonicalOpenAiForwardProvider } from "../../providers/openai-tiers";
11
12
  import { isCodexAccountGenerationLive } from "../../codex/account-store";
@@ -121,8 +122,28 @@ export function usesCodexForwardPoolAuth(
121
122
  }
122
123
 
123
124
 
125
+ /** Live proof that this plain-main response used the observed main credential. */
126
+ export function liveMainQuotaDispatch(
127
+ authCtx: CodexAuthContext, provider: OcxProviderConfig,
128
+ ): MainQuotaDispatch | undefined {
129
+ if (authCtx.kind !== "main" || !authCtx.mainQuotaDispatch) return undefined;
130
+ if (!isCanonicalOpenAiForwardProvider(provider)
131
+ || provider.authMode !== "forward" || provider.adapter !== "openai-responses") return undefined;
132
+ return isMainQuotaDispatchLive(authCtx.mainQuotaDispatch) ? authCtx.mainQuotaDispatch : undefined;
133
+ }
134
+
124
135
  export function codexWsQuotaObserver(authCtx: CodexAuthContext, provider: OcxProviderConfig, modelId?: string): CodexWsQuotaObserver | undefined {
125
- if (!isCanonicalOpenAiForwardProvider(provider) || !usesCodexForwardPoolAuth(authCtx, provider)) return undefined;
136
+ if (!isCanonicalOpenAiForwardProvider(provider)) return undefined;
137
+ if (!usesCodexForwardPoolAuth(authCtx, provider)) {
138
+ const captured = liveMainQuotaDispatch(authCtx, provider);
139
+ if (!captured || authCtx.kind !== "main") return undefined;
140
+ const dispatch = authCtx.mainQuotaDispatch = renewMainQuotaDispatchForAttempt(captured);
141
+ return headers => {
142
+ claimMainQuotaDispatchForWs(dispatch);
143
+ if (!isMainQuotaDispatchLive(dispatch)) return;
144
+ applyCapturedCodexQuota(MAIN_CODEX_ACCOUNT_ID, headers, dispatch.configGeneration, dispatch.writer, { modelId });
145
+ };
146
+ }
126
147
  const { accountId, writerGeneration } = authCtx;
127
148
  const credentialGeneration = authCtx.kind === "pool" ? authCtx.generation : undefined;
128
149
  const mainWriter = authCtx.kind === "main-pool" ? authCtx.mainQuotaWriter : undefined;
@@ -18,7 +18,7 @@ import type { OcxConfig } from "../../types";
18
18
  import type { RouteResult } from "../../router";
19
19
  import type { DataPlaneAdmission } from "../auth-cors";
20
20
  import type { AdmissionLease } from "../../lib/admission";
21
- import type { RequestExecutionBudget } from "../../lib/request-execution-budget";
21
+ import type { RequestExecutionBudget, SingleUseDispatchPermit } from "../../lib/request-execution-budget";
22
22
  import type { TransientSendBudget } from "../../lib/upstream-retry";
23
23
  import type { ResponsesTerminalStatus } from "../../bridge";
24
24
  import type { NativeChatFinishLog } from "../chat-native";
@@ -32,7 +32,7 @@ import {
32
32
  type RequestLogContext,
33
33
  } from "../request-log";
34
34
  import { captureRouteStaticPolicy, routeConcreteModel } from "../../router";
35
- import { comboDefaultEffort, concreteComboRequestBody, getCombo } from "../../combos";
35
+ import { comboDefaultEffort, concreteComboRequestBody, getCombo, type JevDecision } from "../../combos";
36
36
  import { supportedLadderFor } from "../effort-policy";
37
37
  import { applyDroidReasoningDefault } from "../droid-reasoning-default";
38
38
  import { resolveWireProtocolOverride } from "../adapter-resolve";
@@ -62,6 +62,7 @@ export interface NativeComboChildRun {
62
62
  childLog: RequestLogContext;
63
63
  attemptHandle: InferenceAttempt;
64
64
  sendBudget: RequestExecutionBudget;
65
+ comboDispatchPermit?: SingleUseDispatchPermit;
65
66
  turnAdmissionLease?: AdmissionLease;
66
67
  onFirstOutput: () => void;
67
68
  /** Reports to the combo's child callbacks; it never writes the parent's final row itself. */
@@ -93,6 +94,7 @@ export interface ComboProtocolLanes {
93
94
  target: ComboTarget,
94
95
  targetRoute: RouteResult,
95
96
  targetSendBudget: TransientSendBudget | undefined,
97
+ initialDecision?: Pick<JevDecision, "effort">,
96
98
  ): NativeComboChildPlan | undefined;
97
99
  }
98
100
 
@@ -100,6 +102,7 @@ interface CandidateVerdict {
100
102
  skip?: ProtocolFeature[];
101
103
  }
102
104
 
105
+ /** Build opt-in native eligibility and child plans; only the initial plan applies a JEV choice. */
103
106
  export function createComboProtocolLanes(input: {
104
107
  source: ComboProtocolSource | undefined;
105
108
  req: Request;
@@ -202,7 +205,7 @@ export function createComboProtocolLanes(input: {
202
205
  400, unrepresentableMessage(features), "invalid_request_error", "unsupported_feature",
203
206
  ), "chat");
204
207
  },
205
- nativeChild: (target, targetRoute, targetSendBudget) => {
208
+ nativeChild: (target, targetRoute, targetSendBudget, initialDecision) => {
206
209
  // Without the request's execution budget a native child could not share its sends, so it
207
210
  // keeps the bridge rather than opening a tracker of its own.
208
211
  if (!isRequestExecutionBudget(targetSendBudget)) return undefined;
@@ -213,7 +216,22 @@ export function createComboProtocolLanes(input: {
213
216
  provider: targetRoute.provider,
214
217
  modelId: targetRoute.modelId,
215
218
  });
216
- applyComboEffort(body, config, comboId, target, targetRoute);
219
+ if (initialDecision) {
220
+ // A validated JEV choice owns only this first dispatch, including explicit no-effort.
221
+ // Do not run ordinary force defaults for null: they require a non-null default.
222
+ delete body.service_tier;
223
+ delete body.thinking_budget;
224
+ delete body.thinking;
225
+ const reasoning = body.reasoning;
226
+ if (reasoning && typeof reasoning === "object" && !Array.isArray(reasoning)) {
227
+ const preserved = { ...reasoning as Rec };
228
+ delete preserved.effort;
229
+ if (Object.keys(preserved).length > 0) body.reasoning = preserved;
230
+ else delete body.reasoning;
231
+ }
232
+ if (initialDecision.effort === null) delete body.reasoning_effort;
233
+ else body.reasoning_effort = initialDecision.effort;
234
+ } else applyComboEffort(body, config, comboId, target, targetRoute);
217
235
  return { route, body, sendBudget: targetSendBudget };
218
236
  },
219
237
  };
@@ -272,6 +290,7 @@ export interface ComboChildCallbacks {
272
290
  export async function dispatchNativeComboChild(input: {
273
291
  source: ComboProtocolSource;
274
292
  plan: NativeComboChildPlan;
293
+ comboDispatchPermit?: SingleUseDispatchPermit;
275
294
  logCtx: RequestLogContext;
276
295
  childLog: RequestLogContext;
277
296
  attempt: PersistedUsageAttempt;
@@ -317,6 +336,7 @@ export async function dispatchNativeComboChild(input: {
317
336
  childLog,
318
337
  attemptHandle,
319
338
  sendBudget: plan.sendBudget,
339
+ comboDispatchPermit: input.comboDispatchPermit,
320
340
  ...(input.turnAdmissionLease ? { turnAdmissionLease: input.turnAdmissionLease } : {}),
321
341
  onFirstOutput: () => {
322
342
  outputSeen = true;
@@ -52,11 +52,10 @@ import { routeConcreteModel, comboRouteDecisionTrace } from "../../router";
52
52
  import { memoryModelRouteReason } from "./memory-models";
53
53
  import { poolAccountProviderLabel } from "../../providers/label";
54
54
  import { getAccountSet } from "../../oauth/store";
55
+ import { configuredAnthropicInstance } from "../../providers/anthropic-instance";
55
56
  import {
57
+ anthropicRoutingFor,
56
58
  formatAnthropicProviderForLog,
57
- getAnthropicAccountHealthSnapshot,
58
- getAnthropicPoolRetryAfterSeconds,
59
- getEligibleAnthropicAccounts,
60
59
  } from "../../oauth/anthropic-routing";
61
60
  import { codexAccountLogLabel } from "../../codex/account-label";
62
61
  import { codexQuotaScopeForModel, getCodexQuotaHealthSnapshot } from "../../codex/routing";
@@ -118,10 +117,11 @@ export const COMBO_TARGET_BASE_SENDS = CODEX_TEXT_GUARDED_BUDGET_POLICY.baseSend
118
117
 
119
118
  function cooledPoolAccountLabel(config: OcxConfig, providerName: string, modelId: string, label: string | undefined): string | undefined {
120
119
  if (!label) return undefined;
121
- if (providerName === "anthropic") {
122
- const matches = getAccountSet("anthropic")?.accounts.filter(account =>
123
- formatAnthropicProviderForLog("anthropic", account.id) === label) ?? [];
124
- return matches.length === 1 && getAnthropicAccountHealthSnapshot(matches[0]!.id) ? label : undefined;
120
+ const instance = configuredAnthropicInstance(config, providerName);
121
+ if (instance && config.providers[providerName]?.authMode === "oauth") {
122
+ const matches = getAccountSet(instance)?.accounts.filter(account =>
123
+ formatAnthropicProviderForLog(instance, account.id) === label) ?? [];
124
+ return matches.length === 1 && anthropicRoutingFor(instance).getAnthropicAccountHealthSnapshot(matches[0]!.id) ? label : undefined;
125
125
  }
126
126
  const provider = config.providers[providerName];
127
127
  if (!provider || !isCanonicalOpenAiForwardProvider(provider)) return undefined;
@@ -152,12 +152,13 @@ export function isAnthropicPoolLocalRefusal(
152
152
  failedAccount: string | undefined,
153
153
  now = Date.now(),
154
154
  ): boolean {
155
- return providerName === "anthropic"
155
+ const instance = configuredAnthropicInstance(config, providerName);
156
+ return instance !== undefined
156
157
  && config.providers[providerName]?.authMode === "oauth"
157
158
  && status === 429
158
159
  && failedAccount === undefined
159
- && getEligibleAnthropicAccounts(now).length === 0
160
- && getAnthropicPoolRetryAfterSeconds(now) !== null;
160
+ && anthropicRoutingFor(instance).getEligibleAnthropicAccounts(now).length === 0
161
+ && anthropicRoutingFor(instance).getAnthropicPoolRetryAfterSeconds(now) !== null;
161
162
  }
162
163
 
163
164
  /**
@@ -713,30 +714,35 @@ export async function executeComboResponses(
713
714
  while (pick) {
714
715
  if (options.abortSignal?.aborted) return clientCancelledResponse();
715
716
  const firstComboTarget = comboTargetsDispatched === 0;
716
- // The first target seeds the ledger's target identity and charges nothing; every later one
717
- // is a real transition, refused once the declared hops, the alternate-target ledger or the
718
- // request total are spent. `countedExternally` is required: the child charges its own
719
- // physical sends, and charging here as well would halve the cap without saying so.
717
+ const targetRoute = routeConcreteModel(config, `${pick.target.provider}/${pick.target.model}`);
718
+ // The inherited spend tracker observes this parent log, before the child has its own label.
719
+ logCtx.spendPoolId = targetRoute.providerName;
720
+ // Derive the target's allowance before booking its initial send; the booking occupies it.
721
+ const targetSendBudget = comboSendScope
722
+ ? comboTargetSendBudget(comboSendScope, combo.targets.length - 1 - comboTargetsDispatched)
723
+ : options.sendBudget;
724
+ // Every target prepays one send. Only its child may consume that booking; later targets
725
+ // remain bounded by the shared total, transition ledger and per-target holdback.
726
+ // `countedExternally` is required: the child charges its own physical sends against this
727
+ // booking through the exact permit, and charging here as well would halve the cap.
720
728
  const hopDecision = comboSendScope?.reserveDispatch({
721
729
  sendClass: firstComboTarget ? "initial" : "combo-failover",
722
730
  targetKey: `${pick.target.provider}/${pick.target.model}`,
723
731
  countedExternally: true,
724
732
  });
725
- if (hopDecision && hopDecision.allowed) hopDecision.permit.use();
726
- else if (hopDecision && firstComboTarget) {
733
+ if (hopDecision && !hopDecision.allowed && firstComboTarget) {
727
734
  // A refused initial reservation authorizes no child send and has no upstream failure to return.
728
735
  return formatErrorResponse(429, SEND_BUDGET_EXHAUSTED_CODE, "request send budget exhausted before combo dispatch");
729
736
  }
730
- else if (hopDecision) {
737
+ else if (hopDecision && !hopDecision.allowed) {
731
738
  // Out of budget is not this target's failure. The established exhaustion contract is to
732
739
  // return the last real upstream answer with its status, headers and any quota body
733
740
  // intact rather than to mint a synthetic error, and a later target only exists because
734
741
  // an earlier one already recorded one.
735
742
  return exhaustedFailure();
736
743
  }
737
- const targetSendBudget = comboSendScope
738
- ? comboTargetSendBudget(comboSendScope, combo.targets.length - 1 - comboTargetsDispatched)
739
- : options.sendBudget;
744
+ const initialSend = hopDecision?.allowed ? { permit: hopDecision.permit, producerOwned: false } : undefined;
745
+ try {
740
746
  comboTargetsDispatched += 1;
741
747
  const childLog: RequestLogContext = {
742
748
  model: pick.target.model,
@@ -744,7 +750,6 @@ export async function executeComboResponses(
744
750
  ...(logCtx.conversationId ? { conversationId: logCtx.conversationId } : {}),
745
751
  ...(logCtx.surface ? { surface: logCtx.surface } : {}),
746
752
  };
747
- const targetRoute = routeConcreteModel(config, `${pick.target.provider}/${pick.target.model}`);
748
753
  const targetReasoningEfforts = supportedLadderFor({
749
754
  provider: targetRoute.provider,
750
755
  modelId: targetRoute.modelId,
@@ -761,7 +766,7 @@ export async function executeComboResponses(
761
766
  initialJevDecision ? initialJevDecision.effort : comboDefaultEffort(config, comboId),
762
767
  initialJevDecision?.effort === null ? [] : targetReasoningEfforts,
763
768
  combo.reasoningEffortMode,
764
- initialJevDecision !== undefined && initialJevDecision.effort !== null ? "force" : combo.defaultEffortMode,
769
+ initialJevDecision ? (initialJevDecision.effort === null ? "fallback" : "force") : combo.defaultEffortMode,
765
770
  );
766
771
  if (initialJevDecision) {
767
772
  delete childBody.service_tier;
@@ -857,10 +862,11 @@ export async function executeComboResponses(
857
862
  && targetEligible(target)
858
863
  && !isComboTargetInCooldown(comboId, target),
859
864
  );
860
- const nativeChild = protocolLanes?.nativeChild(pick.target, targetRoute, targetSendBudget);
865
+ const nativeChild = protocolLanes?.nativeChild(pick.target, targetRoute, targetSendBudget, initialJevDecision);
861
866
  response = nativeChild ? await dispatchNativeComboChild({
862
867
  source: options.protocolSource!,
863
868
  plan: nativeChild,
869
+ comboDispatchPermit: hopDecision?.allowed ? hopDecision.permit : undefined,
864
870
  logCtx,
865
871
  childLog,
866
872
  attempt,
@@ -886,6 +892,8 @@ export async function executeComboResponses(
886
892
  // parent arrived with.
887
893
  sendBudget: targetSendBudget,
888
894
  comboAttempt: true,
895
+ comboInitialSend: initialSend,
896
+ comboDispatchPermit: hopDecision?.allowed ? hopDecision.permit : undefined,
889
897
  comboReplaySnapshot,
890
898
  deferCodexResetDerivedCooldown,
891
899
  // Attempt-relative TTFT is recorded HERE (not via childLog.firstOutputMs — a later
@@ -904,8 +912,8 @@ export async function executeComboResponses(
904
912
  onNativePassthroughCancel: callbackGate.onCancel,
905
913
  onResponseComplete: callbackGate.onResponseComplete,
906
914
  });
907
- // Native Chat rebuilds its own body and does not apply the initial JEV effort.
908
- restoreOriginalRequestedEffort(childLog, nativeChild ? undefined : initialJevDecision?.effort);
915
+ // Both lanes applied the initial choice before provider pins/caps; retain those transitions.
916
+ restoreOriginalRequestedEffort(childLog, initialJevDecision?.effort);
909
917
  } catch (error) {
910
918
  callbackGate.discard();
911
919
  if (options.abortSignal?.aborted) {
@@ -1180,6 +1188,11 @@ export async function executeComboResponses(
1180
1188
  // Waiting or recovery may have observed cancellation after the check above.
1181
1189
  if (options.abortSignal?.aborted) return clientCancelledResponse();
1182
1190
  }
1191
+ } finally {
1192
+ // External receipts make release a no-op after real work. runTurn owns its asynchronous
1193
+ // producer; core transfers cleanup there before returning a streaming response.
1194
+ if (!initialSend?.producerOwned) initialSend?.permit.release();
1195
+ }
1183
1196
  }
1184
1197
  const failure = exhaustedFailure();
1185
1198
  if (
@@ -1,4 +1,5 @@
1
1
  import type { TranslatorBudget } from "../../lib/translator-budget";
2
+ import { finalizeTranslatorBudgetResponse } from "../../lib/translator-budget";
2
3
  import {
3
4
  isNativePassthroughSseResponse,
4
5
  markNativePassthroughSseResponse,
@@ -37,42 +38,8 @@ export const UPSTREAM_JSON_BODY_READ_OPTIONS = {
37
38
 
38
39
 
39
40
 
40
- export function finalizeOwnedTranslatorBudget(response: Response, budget: TranslatorBudget): Response {
41
- if (!response.body) {
42
- budget.dispose();
43
- return response;
44
- }
45
- const reader = response.body.getReader();
46
- let finalized = false;
47
- const finalize = () => {
48
- if (finalized) return;
49
- finalized = true;
50
- budget.dispose();
51
- };
52
- const body = new ReadableStream<Uint8Array>({
53
- async pull(controller) {
54
- try {
55
- const result = await reader.read();
56
- if (result.done) {
57
- finalize();
58
- controller.close();
59
- } else {
60
- controller.enqueue(result.value);
61
- }
62
- } catch (error) {
63
- finalize();
64
- controller.error(error);
65
- }
66
- },
67
- async cancel(reason) {
68
- try { await reader.cancel(reason); } finally { finalize(); }
69
- },
70
- });
71
- const finalizedResponse = new Response(body, {
72
- status: response.status,
73
- statusText: response.statusText,
74
- headers: response.headers,
75
- });
41
+ export function finalizeOwnedTranslatorBudget(response: Response, budget: TranslatorBudget, signal?: AbortSignal): Response {
42
+ const finalizedResponse = finalizeTranslatorBudgetResponse(response, budget, signal);
76
43
  if (isNativePassthroughSseResponse(response)) {
77
44
  markNativePassthroughSseResponse(finalizedResponse);
78
45
  }
@@ -210,6 +210,7 @@ export async function applyFinalRouteRequestNormalization(args: {
210
210
  if (preserveAnthropicResponseModel) parsed._responseModelId = responseModelId;
211
211
  logCtx.model = virtualModel?.selectedModelId ?? route.modelId;
212
212
  logCtx.provider = route.providerName;
213
+ logCtx.spendPoolId = route.providerName;
213
214
  logCtx.providerAdapter = route.provider.adapter;
214
215
  logCtx.routeDecision = route.routeDecision;
215
216
  logCtx.policyEligibility = route.policyEligibility;