@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,4 @@
1
- import { rotateAnthropicAccountOnResponse } from "../../oauth/anthropic-account-refusal";
1
+ import { rotateAnthropicAccountOnResponseForInstance } from "../../oauth/anthropic-account-refusal";
2
2
  import type { ResponsesRequestContext } from "./core-options";
3
3
  import type { PreparedResponsesRequest } from "./request-prepare";
4
4
  import type { ResponsesTransport } from "./request-transport";
@@ -35,7 +35,7 @@ import { resolveWireProtocolOverride } from "../adapter-resolve";
35
35
  import { bindRouteReasoningReplayScope } from "./core-replay";
36
36
  import {
37
37
  ANTHROPIC_POOL_MAX_FAILOVERS_PER_REQUEST,
38
- getAnthropicPoolAccessSnapshot,
38
+ anthropicRoutingFor,
39
39
  formatAnthropicProviderForLog,
40
40
  } from "../../oauth/anthropic-routing";
41
41
  import {
@@ -83,6 +83,8 @@ export function createAdapterContinuations(
83
83
  | "oauthDispatch"
84
84
  | "invalidateSameTargetRequest"
85
85
  | "resolveSelectionAdapter"
86
+ | "anthropicInstance"
87
+ | "currentAnthropicRouteDecision"
86
88
  | "anthropicRouteDecision"
87
89
  | "anthropicPoolAccountId"
88
90
  | "anthropicPoolFailovers"
@@ -102,7 +104,8 @@ export function createAdapterContinuations(
102
104
  | "noteAdapterPhysicalSend"
103
105
  | "noteAdapterRecoveryWithheld"
104
106
  | "remainingTransientSendBudget"
105
- | "noteTransientSends"
107
+ | "adapterSendBudget"
108
+ | "transientSendReporter"
106
109
  | "reserveCredentialHop"
107
110
  | "pendingHopPermit"
108
111
  | "sendBudgetExhausted"
@@ -122,6 +125,8 @@ export function createAdapterContinuations(
122
125
  oauthDispatch,
123
126
  invalidateSameTargetRequest,
124
127
  resolveSelectionAdapter,
128
+ anthropicInstance,
129
+ currentAnthropicRouteDecision,
125
130
  anthropicSessionKey,
126
131
  commitResolvedOAuthSelection,
127
132
  applyFailoverSnapshot,
@@ -135,7 +140,7 @@ export function createAdapterContinuations(
135
140
  noteAdapterPhysicalSend,
136
141
  noteAdapterRecoveryWithheld,
137
142
  remainingTransientSendBudget,
138
- noteTransientSends,
143
+ transientSendReporter,
139
144
  reserveCredentialHop,
140
145
  sendBudgetExhausted,
141
146
  } = sendBudgetState;
@@ -211,6 +216,8 @@ export function createAdapterContinuations(
211
216
  try {
212
217
  if (transportState.activeAdapter.fetchResponse) {
213
218
  transportState.noteRoutedAttemptSend(continuationEstimate, replayKind);
219
+ const producer = adapterDispatchBudget?.beginSpendProducer?.();
220
+ try {
214
221
  return await withProviderRequestSlot(route.providerName, route.provider, nextParsed.modelId, upstream.signal, pacingSlot =>
215
222
  transportState.activeAdapter.fetchResponse!(builtContinuationRequest, {
216
223
  kiroPreferAccountFailover: route.providerName === "kiro" && isGenericOAuthFailoverEnabled(config, "kiro"),
@@ -228,6 +235,7 @@ export function createAdapterContinuations(
228
235
  modelId: nextParsed.modelId,
229
236
  }),
230
237
  }));
238
+ } finally { producer?.close(); }
231
239
  }
232
240
  // Same #1851 scope guard as the initial send: transient-5xx retry only for direct
233
241
  // Google AI Studio; every other adapter keeps reset-only semantics here.
@@ -237,6 +245,9 @@ export function createAdapterContinuations(
237
245
  : fetchWithResetRetry;
238
246
  return await fetchContinuationWithRetryPolicy(
239
247
  recovery => {
248
+ sendBudgetState.adapterSendBudget?.startRequest?.({
249
+ poolId: logCtx.spendPoolId ?? route.providerName, identityId: logCtx.accountLogLabel,
250
+ });
240
251
  transportState.noteRoutedAttemptSend(continuationEstimate, recovery ?? replayKind);
241
252
  return fetchWithHeaderTimeout(
242
253
  builtContinuationRequest.url,
@@ -261,10 +272,10 @@ export function createAdapterContinuations(
261
272
  // Same request-scoped budget as the initial send and the 429/rotation refetches:
262
273
  // a terminal-guard continuation is another leg of ONE request, so handing it a
263
274
  // fresh `attempts` would let one request exceed the configured total-send ceiling.
264
- ...(continuationTransientPolicy
275
+ ...(continuationTransientPolicy || sendBudgetState.adapterSendBudget?.spendEnforced
265
276
  ? {
266
- attempts: remainingTransientSendBudget(continuationTransientPolicy.attempts),
267
- onSendsConsumed: noteTransientSends,
277
+ attempts: remainingTransientSendBudget(continuationTransientPolicy?.attempts ?? 1),
278
+ onSendsConsumed: transientSendReporter(),
268
279
  }
269
280
  : {}),
270
281
  },
@@ -390,33 +401,34 @@ export function createAdapterContinuations(
390
401
  }
391
402
  }
392
403
  if (
393
- (response.status === 429 || response.status === 403)
404
+ (response.status === 429 || response.status === 403 || response.status === 401)
405
+ && anthropicInstance
394
406
  && transportState.anthropicPoolAccountId
395
407
  && !isNonReplayableResponse(response)
396
408
  ) {
397
- const nextAccountId = await rotateAnthropicAccountOnResponse(response, {
409
+ const nextAccountId = await rotateAnthropicAccountOnResponseForInstance(anthropicInstance, response, {
398
410
  config, accountId: transportState.anthropicPoolAccountId, sessionKey: anthropicSessionKey,
399
- model: route.modelId, requestKey: transportState, decision: transportState.anthropicRouteDecision, signal: upstream.signal,
411
+ model: route.modelId, requestKey: transportState, decision: transportState.anthropicRouteDecision, currentDecision: currentAnthropicRouteDecision, signal: upstream.signal,
400
412
  canRetry: !sendBudgetExhausted() && transportState.anthropicPoolFailovers < ANTHROPIC_POOL_MAX_FAILOVERS_PER_REQUEST,
401
413
  allowAccountRefusal, allow429Recovery: allowAccountRefusal,
402
414
  });
403
415
  if (nextAccountId) {
404
416
  try {
405
- const admitted = await commitResolvedOAuthSelection(await getAnthropicPoolAccessSnapshot(nextAccountId));
417
+ const admitted = await commitResolvedOAuthSelection(await anthropicRoutingFor(anthropicInstance).getAnthropicPoolAccessSnapshot(nextAccountId));
406
418
  if (!admitted) throw new Error("OAuth selection changed during recovery");
407
419
  try { void response.body?.cancel().catch(() => {}); } catch { /* already consumed/closed */ }
408
420
  transportState.anthropicPoolAccountId = admitted.accountId;
409
421
  transportState.anthropicPoolFailovers += 1;
410
422
  route.provider = { ...route.provider, apiKey: admitted.accessToken };
411
423
  invalidateSameTargetRequest();
412
- logCtx.provider = formatAnthropicProviderForLog("anthropic", admitted.accountId, config);
424
+ logCtx.provider = formatAnthropicProviderForLog(anthropicInstance, admitted.accountId, config);
413
425
  transportState.activeAdapter = resolveSelectionAdapter(
414
426
  resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire, route.staticPolicy),
415
427
  config.cacheRetention,
416
428
  );
417
429
  sealRequestAttemptIdentity(logCtx.activeAttempt, logCtx.provider, transportState.activeAdapter.name, logCtx.accountLogLabel);
418
430
  recordAttemptCredentialSource(logCtx.activeAttempt, route.providerName, route.provider, transportState.activeAdapter.name);
419
- nextContinuationRecoveryKind = "anthropic-oauth-429";
431
+ nextContinuationRecoveryKind = response.status === 401 ? "oauth-401" : "anthropic-oauth-429";
420
432
  continue;
421
433
  } catch {
422
434
  // fall through to emit continuation error below
@@ -513,9 +525,9 @@ export function createAdapterContinuations(
513
525
  recordAttemptCredentialSource(logCtx.activeAttempt, route.providerName, route.provider, transportState.activeAdapter.name);
514
526
  // The replay goes out on the next iteration. An adapter that owns its ladder
515
527
  // reserves for that send itself, so hand this reservation down rather than let it
516
- // take a second one for the same replay. A helper-routed replay needs no handoff:
517
- // its reporter settles the booking made above.
518
- if (adapterOwnsDispatch) sendBudgetState.pendingHopPermit = hop.permit;
528
+ // take a second one for the same replay. A helper-routed replay carries the
529
+ // same permit so its reporter settles only this booking.
530
+ sendBudgetState.pendingHopPermit = hop.permit;
519
531
  nextContinuationRecoveryKind = "oauth-account-429";
520
532
  kiroRefusalPendingReplay = response;
521
533
  continue;
@@ -604,9 +616,9 @@ export function createAdapterContinuations(
604
616
  recordAttemptCredentialSource(logCtx.activeAttempt, route.providerName, route.provider, transportState.activeAdapter.name);
605
617
  // The replay goes out on the next iteration. An adapter that owns its ladder
606
618
  // reserves for that send itself, so hand this reservation down rather than let it
607
- // take a second one for the same replay. A helper-routed replay needs no handoff:
608
- // its reporter settles the booking made above.
609
- if (adapterOwnsDispatch) sendBudgetState.pendingHopPermit = hop.permit;
619
+ // take a second one for the same replay. A helper-routed replay carries the
620
+ // same permit so its reporter settles only this booking.
621
+ sendBudgetState.pendingHopPermit = hop.permit;
610
622
  nextContinuationRecoveryKind = "oauth-account-429";
611
623
  continue;
612
624
  }
@@ -1,5 +1,5 @@
1
1
  import { classifyAnthropic429 } from "../../oauth/anthropic-rate-limit-policy";
2
- import { rotateAnthropicAccountOnResponse } from "../../oauth/anthropic-account-refusal";
2
+ import { rotateAnthropicAccountOnResponseForInstance } from "../../oauth/anthropic-account-refusal";
3
3
  import { authorizeResendForRecovery } from "../../lib/request-resend-gate";
4
4
  import { ambiguousResendAllowanceFor, selfContainedResponsesBody } from "./reset-replay";
5
5
  import { transientSendCapFor } from "./request-send-budget";
@@ -65,7 +65,7 @@ import { bindRouteReasoningReplayScope } from "./core-replay";
65
65
  import {
66
66
  AnthropicAccountCooldownError,
67
67
  ANTHROPIC_POOL_MAX_FAILOVERS_PER_REQUEST,
68
- getAnthropicPoolAccessSnapshot,
68
+ anthropicRoutingFor,
69
69
  formatAnthropicProviderForLog,
70
70
  } from "../../oauth/anthropic-routing";
71
71
  import {
@@ -106,6 +106,7 @@ import { isCanonicalOpenAiForwardProvider } from "../../providers/openai-tiers";
106
106
  import { mapCodexAuthContextErrorToResponse } from "./codex-auth-error";
107
107
  import { chargeWorkflowSends } from "../../lib/workflow-budget";
108
108
  import { isAntigravityValidationRefusal } from "./antigravity-validation-refusal";
109
+ import { unboundPoolSpendRefusalResponse } from "../workflow-refusal";
109
110
 
110
111
  /** One responsibility of the Responses request pipeline; state owners are explicit. */
111
112
  export async function prepareAdapterExchange(
@@ -139,6 +140,8 @@ export async function prepareAdapterExchange(
139
140
  | "replayOAuthCredentialSnapshot"
140
141
  | "invalidateSameTargetRequest"
141
142
  | "resolveSelectionAdapter"
143
+ | "anthropicInstance"
144
+ | "currentAnthropicRouteDecision"
142
145
  | "anthropicRouteDecision"
143
146
  | "anthropicPoolAccountId"
144
147
  | "anthropicPoolFailovers"
@@ -157,7 +160,8 @@ export async function prepareAdapterExchange(
157
160
  | "noteAdapterPhysicalSend"
158
161
  | "noteAdapterRecoveryWithheld"
159
162
  | "remainingTransientSendBudget"
160
- | "noteTransientSends"
163
+ | "adapterSendBudget"
164
+ | "transientSendReporter"
161
165
  | "recoverySendAllowance"
162
166
  | "recoveryClassFor"
163
167
  | "sendBudgetExhausted"
@@ -166,6 +170,9 @@ export async function prepareAdapterExchange(
166
170
  | "workflowRootId"
167
171
  | "claimAmbiguousResend"
168
172
  | "sendsUsed"
173
+ | "targetSendsUsed"
174
+ | "initialSendAllowance"
175
+ | "noteInitialDispatch"
169
176
  >,
170
177
  ) {
171
178
  const { options, config, logCtx, req } = requestContext;
@@ -175,6 +182,8 @@ export async function prepareAdapterExchange(
175
182
  refreshResolvedOAuthSelection,
176
183
  invalidateSameTargetRequest,
177
184
  resolveSelectionAdapter,
185
+ anthropicInstance,
186
+ currentAnthropicRouteDecision,
178
187
  anthropicSessionKey,
179
188
  commitResolvedOAuthSelection,
180
189
  applyFailoverSnapshot,
@@ -194,7 +203,7 @@ export async function prepareAdapterExchange(
194
203
  noteAdapterPhysicalSend,
195
204
  noteAdapterRecoveryWithheld,
196
205
  remainingTransientSendBudget,
197
- noteTransientSends,
206
+ transientSendReporter,
198
207
  recoverySendAllowance,
199
208
  recoveryClassFor,
200
209
  sendBudgetExhausted,
@@ -333,6 +342,8 @@ export async function prepareAdapterExchange(
333
342
  try {
334
343
  if (transportState.activeAdapter.fetchResponse) {
335
344
  transportState.noteRoutedAttemptSend(inputTokenEstimate);
345
+ const producer = adapterDispatchBudget?.beginSpendProducer?.();
346
+ try {
336
347
  upstreamResponse = await withProviderRequestSlot(route.providerName, route.provider, route.modelId, upstream.signal, pacingSlot =>
337
348
  transportState.activeAdapter.fetchResponse!(builtInitialRequest, {
338
349
  kiroPreferAccountFailover: route.providerName === "kiro" && isGenericOAuthFailoverEnabled(config, "kiro"),
@@ -352,6 +363,7 @@ export async function prepareAdapterExchange(
352
363
  ? createCodexAuthDispatchGuard(admissionState.authCtx, options.codexAuthPolicy ?? config, route.modelId, options.admission, options.visionDescribeTerminal === true) : undefined,
353
364
  }),
354
365
  }));
366
+ } finally { producer?.close(); }
355
367
  } else {
356
368
  // #1851 scope guard: transient-5xx retry on this generic adapter path is opt-in for
357
369
  // direct Google AI Studio only (Vertex/Antigravity use fetchResponse above). Other
@@ -361,22 +373,37 @@ export async function prepareAdapterExchange(
361
373
  // legacy direct-Google exception is preserved exactly; every other adapter still keeps
362
374
  // reset-only semantics so combo failover hops on the first 5xx.
363
375
  const transientPolicy = transientRetryPolicyFor(route.provider);
376
+ const resetPolicy = resetReplayPolicyFor(route.provider);
364
377
  const compactPrepaid = options.compactionRecoveryAttempted ? sendBudgetState.pendingHopPermit : undefined;
365
378
  if (compactPrepaid) sendBudgetState.pendingHopPermit = undefined;
366
379
  let compactPrepaidUsed = false;
380
+ // A combo-owned or compaction-prepaid send is settled by the physical-dispatch receipt
381
+ // when spend enforcement is off. Enforced spend and direct requests keep the shared
382
+ // physical-send reporter, which already owns the permit lifecycle there.
383
+ // Stable per request: a Combo or compaction permit comes from `reserveDispatch`, which starts
384
+ // (and freezes) the spend policy first, so `spendEnforced` cannot change before dispatch.
385
+ const receiptMode = !sendBudgetState.adapterSendBudget?.spendEnforced
386
+ && Boolean(options.comboInitialSend || compactPrepaid);
367
387
  // Combo and emergency compaction admission already book this target's first send.
368
388
  // Its configured initial ceiling is target-local; the shared remainder below still
369
389
  // accounts for earlier targets without deducting their sends from this target twice.
370
- const initialSendCap = transientPolicy || resetReplayPolicyFor(route.provider)
390
+ const initialSendCap = transientPolicy || resetPolicy
391
+ || (options.comboAttempt && route.provider.adapter === "google")
371
392
  ? transientSendCapFor(transientPolicy?.attempts,
372
393
  (options.comboAttempt || compactPrepaid) ? 0 : sendBudgetState.sendsUsed)
373
394
  : 1;
374
395
  const fetchWithRetryPolicy = (route.provider.adapter === "google" || transientPolicy)
375
396
  ? fetchWithTransientRetry
376
397
  : fetchWithResetRetry;
377
- upstreamResponse = await fetchWithRetryPolicy(
398
+ try { upstreamResponse = await fetchWithRetryPolicy(
378
399
  recovery => {
379
- if (compactPrepaid && !compactPrepaidUsed) {
400
+ // Unconfigured generic sends may omit the counting reporter. Capture policy
401
+ // independently so a later recovery still belongs to this request's start.
402
+ sendBudgetState.adapterSendBudget?.startRequest?.({
403
+ poolId: logCtx.spendPoolId ?? route.providerName, identityId: logCtx.accountLogLabel,
404
+ });
405
+ // Receipt mode books the physical send at executor admission instead of here.
406
+ if (!receiptMode && compactPrepaid && !compactPrepaidUsed) {
380
407
  if (!compactPrepaid.use()) throw new SendBudgetExhaustedError(safeHostLabel(builtInitialRequest.url));
381
408
  compactPrepaidUsed = true;
382
409
  }
@@ -390,6 +417,14 @@ export async function prepareAdapterExchange(
390
417
  dispatchOverride: oauthDispatch(builtInitialRequest),
391
418
  providerName: route.providerName,
392
419
  modelId: route.modelId,
420
+ onPhysicalDispatch: () => {
421
+ // Preserve the unconfigured direct-adapter accounting contract; Combo admission
422
+ // and explicit retry/compaction budgets require a receipt for their booked send.
423
+ if (receiptMode) {
424
+ sendBudgetState.noteInitialDispatch(compactPrepaid && !compactPrepaidUsed ? compactPrepaid : undefined);
425
+ compactPrepaidUsed = true;
426
+ }
427
+ },
393
428
  beforeDispatch: isCanonicalOpenAiForwardProvider(route.provider)
394
429
  ? createCodexAuthDispatchGuard(admissionState.authCtx, options.codexAuthPolicy ?? config, route.modelId, options.admission, options.visionDescribeTerminal === true) : undefined,
395
430
  }));
@@ -398,16 +433,24 @@ export async function prepareAdapterExchange(
398
433
  abortSignal: upstream.signal,
399
434
  label: safeHostLabel(builtInitialRequest.url),
400
435
  claimAmbiguousResend: claimPreHeaderResend,
401
- ...(transientPolicy || resetReplayPolicyFor(route.provider) || compactPrepaid
436
+ ...(sendBudgetState.adapterSendBudget?.spendEnforced || options.comboDispatchPermit || transientPolicy || resetPolicy || compactPrepaid || options.comboAttempt
402
437
  ? {
403
438
  // A pending compaction permit already booked this leg's first physical send.
404
- attempts: Math.min(initialSendCap,
405
- remainingTransientSendBudget(initialSendCap) + (compactPrepaid ? 1 : 0)),
406
- onSendsConsumed: noteTransientSends,
439
+ ...(transientPolicy || resetPolicy || compactPrepaid || options.comboAttempt
440
+ ? {
441
+ attempts: Math.min(initialSendCap,
442
+ sendBudgetState.initialSendAllowance(initialSendCap) + (compactPrepaid ? 1 : 0)),
443
+ }
444
+ : {}),
445
+ // Reporter path: direct requests and enforced spend. Receipt-mode sends are
446
+ // settled by `onPhysicalDispatch`, so a reporter here would count them twice.
447
+ ...(receiptMode
448
+ ? {}
449
+ : { onSendsConsumed: transientSendReporter(compactPrepaid ?? options.comboDispatchPermit) }),
407
450
  }
408
451
  : {}),
409
452
  },
410
- );
453
+ ); } finally { compactPrepaid?.release(); }
411
454
  }
412
455
  } catch (err) {
413
456
  cleanupUpstreamAbort();
@@ -437,7 +480,8 @@ export async function prepareAdapterExchange(
437
480
  // blaming the provider makes the caller send the whole turn again -- the amplification this
438
481
  // budget exists to stop. The passthrough path has answered 429 here since #4546.
439
482
  if (err instanceof SendBudgetExhaustedError) {
440
- return formatErrorResponse(429, SEND_BUDGET_EXHAUSTED_CODE, err.message);
483
+ return unboundPoolSpendRefusalResponse(logCtx)
484
+ ?? formatErrorResponse(429, SEND_BUDGET_EXHAUSTED_CODE, err.message);
441
485
  }
442
486
  const msg = describeUpstreamConnectFailure(err, connectMs);
443
487
  return formatErrorResponse(502, "upstream_error", msg);
@@ -541,6 +585,8 @@ export async function prepareAdapterExchange(
541
585
  try {
542
586
  if (transportState.activeAdapter.fetchResponse) {
543
587
  transportState.noteRoutedAttemptSend(retryEstimate, recovery);
588
+ const producer = adapterDispatchBudget?.beginSpendProducer?.();
589
+ try {
544
590
  return await withProviderRequestSlot(route.providerName, route.provider, route.modelId, upstream.signal, pacingSlot => {
545
591
  // The dispatch boundary is HERE, not before the pacing wait: that wait can reject for
546
592
  // an abort, a saturated queue, an expired slot or a removed provider, and none of
@@ -570,6 +616,7 @@ export async function prepareAdapterExchange(
570
616
  }),
571
617
  });
572
618
  });
619
+ } finally { producer?.close(); }
573
620
  }
574
621
  // #2643 review: this leg used to call fetchWithHeaderTimeout directly, so an
575
622
  // opted-in provider's transient-5xx policy applied to the initial send and to
@@ -581,11 +628,10 @@ export async function prepareAdapterExchange(
581
628
  const refetchWithPolicy = (route.provider.adapter === "google" || refetchTransientPolicy)
582
629
  ? fetchWithTransientRetry
583
630
  : fetchWithResetRetry;
584
- const helperCountsSends = refetchTransientPolicy !== null || resetReplayPolicyFor(route.provider) !== null;
631
+ const helperCountsSends = sendBudgetState.adapterSendBudget?.spendEnforced === true || refetchTransientPolicy !== null || resetReplayPolicyFor(route.provider) !== null;
585
632
  const prepaid = sendBudgetState.pendingHopPermit;
586
633
  const configuredTotal = refetchTransientPolicy?.attempts;
587
- const refetchCap = transientSendCapFor(configuredTotal,
588
- sendBudgetState.sendsUsed - (prepaid ? 1 : 0));
634
+ const refetchCap = transientSendCapFor(configuredTotal, sendBudgetState.targetSendsUsed);
589
635
  // An exact total includes a booked hop, even when it consumed the last base slot.
590
636
  // Keep that funded send while forbidding the final reserve from widening the total.
591
637
  const prepaidLastSlot = helperCountsSends && configuredTotal !== undefined && prepaid
@@ -601,6 +647,9 @@ export async function prepareAdapterExchange(
601
647
  try {
602
648
  return await refetchWithPolicy(
603
649
  recoveryKind => {
650
+ sendBudgetState.adapterSendBudget?.startRequest?.({
651
+ poolId: logCtx.spendPoolId ?? route.providerName, identityId: logCtx.accountLogLabel,
652
+ });
604
653
  if (refetchAllowance?.permit && !refetchAllowance.permit.use()) {
605
654
  throw new SendBudgetExhaustedError(safeHostLabel(retryRequest.url));
606
655
  }
@@ -629,7 +678,7 @@ export async function prepareAdapterExchange(
629
678
  ...(refetchAllowance
630
679
  ? {
631
680
  attempts: refetchAllowance.attempts,
632
- onSendsConsumed: noteTransientSends,
681
+ onSendsConsumed: transientSendReporter(refetchAllowance.permit),
633
682
  }
634
683
  : {}),
635
684
  },
@@ -673,7 +722,8 @@ export async function prepareAdapterExchange(
673
722
  // Same rule on the recovery leg: the ladder refused to send again, so the answer names
674
723
  // this proxy rather than the provider it never reached.
675
724
  if (err instanceof SendBudgetExhaustedError) {
676
- return { failed: formatErrorResponse(429, SEND_BUDGET_EXHAUSTED_CODE, err.message) };
725
+ return { failed: unboundPoolSpendRefusalResponse(logCtx)
726
+ ?? formatErrorResponse(429, SEND_BUDGET_EXHAUSTED_CODE, err.message) };
677
727
  }
678
728
  const msg = describeUpstreamConnectFailure(err, connectMs);
679
729
  return { failed: formatErrorResponse(502, "upstream_error", msg) };
@@ -1047,31 +1097,32 @@ export async function prepareAdapterExchange(
1047
1097
  // Anthropic OAuth: recover a rate limit or proven account entitlement refusal
1048
1098
  // before output, within the shared request and account rotation limits.
1049
1099
  while (
1050
- (upstreamResponse.status === 429 || upstreamResponse.status === 403)
1100
+ (upstreamResponse.status === 429 || upstreamResponse.status === 403 || upstreamResponse.status === 401)
1101
+ && anthropicInstance
1051
1102
  && transportState.anthropicPoolAccountId
1052
1103
  ) {
1053
- const nextAccountId = await rotateAnthropicAccountOnResponse(upstreamResponse, {
1104
+ const nextAccountId = await rotateAnthropicAccountOnResponseForInstance(anthropicInstance, upstreamResponse, {
1054
1105
  config, accountId: transportState.anthropicPoolAccountId, sessionKey: anthropicSessionKey,
1055
- model: route.modelId, requestKey: transportState, decision: transportState.anthropicRouteDecision, signal: upstream.signal,
1106
+ model: route.modelId, requestKey: transportState, decision: transportState.anthropicRouteDecision, currentDecision: currentAnthropicRouteDecision, signal: upstream.signal,
1056
1107
  canRetry: !sendBudgetExhausted() && transportState.anthropicPoolFailovers < ANTHROPIC_POOL_MAX_FAILOVERS_PER_REQUEST,
1057
1108
  });
1058
1109
  if (!nextAccountId) break;
1059
1110
  try {
1060
- const admitted = await commitResolvedOAuthSelection(await getAnthropicPoolAccessSnapshot(nextAccountId));
1111
+ const admitted = await commitResolvedOAuthSelection(await anthropicRoutingFor(anthropicInstance).getAnthropicPoolAccessSnapshot(nextAccountId));
1061
1112
  if (!admitted) throw new Error("OAuth selection changed during recovery");
1062
1113
  try { void upstreamResponse.body?.cancel().catch(() => {}); } catch { /* already consumed/closed */ }
1063
1114
  transportState.anthropicPoolAccountId = admitted.accountId;
1064
1115
  transportState.anthropicPoolFailovers += 1;
1065
1116
  route.provider = { ...route.provider, apiKey: admitted.accessToken };
1066
1117
  invalidateSameTargetRequest();
1067
- logCtx.provider = formatAnthropicProviderForLog("anthropic", admitted.accountId, config);
1118
+ logCtx.provider = formatAnthropicProviderForLog(anthropicInstance, admitted.accountId, config);
1068
1119
  transportState.activeAdapter = resolveSelectionAdapter(
1069
1120
  resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire, route.staticPolicy),
1070
1121
  config.cacheRetention,
1071
1122
  );
1072
1123
  sealRequestAttemptIdentity(logCtx.activeAttempt, logCtx.provider, transportState.activeAdapter.name, logCtx.accountLogLabel);
1073
1124
  recordAttemptCredentialSource(logCtx.activeAttempt, route.providerName, route.provider, transportState.activeAdapter.name);
1074
- const result = await rebuildAndRefetch("anthropic-oauth-429");
1125
+ const result = await rebuildAndRefetch(upstreamResponse.status === 401 ? "oauth-401" : "anthropic-oauth-429");
1075
1126
  if ("failed" in result) return result.failed;
1076
1127
  upstreamResponse = result;
1077
1128
  if (isNonReplayableResponse(upstreamResponse)) continue recovery;
@@ -1,19 +1,33 @@
1
- import { ANTIGRAVITY_VALIDATION_REQUIRED_PREFIX } from "../../adapters/google-errors";
1
+ import { ANTIGRAVITY_VALIDATION_REQUIRED_PREFIX, safeAntigravityHttpErrorMessage } from "../../adapters/google-errors";
2
2
  import { readBoundedResponseBody } from "../../lib/bounded-body";
3
3
 
4
+ /** Match only the normalized Antigravity validation-refusal marker. */
5
+ export function hasAntigravityValidationRefusalMarker(text: string): boolean {
6
+ return text.startsWith(`${ANTIGRAVITY_VALIDATION_REQUIRED_PREFIX}: `);
7
+ }
8
+
4
9
  /** Inspect only the Google adapter's normalized, already bounded 403 response. */
5
10
  export async function isAntigravityValidationRefusal(response: Response, signal?: AbortSignal): Promise<boolean> {
11
+ return inspectValidationRefusal(response, signal, false);
12
+ }
13
+
14
+ /** Sidecar dispatch retains the raw Google envelope; normalize only a complete bounded body. */
15
+ export async function isAntigravityRawValidationRefusal(response: Response, signal?: AbortSignal): Promise<boolean> {
16
+ return inspectValidationRefusal(response, signal, true);
17
+ }
18
+
19
+ async function inspectValidationRefusal(response: Response, signal: AbortSignal | undefined, raw: boolean): Promise<boolean> {
6
20
  if (response.status !== 403 || signal?.aborted) return false;
7
21
  try {
8
22
  const body = await readBoundedResponseBody(response.clone(), {
9
- maxBytes: 1024,
10
- totalTimeoutMs: 1000,
11
- firstByteTimeoutMs: 1000,
12
- inactivityTimeoutMs: 1000,
23
+ maxBytes: raw ? 4096 : 1024,
24
+ totalTimeoutMs: raw ? 2000 : 1000,
25
+ firstByteTimeoutMs: raw ? 2000 : 1000,
26
+ inactivityTimeoutMs: raw ? 2000 : 1000,
13
27
  signal,
14
28
  });
15
29
  return !signal?.aborted && body.displaySafe && !body.truncated && !body.timedOut && !body.oversized
16
- && body.text.startsWith(`${ANTIGRAVITY_VALIDATION_REQUIRED_PREFIX}: `);
30
+ && hasAntigravityValidationRefusalMarker(raw ? safeAntigravityHttpErrorMessage(403, body.text) : body.text);
17
31
  } catch {
18
32
  return false;
19
33
  }
@@ -9,7 +9,7 @@ import { CODEX_RESPONSES_HTTP_URL, type PreparedCodexWsRequest } from "./codex-w
9
9
  import { CodexWsCorrelation } from "./codex-ws-correlation";
10
10
  import type { CodexWsSession } from "./codex-ws-session";
11
11
  import { UPGRADE_DEADLINE_MS, CODEX_WS_LIVENESS_PING_INTERVAL_MS, CODEX_WS_RESPONSE_PRELUDE_TIMEOUT_MS, MAX_CODEX_WS_FRAME_BYTES,
12
- MAX_CODEX_WS_QUEUE_BYTES, markCodexWsResponse, normalizeResponsesWsRelayEvent, closedBeforeTerminalMessage,
12
+ MAX_CODEX_WS_QUEUE_BYTES, markCodexWsResponse, markCodexWsPreludeProjection, normalizeResponsesWsRelayEvent, closedBeforeTerminalMessage,
13
13
  codexWsCreateFrameExceedsLimit, codexWsFailureDetail, codexWsPreResponseFailure, markCodexWsStage, codexWsOcxVersion,
14
14
  markCodexWsSocketDeath, type CodexWsFailureStage, type CodexWsStageRecord } from "./codex-ws-wire";
15
15
 
@@ -23,6 +23,7 @@ interface ExchangeOptions {
23
23
  sseFallback: typeof globalThis.fetch;
24
24
  onQuota?: CodexWsQuotaObserver;
25
25
  beforeDispatch?: (headers: Headers) => void;
26
+ onPhysicalDispatch?: () => void;
26
27
  /** Bun version string the caller gated on; stamped onto the stage record. */
27
28
  bunVersion?: string;
28
29
  }
@@ -44,7 +45,8 @@ function rejectionHeaders(source: Record<string, unknown>, prelude: Headers): He
44
45
  }
45
46
  }
46
47
  // Reuse the metadata owner's count/value/family budgets and window freshness
47
- // rules, without publishing quota twice. The unmarked HTTP response owns it.
48
+ // rules, without publishing quota twice. Pool bookkeeping consumes the HTTP
49
+ // projection; plain-main publication belongs only to the WS observer.
48
50
  const projected = new CodexWsMetadata();
49
51
  try {
50
52
  for (const values of [Object.fromEntries(prelude), source]) {
@@ -240,6 +242,7 @@ export function codexWsExchange(options: ExchangeOptions): Promise<Response> {
240
242
  // are left out: their channel may already have sent continuation frames on this socket, so
241
243
  // the create frame alone no longer describes the turn.
242
244
  if (socketDied && !nativeControl) markCodexWsSocketDeath(failureResponse, stage);
245
+ markCodexWsPreludeProjection(failureResponse);
243
246
  resolve(failureResponse);
244
247
  return;
245
248
  }
@@ -411,6 +414,17 @@ export function codexWsExchange(options: ExchangeOptions): Promise<Response> {
411
414
  failStream(error);
412
415
  return;
413
416
  }
417
+ try { options.onPhysicalDispatch?.(); } catch (error) {
418
+ // Admission refused before the frame left. This is a pre-open rejection,
419
+ // not a transport failure and not an authorization to replay over SSE.
420
+ settledPreOpen = true;
421
+ sent = false;
422
+ terminal = true;
423
+ cleanup();
424
+ session.dispose();
425
+ reject(error);
426
+ return;
427
+ }
414
428
  try {
415
429
  ws.send(frameText);
416
430
  sentAt = Date.now();
@@ -501,6 +515,7 @@ export function codexWsExchange(options: ExchangeOptions): Promise<Response> {
501
515
  cleanup();
502
516
  try { controller.close(); } catch { /* unused stream already closed */ }
503
517
  session.dispose();
518
+ markCodexWsPreludeProjection(rejection);
504
519
  resolve(rejection);
505
520
  return;
506
521
  }