@bitkyc08/opencodex 2.86.0 → 2.87.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (378) hide show
  1. package/gui/dist/assets/App-ZfYbLHkB.js +58 -0
  2. package/gui/dist/assets/App-wT3mXqFz.css +1 -0
  3. package/gui/dist/assets/{Tray-DyR9yBee.js → Tray-DCd-A1az.js} +1 -1
  4. package/gui/dist/assets/index-CfYDMj34.js +94 -0
  5. package/gui/dist/assets/index-DPeB9HpV.css +1 -0
  6. package/gui/dist/assets/quota-summary-ChvmKGOk.js +1 -0
  7. package/gui/dist/index.html +2 -2
  8. package/package.json +1 -1
  9. package/src/adapters/anthropic/beta-allowlist.ts +9 -8
  10. package/src/adapters/anthropic/beta-body-pairs.ts +93 -0
  11. package/src/adapters/anthropic/passthrough.ts +7 -23
  12. package/src/adapters/anthropic-progress.ts +84 -0
  13. package/src/adapters/anthropic.ts +108 -21
  14. package/src/adapters/base.ts +2 -0
  15. package/src/adapters/claude-cli/adapter.ts +111 -23
  16. package/src/adapters/claude-cli/profiles.ts +2 -1
  17. package/src/adapters/claude-cli/stable-replay.ts +108 -0
  18. package/src/adapters/claude-cli/usage-admission.ts +230 -0
  19. package/src/adapters/coding-agent/protocol.ts +1 -1
  20. package/src/adapters/coding-agent/turn.ts +18 -2
  21. package/src/adapters/dsh-account.ts +97 -0
  22. package/src/adapters/google.ts +35 -8
  23. package/src/adapters/openai-chat/messages.ts +4 -3
  24. package/src/adapters/openai-chat/passthrough.ts +8 -2
  25. package/src/adapters/openai-chat/reasoning-replay.ts +52 -0
  26. package/src/adapters/openai-responses/compaction-search-history.ts +14 -7
  27. package/src/adapters/openai-responses/passthrough.ts +57 -125
  28. package/src/adapters/openai-responses/request-strips.ts +25 -3
  29. package/src/adapters/openai-responses/transport.ts +81 -0
  30. package/src/adapters/qoder/adapter.ts +4 -2
  31. package/src/adapters/registry.ts +5 -0
  32. package/src/adapters/zed.ts +26 -1
  33. package/src/advisor/activity.ts +55 -0
  34. package/src/advisor/budget.ts +62 -0
  35. package/src/advisor/consult.ts +4 -1
  36. package/src/advisor/context.ts +16 -4
  37. package/src/advisor/evidence-tool.ts +21 -0
  38. package/src/advisor/evidence.ts +117 -0
  39. package/src/advisor/runtime.ts +79 -31
  40. package/src/advisor/settings.ts +11 -2
  41. package/src/bridge/errors.ts +19 -1
  42. package/src/bridge/internal.ts +3 -1
  43. package/src/bridge/response-json.ts +18 -3
  44. package/src/bridge/sse.ts +58 -8
  45. package/src/bridge/terminal-refusal.ts +59 -0
  46. package/src/claude/desktop-3p-library.ts +32 -7
  47. package/src/claude/desktop-3p.ts +55 -13
  48. package/src/claude/desktop-first-party.ts +1 -1
  49. package/src/claude/desktop-picker-platform.ts +4 -0
  50. package/src/claude/desktop-picker.ts +15 -8
  51. package/src/claude/desktop-system-proxy.ts +50 -4
  52. package/src/claude/inbound-content-options.ts +11 -0
  53. package/src/claude/inbound.ts +33 -0
  54. package/src/claude/intercept/picker-ca-startup.ts +2 -1
  55. package/src/claude/intercept/picker-runtime.ts +6 -5
  56. package/src/claude/intercept/picker-trust-windows.ts +338 -0
  57. package/src/claude/intercept/picker-trust.ts +19 -0
  58. package/src/claude/intercept/port-policy.ts +30 -0
  59. package/src/claude/intercept/runtime.ts +97 -94
  60. package/src/claude/outbound.ts +38 -13
  61. package/src/cli/account-api.ts +1 -0
  62. package/src/cli/account-policy-dto.ts +6 -0
  63. package/src/cli/account-policy.ts +34 -5
  64. package/src/cli/account-preservation.ts +44 -0
  65. package/src/cli/account.ts +8 -2
  66. package/src/cli/advisor.ts +17 -3
  67. package/src/cli/capabilities-accounts.ts +20 -1
  68. package/src/cli/capabilities-agents-routing.ts +5 -2
  69. package/src/cli/capabilities-base.ts +38 -10
  70. package/src/cli/capabilities-observe-system.ts +63 -3
  71. package/src/cli/capabilities-provider-models.ts +11 -0
  72. package/src/cli/codex-cli-update.ts +151 -41
  73. package/src/cli/codex-provider.ts +44 -0
  74. package/src/cli/combo-input.ts +10 -1
  75. package/src/cli/combo-result.ts +1 -1
  76. package/src/cli/combo.ts +2 -1
  77. package/src/cli/cross-home-owner.ts +17 -14
  78. package/src/cli/dispatch.ts +25 -4
  79. package/src/cli/doctor.ts +6 -1
  80. package/src/cli/ensure-desktop-cold-start.ts +60 -7
  81. package/src/cli/help.ts +3 -2
  82. package/src/cli/index.ts +1 -1
  83. package/src/cli/integration-plan-dto.ts +1 -0
  84. package/src/cli/log-history.ts +172 -0
  85. package/src/cli/models-runtime-subcommands.ts +1 -0
  86. package/src/cli/models-runtime.ts +72 -8
  87. package/src/cli/observe-stream.ts +2 -0
  88. package/src/cli/observe.ts +16 -4
  89. package/src/cli/provider.ts +27 -5
  90. package/src/cli/registry.ts +28 -8
  91. package/src/cli/runtime-api.ts +5 -3
  92. package/src/cli/system-command.ts +40 -8
  93. package/src/cli/uninstall-client-state.ts +32 -0
  94. package/src/cli/usage-compare.ts +309 -0
  95. package/src/cli/usage-model-search.ts +4 -4
  96. package/src/cli/usage-report.ts +2 -2
  97. package/src/cli/v2.ts +1 -1
  98. package/src/client/machine-listener.ts +1 -3
  99. package/src/client/runtime.ts +1 -0
  100. package/src/clients/client-path.ts +51 -0
  101. package/src/clients/config-export/contracts.ts +2 -1
  102. package/src/clients/config-export.ts +18 -44
  103. package/src/clients/gentle-shell-path.ts +87 -0
  104. package/src/codex/account-auto-switch.ts +2 -0
  105. package/src/codex/account-identity.ts +29 -0
  106. package/src/codex/account-label.ts +1 -1
  107. package/src/codex/account-lifecycle.ts +21 -5
  108. package/src/codex/account-preservation.ts +28 -0
  109. package/src/codex/account-use-remaining.ts +23 -0
  110. package/src/codex/app-server-processes.ts +97 -7
  111. package/src/codex/app-server-restart-service.ts +55 -4
  112. package/src/codex/auth-api/account-list.ts +4 -0
  113. package/src/codex/auth-api/account-pause-group.ts +6 -50
  114. package/src/codex/auth-api/account-preservation.ts +57 -0
  115. package/src/codex/auth-api/routes.ts +8 -0
  116. package/src/codex/auth-api/use-remaining-route.ts +36 -0
  117. package/src/codex/auth-api.ts +0 -1
  118. package/src/codex/auth-context.ts +2 -2
  119. package/src/codex/catalog/auto-review.ts +20 -3
  120. package/src/codex/catalog/build-entries.ts +9 -1
  121. package/src/codex/catalog/combo-member.ts +14 -5
  122. package/src/codex/catalog/configured-tiers.ts +1 -1
  123. package/src/codex/catalog/gather-capture.ts +2 -0
  124. package/src/codex/catalog/metadata.ts +11 -9
  125. package/src/codex/catalog/model-hints.ts +21 -16
  126. package/src/codex/catalog/model-visibility.ts +3 -1
  127. package/src/codex/catalog/parsing.ts +30 -86
  128. package/src/codex/catalog/provider-models.ts +36 -34
  129. package/src/codex/catalog/routed-gather.ts +12 -13
  130. package/src/codex/cli-install-provenance.ts +105 -17
  131. package/src/codex/cli-update-plan.ts +545 -0
  132. package/src/codex/config-write-lock.ts +23 -2
  133. package/src/codex/desktop-app/picker-freshness.ts +96 -0
  134. package/src/codex/desktop-app-restart.ts +21 -0
  135. package/src/codex/desktop-compatibility/installed-build.ts +24 -6
  136. package/src/codex/desktop-compatibility/routing-preflight.ts +1 -1
  137. package/src/codex/desktop-compatibility/runtime.ts +12 -3
  138. package/src/codex/desktop-compatibility/windows-key-protection.ts +115 -9
  139. package/src/codex/features.ts +85 -45
  140. package/src/codex/inject/history-failure.ts +27 -0
  141. package/src/codex/inject/restore.ts +24 -2
  142. package/src/codex/inject/routing-classify.ts +4 -3
  143. package/src/codex/inject.ts +18 -27
  144. package/src/codex/main-account-hard-lock.ts +30 -1
  145. package/src/codex/native-profile-startup.ts +1 -1
  146. package/src/codex/ocx-compaction-history.ts +12 -10
  147. package/src/codex/paths.ts +86 -1
  148. package/src/codex/provider-routing.ts +174 -0
  149. package/src/codex/refresh.ts +2 -2
  150. package/src/codex/routing/health-store.ts +1 -1
  151. package/src/codex/routing/selection.ts +4 -3
  152. package/src/codex/routing/thread-affinity.ts +1 -1
  153. package/src/codex/signed-in-identity.ts +43 -0
  154. package/src/codex/subagent-model-fallback.ts +1 -1
  155. package/src/codex/sync.ts +6 -1
  156. package/src/codex/user-identity.ts +16 -5
  157. package/src/codex/windows-local-appdata.ts +84 -0
  158. package/src/combos/failover.ts +6 -1
  159. package/src/combos/index.ts +1 -0
  160. package/src/combos/resolve.ts +46 -10
  161. package/src/combos/types.ts +15 -0
  162. package/src/config/diagnostics.ts +33 -6
  163. package/src/config/live-reconcile.ts +38 -2
  164. package/src/config/load-degrade.ts +17 -0
  165. package/src/config/opencode-wire-upgrade.ts +54 -0
  166. package/src/config/opencode-zen-upgrade.ts +52 -0
  167. package/src/config/owner-registry.ts +22 -17
  168. package/src/config/provider-validation.ts +45 -2
  169. package/src/config/schema/config-schema.ts +22 -0
  170. package/src/config/schema/leaf-validators.ts +38 -9
  171. package/src/config/schema/main-account-hard-lock-fallback.ts +26 -0
  172. package/src/config/schema/native-reasoning-retention.ts +26 -0
  173. package/src/config.ts +2 -0
  174. package/src/generated/compatibility-version.json +608 -284
  175. package/src/grok/sync.ts +1 -1
  176. package/src/images/loop.ts +46 -8
  177. package/src/integrations/catalog-refresh.ts +1 -1
  178. package/src/integrations/mutation-plan.ts +1 -0
  179. package/src/integrations/registry.ts +8 -0
  180. package/src/lib/codex-restart-contract.ts +15 -0
  181. package/src/lib/desktop-upstream-tunnel.ts +18 -53
  182. package/src/lib/empty-tool-catalog.ts +62 -0
  183. package/src/lib/errors.ts +19 -3
  184. package/src/lib/gcp-adc.ts +96 -9
  185. package/src/lib/gcp-credential-input.ts +12 -0
  186. package/src/lib/lab-live-route-production.ts +5 -0
  187. package/src/lib/opaque-proxy-tunnel.ts +217 -0
  188. package/src/lib/redact.ts +2 -2
  189. package/src/oauth/account-quota-rank.ts +69 -2
  190. package/src/oauth/anthropic-account-refusal.ts +20 -2
  191. package/src/oauth/anthropic-oauth-definitions.ts +3 -1
  192. package/src/oauth/anthropic-routing.ts +1 -21
  193. package/src/oauth/anthropic.ts +38 -7
  194. package/src/oauth/callback-server.ts +27 -1
  195. package/src/oauth/chatgpt-device.ts +1 -1
  196. package/src/oauth/chatgpt-shared.ts +83 -0
  197. package/src/oauth/chatgpt.ts +8 -72
  198. package/src/oauth/dsh.ts +203 -0
  199. package/src/oauth/generic-account-failover.ts +33 -7
  200. package/src/oauth/generic-session-affinity.ts +125 -0
  201. package/src/oauth/index.ts +184 -109
  202. package/src/oauth/local-token-detect.ts +21 -1
  203. package/src/oauth/login-cli-errors.ts +44 -0
  204. package/src/oauth/login-cli.ts +69 -7
  205. package/src/oauth/login-flow-state.ts +371 -23
  206. package/src/oauth/meta-muse-errors.ts +41 -0
  207. package/src/oauth/meta-muse-keychain.ts +52 -0
  208. package/src/oauth/meta-muse.ts +51 -84
  209. package/src/oauth/model-discovery-auth.ts +3 -0
  210. package/src/oauth/pool-settings-capability.ts +4 -2
  211. package/src/oauth/refresh-flight-retirement.ts +48 -0
  212. package/src/oauth/session-affinity-key.ts +23 -0
  213. package/src/oauth/store-anthropic-instance.ts +9 -2
  214. package/src/oauth/store.ts +12 -5
  215. package/src/protocols/encoders/adapter-events.ts +5 -4
  216. package/src/protocols/encoders/messages.ts +9 -6
  217. package/src/protocols/provider-summary.ts +2 -1
  218. package/src/providers/api-key-cooldowns.ts +49 -0
  219. package/src/providers/api-key-resolve.ts +11 -6
  220. package/src/providers/api-key-response-ownership.ts +57 -0
  221. package/src/providers/api-key-selection.ts +31 -6
  222. package/src/providers/api-keys.ts +214 -3
  223. package/src/providers/claude-cli-usage.ts +96 -0
  224. package/src/providers/dsh-account-destination.ts +31 -0
  225. package/src/providers/dsh-account-models.ts +41 -0
  226. package/src/providers/gcp-credential-cleanup.ts +59 -0
  227. package/src/providers/key-failover.ts +54 -50
  228. package/src/providers/key-store.ts +7 -2
  229. package/src/providers/model-rename-fields.ts +3 -0
  230. package/src/providers/model-rename-migration.ts +2 -2
  231. package/src/providers/openai-tiers-destination.ts +11 -2
  232. package/src/providers/openai-tiers.ts +5 -12
  233. package/src/providers/opencode-go-transport.ts +3 -1
  234. package/src/providers/quota/account-cache.ts +6 -5
  235. package/src/providers/quota/account-row.ts +34 -0
  236. package/src/providers/quota/antigravity.ts +28 -7
  237. package/src/providers/quota/dsh-account.ts +70 -0
  238. package/src/providers/quota/vendor-probes-oauth.ts +1 -93
  239. package/src/providers/quota-types.ts +2 -0
  240. package/src/providers/quota.ts +14 -20
  241. package/src/providers/registry/entries-core.ts +37 -8
  242. package/src/providers/registry/types.ts +1 -1
  243. package/src/providers/request-pacing.ts +77 -19
  244. package/src/providers/routed-config.ts +339 -0
  245. package/src/providers/service-tier.ts +2 -1
  246. package/src/quota/window-mapping.ts +5 -2
  247. package/src/responses/anthropic-progress.ts +34 -0
  248. package/src/responses/assistant-replay.ts +35 -0
  249. package/src/responses/compaction-history-framing.ts +65 -0
  250. package/src/responses/compaction.ts +6 -1
  251. package/src/responses/incomplete-reason.ts +31 -0
  252. package/src/responses/parser.ts +50 -5
  253. package/src/responses/reasoning-envelope.ts +4 -0
  254. package/src/responses/reasoning-replay-cache.ts +24 -2
  255. package/src/responses/reasoning-retention.ts +173 -0
  256. package/src/responses/retained-compaction.ts +46 -0
  257. package/src/responses/schema.ts +23 -0
  258. package/src/router.ts +8 -321
  259. package/src/routing/health.ts +37 -22
  260. package/src/routing/history/indexer.ts +6 -5
  261. package/src/routing/history/ingest.ts +6 -2
  262. package/src/routing/profile.ts +1 -1
  263. package/src/server/admission-model-scope.ts +17 -0
  264. package/src/server/auth-cors.ts +11 -0
  265. package/src/server/chat-completions.ts +7 -1
  266. package/src/server/chat-native-reasoning.ts +123 -0
  267. package/src/server/chat-native-sse.ts +110 -7
  268. package/src/server/chat-native.ts +76 -17
  269. package/src/server/claude-messages.ts +55 -414
  270. package/src/server/effort-policy.ts +1 -1
  271. package/src/server/images.ts +15 -2
  272. package/src/server/index/model-alias-id.ts +36 -0
  273. package/src/server/index/optional-listeners.ts +2 -0
  274. package/src/server/index/serve-options.ts +9 -1
  275. package/src/server/index/startup-warnings.ts +2 -0
  276. package/src/server/index.ts +1 -1
  277. package/src/server/inference/advisor-diagnostics.ts +31 -0
  278. package/src/server/management/advisor-routes.ts +15 -4
  279. package/src/server/management/claude-desktop-picker-routes.ts +1 -1
  280. package/src/server/management/combo-routes.ts +176 -50
  281. package/src/server/management/companion-routes.ts +1 -1
  282. package/src/server/management/config-routes.ts +75 -4
  283. package/src/server/management/cursor-integration-routes.ts +1 -1
  284. package/src/server/management/model-routes.ts +11 -6
  285. package/src/server/management/model-rows.ts +14 -0
  286. package/src/server/management/native-integration-routes.ts +2 -7
  287. package/src/server/management/native-reasoning-retention-routes.ts +41 -0
  288. package/src/server/management/oauth-account-routes.ts +87 -14
  289. package/src/server/management/provider-gcp-credential-routes.ts +29 -0
  290. package/src/server/management/provider-key-pacing-routes.ts +31 -0
  291. package/src/server/management/provider-overwrite-carry.ts +7 -0
  292. package/src/server/management/provider-routes.ts +25 -30
  293. package/src/server/management/route-registry.ts +10 -1
  294. package/src/server/management-api.ts +3 -1
  295. package/src/server/messages-native-tool-guard.ts +185 -0
  296. package/src/server/messages-native.ts +101 -21
  297. package/src/server/messages-shared.ts +431 -0
  298. package/src/server/relay.ts +1 -1
  299. package/src/server/request-log-terminal-status.ts +1 -1
  300. package/src/server/request-log.ts +6 -1
  301. package/src/server/responses/account-change-state.ts +8 -2
  302. package/src/server/responses/adapter-continuation.ts +13 -1
  303. package/src/server/responses/adapter-delivery.ts +19 -6
  304. package/src/server/responses/adapter-dispatch.ts +19 -2
  305. package/src/server/responses/advisor-retention.ts +117 -0
  306. package/src/server/responses/advisor-slot.ts +73 -41
  307. package/src/server/responses/codex-ws-wire.ts +15 -8
  308. package/src/server/responses/compact.ts +303 -125
  309. package/src/server/responses/compaction-recovery.ts +18 -10
  310. package/src/server/responses/compaction-retention.ts +61 -0
  311. package/src/server/responses/context-overflow.ts +11 -6
  312. package/src/server/responses/core-auth.ts +6 -0
  313. package/src/server/responses/core-codex-account.ts +11 -1
  314. package/src/server/responses/core-combo-failure.ts +2 -0
  315. package/src/server/responses/core-combo.ts +120 -36
  316. package/src/server/responses/core-errors.ts +3 -3
  317. package/src/server/responses/core-lifetime.ts +5 -2
  318. package/src/server/responses/core-opaque-recovery.ts +35 -0
  319. package/src/server/responses/core-options.ts +5 -0
  320. package/src/server/responses/core-pipeline.ts +141 -0
  321. package/src/server/responses/core-replay.ts +18 -2
  322. package/src/server/responses/core.ts +2 -115
  323. package/src/server/responses/fetch-helpers.ts +73 -19
  324. package/src/server/responses/main-hard-lock-routing.ts +109 -0
  325. package/src/server/responses/native-response-control.ts +4 -2
  326. package/src/server/responses/passthrough-delivery.ts +3 -1
  327. package/src/server/responses/passthrough-dispatch.ts +15 -13
  328. package/src/server/responses/request-prepare.ts +76 -12
  329. package/src/server/responses/request-sidecar-auth.ts +2 -2
  330. package/src/server/responses/request-transport.ts +104 -27
  331. package/src/server/responses/response-effects.ts +1 -0
  332. package/src/server/responses/run-turn-execution.ts +96 -30
  333. package/src/server/responses/sidecar-execution.ts +20 -2
  334. package/src/server/responses/terminal-guard.ts +2 -16
  335. package/src/service/auth-preflight.ts +71 -0
  336. package/src/service/cli.ts +4 -2
  337. package/src/service/definition.ts +97 -0
  338. package/src/service/guards.ts +1 -61
  339. package/src/service/health-sentinel-linux.ts +112 -0
  340. package/src/service/health-sentinel-main.ts +76 -57
  341. package/src/service/health.ts +5 -89
  342. package/src/service/launchd.ts +27 -7
  343. package/src/service/logging.ts +72 -9
  344. package/src/service/repair.ts +2 -1
  345. package/src/service/state.ts +7 -8
  346. package/src/service/systemd.ts +1 -1
  347. package/src/service/windows-guarded-manager.ts +2 -1
  348. package/src/service/windows-ops.ts +1 -1
  349. package/src/service/windows-scheduler.ts +2 -7
  350. package/src/service/windows-taskxml.ts +2 -2
  351. package/src/service/windows-wscript.ts +14 -0
  352. package/src/service.ts +4 -2
  353. package/src/sidecar/auth.ts +2 -0
  354. package/src/types/config.ts +64 -3
  355. package/src/types/opencode-go.ts +61 -0
  356. package/src/types/opencode-zen.ts +29 -0
  357. package/src/types/provider.ts +32 -6
  358. package/src/types/request.ts +23 -2
  359. package/src/types/wire.ts +29 -37
  360. package/src/types.ts +1 -0
  361. package/src/update/npm-invocation.mjs +12 -2
  362. package/src/usage/cost.ts +48 -12
  363. package/src/usage/expected-prices.ts +3 -0
  364. package/src/usage/ledger-retention.ts +2 -1
  365. package/src/usage/ledger-scanner.ts +6 -5
  366. package/src/usage/log.ts +17 -8
  367. package/src/usage/summary.ts +22 -9
  368. package/src/usage/user-cost-overlays.ts +14 -19
  369. package/src/usage/user-cost-validation.ts +38 -0
  370. package/src/vision/eligibility.ts +13 -0
  371. package/src/web-search/loop.ts +55 -5
  372. package/src/web-search/progress-stream.ts +4 -1
  373. package/src/web-search/run-turn-loop.ts +9 -1
  374. package/gui/dist/assets/App-BjlttVLy.js +0 -58
  375. package/gui/dist/assets/App-CSJvmjNs.css +0 -1
  376. package/gui/dist/assets/index-BB6uK6tU.css +0 -1
  377. package/gui/dist/assets/index-BMssP4k3.js +0 -94
  378. package/gui/dist/assets/quota-summary-D8M1bc_R.js +0 -1
@@ -27,10 +27,10 @@ import {
27
27
  anthropicRoutingFor,
28
28
  AnthropicAccountCooldownError,
29
29
  formatAnthropicProviderForLog,
30
- anthropicSessionKeyFromParts,
31
30
  } from "../../oauth/anthropic-routing";
32
31
  import {
33
32
  OAuthAccountPausedError,
33
+ OAuthTokenRefreshBusyError,
34
34
  getValidAccessSnapshotForAccount,
35
35
  forceRefreshOAuthAccessSnapshot,
36
36
  getValidAccessTokenSnapshot,
@@ -49,6 +49,9 @@ import {
49
49
  refusalAwareInitialKiroAccount,
50
50
  noteGenericPoolSelection,
51
51
  } from "../../oauth/generic-account-failover";
52
+ import { admitGenericSessionAccount, bindGenericSessionAccount, genericSessionAccount, genericSessionAccountEligible, genericSessionAffinityEnabled, preferredGenericSessionAccount } from "../../oauth/generic-session-affinity";
53
+ import { oauthSessionKeyFromParts } from "../../oauth/session-affinity-key";
54
+ import { resolveContextPrincipal } from "../auth-cors";
52
55
  import { tryKiroAlternateAfterTerminalRefresh } from "../../oauth/kiro-terminal-failover";
53
56
  import { classifyModelFamilyForQuota } from "../../oauth/account-quota-rank";
54
57
  import { expandInferenceOAuthSendBudget } from "../inference/context";
@@ -78,6 +81,7 @@ import { bindRouteReasoningReplayScope } from "./core-replay";
78
81
  import { sessionIdHeaderFromRequest, normalizeLogConversationId } from "../request-log-conversation";
79
82
  import { redactSecretString } from "../../lib/redact";
80
83
  import { selectProactiveApiKeyTransport } from "../../providers/key-failover";
84
+ import { distributedKeyPhysicalFetch } from "../../providers/api-key-response-ownership";
81
85
  import { isCanonicalOpenAiForwardProvider } from "../../providers/openai-tiers";
82
86
  import { providerConsumesCallerAuthorization } from "../../providers/caller-authorization";
83
87
  import { releaseCodexAuthContextProbeLease, stripCodexRuntimeProviderFields } from "../../codex/auth-context";
@@ -120,6 +124,7 @@ export async function prepareResponsesTransport(
120
124
  ) {
121
125
  const { config, logCtx, options, req } = requestContext;
122
126
  const { route, parsed, inboundWire, translatorBudget } = requestState;
127
+ const genericAffinityMode = genericSessionAffinityEnabled(config, route.providerName);
123
128
  const anthropicInstance = route.provider.authMode === "oauth"
124
129
  ? configuredAnthropicInstance(config, route.providerName) : undefined;
125
130
  // Refuse custom/orphan B OAuth before any generic resolver can observe its auth row.
@@ -176,6 +181,7 @@ export async function prepareResponsesTransport(
176
181
  || route.providerName === "orcarouter-oauth"
177
182
  // runTurn transport: the replay runs on the first-event preflight in run-turn-execution.
178
183
  || route.providerName === "devin"
184
+ || route.providerName === "dsh-account"
179
185
  ) && route.provider.authMode === "oauth";
180
186
  let sentOAuthSnapshot: OAuthAccessSnapshot | undefined;
181
187
  // One inference-401 replay belongs to this request, even when Auto changes exchange wire.
@@ -217,6 +223,8 @@ export async function prepareResponsesTransport(
217
223
  for (let attempt = 0; attempt < maxSelectionAttempts; attempt++) {
218
224
  requireAnthropicAdmission();
219
225
  if (candidate.provider !== route.providerName) return null;
226
+ if (genericAffinityMode && route.provider.googleMode === "cloud-code-assist" && !candidate.projectId)
227
+ throw new OAuthLoginRequiredError(route.providerName);
220
228
  if (anthropicInstance) anthropicRouteDecision = currentAnthropicRouteDecision();
221
229
  if (!oauthSelection) return null;
222
230
  const proactiveEnabled = anthropicInstance !== undefined
@@ -234,7 +242,9 @@ export async function prepareResponsesTransport(
234
242
  if (candidate.provider !== route.providerName) return null;
235
243
  if (anthropicInstance) anthropicRouteDecision = currentAnthropicRouteDecision();
236
244
  if (anthropicRouteDecision && !routeCandidates(anthropicRouting!.getEligibleAnthropicAccounts(Date.now(), route.modelId), anthropicRouteDecision).includes(candidate.accountId)) return null;
237
- const committed = await commitOAuthAccountSelection(route.providerName, candidate.accountId, {
245
+ const committed = genericAffinityMode
246
+ ? admitGenericSessionAccount(config, route.providerName, oauthSelection, candidate, route.modelId, genericAffinityMode)
247
+ : await commitOAuthAccountSelection(route.providerName, candidate.accountId, {
238
248
  expectedSelection: oauthSelection,
239
249
  expectedCredentialGeneration: candidate.generation,
240
250
  requireUsableAccount: true,
@@ -248,14 +258,18 @@ export async function prepareResponsesTransport(
248
258
  )) return null;
249
259
  oauthSelection = committed;
250
260
  servingOAuthSnapshot = candidate;
261
+ bindGenericSessionAccount(config, route.providerName, genericSessionKey, candidate.accountId, route.modelId);
251
262
  if (anthropicInstance) replayOAuthCredentialSnapshot = { accountId: candidate.accountId, generation: candidate.generation };
252
263
  forgetGenericFailoverRoster(route.providerName);
253
264
  return candidate;
254
265
  }
255
266
  // A newer manual choice wins over this request's old proposal, including A→B→A.
256
267
  // Resolve that choice, not the rejected candidate, before trying admission again.
268
+ const previousSelection = oauthSelection;
257
269
  oauthSelection = captureOAuthAccountSelection(route.providerName);
258
270
  if (!oauthSelection) return null;
271
+ const selectionChanged = oauthSelection.accountId !== previousSelection.accountId
272
+ || oauthSelection.revision !== previousSelection.revision;
259
273
  // A revision also changes on per-account policy edits. Re-evaluate the selector
260
274
  // after credential waits even without a model route, rather than reusing stale active.
261
275
  const revisedAnthropic = anthropicInstance !== undefined
@@ -263,9 +277,15 @@ export async function prepareResponsesTransport(
263
277
  const revisedAnthropicId = revisedAnthropic?.accountId;
264
278
  if (anthropicInstance !== undefined && !revisedAnthropicId) return null;
265
279
  if (revisedAnthropic) anthropicReason = revisedAnthropic.reason;
280
+ const genericAccountId = genericAffinityMode && !selectionChanged
281
+ ? (genericSessionAccountEligible(config, route.providerName, candidate.accountId, route.modelId, Date.now(), true)
282
+ ? candidate.accountId : (preferredGenericSessionAccount(config, route.providerName, genericSessionKey, route.modelId)
283
+ ?? oauthSelection.accountId))
284
+ : oauthSelection.accountId;
285
+ if (!genericAccountId && !anthropicInstance) return null;
266
286
  candidate = anthropicInstance !== undefined
267
287
  ? await anthropicRouting!.getAnthropicPoolAccessSnapshot(revisedAnthropicId ?? oauthSelection.accountId)
268
- : await getValidAccessSnapshotForAccount(route.providerName, oauthSelection.accountId, { requireUsableAccount: true });
288
+ : await getValidAccessSnapshotForAccount(route.providerName, genericAccountId!, { requireUsableAccount: true });
269
289
  if (route.provider.googleMode === "cloud-code-assist" && !candidate.projectId) return null;
270
290
  }
271
291
  return null;
@@ -275,7 +295,10 @@ export async function prepareResponsesTransport(
275
295
  // Keyed on the account, not the selection revision: a selection that moved away and back
276
296
  // (A -> B -> A) before the 401 landed still serves the rejected credential, and skipping
277
297
  // the refresh would replay it and spend the one recovery attempt.
278
- const candidate = current?.accountId === sent.accountId ? await forceRefreshOAuthAccessSnapshot(sent) : sent;
298
+ const candidate = (current?.accountId === sent.accountId
299
+ || (genericAffinityMode && current?.revision === oauthSelection?.revision
300
+ && current?.accountId === oauthSelection?.accountId && servingOAuthSnapshot?.accountId === sent.accountId))
301
+ ? await forceRefreshOAuthAccessSnapshot(sent) : sent;
279
302
  const admitted = await commitResolvedOAuthSelection(candidate);
280
303
  if (!admitted) throw new Error("OAuth selection changed during credential recovery");
281
304
  if (kiroLoadEnabled && options.accountLoad?.lease?.accountId !== admitted.accountId) {
@@ -519,6 +542,8 @@ export async function prepareResponsesTransport(
519
542
  return selected?.accountId === binding.selection.accountId && selected?.revision === binding.selection.revision
520
543
  && !!row && !row.paused && !row.needsReauth && row.credential.expires > Date.now()
521
544
  && (anthropicInstance === undefined || !anthropicRouting!.getAnthropicAccountHealthSnapshot(binding.snapshot.accountId) && !anthropicRatePolicy!.anthropicRatePauseUntil(binding.snapshot.accountId) && !anthropicQuota!.anthropicFamilyRejected(binding.snapshot.accountId, route.modelId))
545
+ && (!genericAffinityMode
546
+ || genericSessionAccountEligible(config, route.providerName, binding.snapshot.accountId, route.modelId, Date.now(), true))
522
547
  && credentialGeneration(row.credential) === binding.snapshot.generation;
523
548
  };
524
549
  /** Bind the negotiated wire adapter and its built requests to the serving credential's authority. */
@@ -592,12 +617,23 @@ export async function prepareResponsesTransport(
592
617
  const accountId = await anthropicRouting!.resolveAnthropicDispatchAccountId(config, anthropicSessionKey, anthropicRouteDecision, route.modelId);
593
618
  candidate = await anthropicRouting!.getAnthropicPoolAccessSnapshot(accountId);
594
619
  }
620
+ if (genericAffinityMode
621
+ && (!candidate || !genericSessionAccountEligible(config, route.providerName, candidate.accountId, route.modelId, Date.now(), true))) {
622
+ const accountId = preferredGenericSessionAccount(config, route.providerName, genericSessionKey, route.modelId)
623
+ ?? captureOAuthAccountSelection(route.providerName)?.accountId;
624
+ candidate = accountId ? await getValidAccessSnapshotForAccount(route.providerName, accountId, { requireUsableAccount: true }) : undefined;
625
+ }
595
626
  if (!candidate || !await applyFailoverSnapshot(candidate, requestParsed, true)) {
596
627
  if (anthropicInstance !== undefined) await anthropicRouting!.resolveAnthropicDispatchAccountId(config, anthropicSessionKey, anthropicRouteDecision, route.modelId);
597
628
  throw new Error("OAuth account selection changed before dispatch");
598
629
  }
599
630
  } else {
600
- const current = resolveCurrentProviderApiKeyTransport(config, route.providerName, route.provider);
631
+ let current = resolveCurrentProviderApiKeyTransport(config, route.providerName, route.provider);
632
+ if (!current && (route.provider._apiKeyAttempt?.distributed || (route.provider.requestPacing?.enabled
633
+ && (route.provider.requestPacing.keys || route.provider.requestPacing.keySelection === "round-robin")))) {
634
+ const fresh = resolveCurrentProviderApiKeyTransport(config, route.providerName, { ...route.provider, _apiKeyAttempt: undefined });
635
+ if (fresh) current = selectProactiveApiKeyTransport(config, route.providerName, fresh) ?? fresh;
636
+ }
601
637
  if (!current) throw new Error("API key selection is unavailable before dispatch");
602
638
  route.provider = current;
603
639
  await resolveCopilotSelection(requestParsed, true);
@@ -625,7 +661,10 @@ export async function prepareResponsesTransport(
625
661
  ? (options.sendBudget as import("../../lib/request-execution-budget").RequestExecutionBudget).beginSpendProducer?.() : undefined;
626
662
  try {
627
663
  for (let attempt = 0; attempt < 3; attempt++) {
628
- if (!selectionIsCurrent(adapterBindings.get(selectedAdapter))) selectedAdapter = await refreshRunTurnAdapter(requestParsed);
664
+ incoming.abortSignal?.throwIfAborted();
665
+ const selectionRefreshed = !selectionIsCurrent(adapterBindings.get(selectedAdapter));
666
+ if (attempt === 0 && selectionRefreshed) releaseProviderRequestSlot(incoming.pacingSlot);
667
+ if (selectionRefreshed) selectedAdapter = await refreshRunTurnAdapter(requestParsed);
629
668
  const binding = adapterBindings.get(selectedAdapter);
630
669
  const run = rawRunTurns.get(selectedAdapter);
631
670
  if (!run) throw new Error("Selected provider no longer supports this turn transport");
@@ -635,24 +674,39 @@ export async function prepareResponsesTransport(
635
674
  // Both main and image-loop callers already acquired the initial pacing slot.
636
675
  // Subsequent physical messages retain this adapter/credential and are paced normally.
637
676
  try {
638
- attemptSlot = attempt === 0
639
- ? incoming.pacingSlot ?? await waitForProviderRequestSlot(route.providerName, route.provider, route.modelId, incoming.abortSignal)
677
+ // A pre-acquired lease belongs to the original selection, never its replacement.
678
+ attemptSlot = attempt === 0 && !selectionRefreshed && incoming.pacingSlot
679
+ ? incoming.pacingSlot
640
680
  : await waitForProviderRequestSlot(route.providerName, route.provider, route.modelId, incoming.abortSignal);
681
+ incoming.abortSignal?.throwIfAborted();
682
+ // Every admission can queue, including a replacement's. Never enter an
683
+ // adapter on a lease whose credential changed while it waited.
684
+ if (!selectionIsCurrent(binding)) {
685
+ refused = true;
686
+ throw new Error("Account selection changed before turn admission");
687
+ }
688
+ const beforePhysicalSend = (): void => {
689
+ incoming.abortSignal?.throwIfAborted();
690
+ if (sent) {
691
+ if (binding?.kind === "api-key" && binding.provider._apiKeyAttempt?.distributed
692
+ && !selectionIsCurrent(binding)) throw new Error("API key became unavailable during the turn");
693
+ return;
694
+ }
695
+ if (!selectionIsCurrent(binding)) {
696
+ refused = true;
697
+ throw new Error("Account selection changed before the first turn dispatch");
698
+ }
699
+ incoming.beforePhysicalSend?.();
700
+ commitKeyAttemptSend();
701
+ sent = true;
702
+ };
641
703
  const fetch = providerFetch(route.provider, options.codexWsRuntimeIdentity, {
642
704
  providerName: route.providerName, modelId: route.modelId, pacingSlotAcquired: true,
643
705
  pacingSlot: attemptSlot,
644
706
  turnScopedPacing: true,
645
- beforeDispatch: () => {
646
- if (sent) return;
647
- if (!selectionIsCurrent(binding)) {
648
- refused = true;
649
- throw new Error("Account selection changed before the first turn dispatch");
650
- }
651
- commitKeyAttemptSend();
652
- sent = true;
653
- },
707
+ beforeDispatch: beforePhysicalSend,
654
708
  });
655
- await run(requestParsed, { ...incoming, providerFetch: fetch }, event => { if (!refused) emit(event); });
709
+ await run(requestParsed, { ...incoming, pacingSlot: attemptSlot, providerFetch: fetch, beforePhysicalSend }, event => { if (!refused) emit(event); });
656
710
  } catch (error) {
657
711
  if (!refused) throw error;
658
712
  } finally {
@@ -661,7 +715,7 @@ export async function prepareResponsesTransport(
661
715
  if (!refused) return;
662
716
  // The adapter may map the guard's exception to an error event. Neither that
663
717
  // event nor a refused send may escape before retrying the newly selected account.
664
- selectedAdapter = await refreshRunTurnAdapter(requestParsed);
718
+ if (attempt < 2) selectedAdapter = await refreshRunTurnAdapter(requestParsed);
665
719
  }
666
720
  throw new Error("Account selection changed repeatedly before turn dispatch");
667
721
  } finally { producer?.close(); }
@@ -669,10 +723,16 @@ export async function prepareResponsesTransport(
669
723
  /** Fence the physical send, with bounded credential/session rebuilds and safe negotiated refusals. */
670
724
  const oauthDispatch = (wireRequest: AdapterRequest, requestParsed = parsed, owner: "adapter" | "passthrough" = "adapter"): ProviderFetchOptions["dispatchOverride"] => {
671
725
  if (route.provider.authMode === "forward") return undefined;
672
- return /** Dispatch with the current binding and session, rebuilding at most three times before returning a safe refusal. */ async (input, init, execute) => {
726
+ return /** Dispatch with the current binding and session, rebuilding at most three times before returning a safe refusal. */ async (input, init, execute, reacquirePacing) => {
673
727
  let destination = input;
674
728
  let dispatchInit = init;
729
+ let pacingChanged = false;
675
730
  for (let attempt = 0; attempt < 3; attempt++) {
731
+ if (pacingChanged) {
732
+ if (!reacquirePacing) throw new Error("Refreshed dispatch requires pacing readmission");
733
+ await reacquirePacing(route.provider, route.modelId);
734
+ pacingChanged = false;
735
+ }
676
736
  if (selectionIsCurrent(requestBindings.get(wireRequest)) && copilotSessionExpiresAt > Date.now() + 1000) {
677
737
  const fetchImpl = (route.provider as OcxProviderConfig & { fetch?: typeof globalThis.fetch }).fetch ?? execute;
678
738
  const binding = requestBindings.get(wireRequest);
@@ -713,7 +773,7 @@ export async function prepareResponsesTransport(
713
773
  }
714
774
  commitKeyAttemptSend();
715
775
  response = await sendWithConnectionPolicy(
716
- anthropicBillingPhysicalFetch(fetchImpl, route.providerName, route.provider),
776
+ distributedKeyPhysicalFetch(anthropicBillingPhysicalFetch(fetchImpl, route.providerName, route.provider), route.providerName, route.provider, target),
717
777
  destination,
718
778
  { ...dispatchInit, redirect: "manual" },
719
779
  { providerName: route.providerName, provider: route.provider },
@@ -775,13 +835,15 @@ export async function prepareResponsesTransport(
775
835
  sameTargetToken = transportToken;
776
836
  destination = rebuilt.url;
777
837
  dispatchInit = { ...dispatchInit, method: rebuilt.method, headers, body: rebuilt.body };
778
- // The next iteration validates synchronously and calls fetch in that same turn.
838
+ pacingChanged = route.provider.requestPacing?.enabled === true
839
+ && (!!route.provider.requestPacing.keys || route.provider.requestPacing.keySelection === "round-robin");
840
+ // A refreshed key must reacquire admission before the next synchronous send guard.
779
841
  }
780
842
  throw new Error("OAuth account selection changed repeatedly before dispatch");
781
843
  };
782
844
  };
783
- const anthropicSessionKey = anthropicInstance !== undefined && route.provider.authMode === "oauth"
784
- ? anthropicSessionKeyFromParts({
845
+ const oauthSessionKey = route.provider.authMode === "oauth"
846
+ ? oauthSessionKeyFromParts({
785
847
  sessionIdHeader: sessionIdHeaderFromRequest(req.headers),
786
848
  threadIdHeader: req.headers.get("thread-id"),
787
849
  promptCacheKey: typeof parsed.options.promptCacheKey === "string" ? parsed.options.promptCacheKey : null,
@@ -789,6 +851,9 @@ export async function prepareResponsesTransport(
789
851
  promptCacheKeyIsSharedCohort: options.promptCacheKeyIsSharedCohort === true,
790
852
  })
791
853
  : null;
854
+ const anthropicSessionKey = anthropicInstance ? oauthSessionKey : null;
855
+ const genericSessionKey = genericAffinityMode && oauthSessionKey
856
+ ? JSON.stringify([resolveContextPrincipal(req, config, options.admission) ?? "loopback", oauthSessionKey]) : null;
792
857
  if (route.provider.authMode === "oauth") {
793
858
  try {
794
859
  if (anthropicInstance !== undefined && anthropicRouting!.isAnthropicAccountPoolEnabled(config)) {
@@ -821,10 +886,17 @@ export async function prepareResponsesTransport(
821
886
  // without quota evidence keeps the resolution it has today.
822
887
  const refusalAwareId = route.providerName === "kiro" && oauthSelection?.accountId
823
888
  ? refusalAwareInitialKiroAccount(config, oauthSelection.accountId, Date.now(), route.modelId) : null;
824
- const preferredAccountId = refusalAwareId ?? (isGenericFailoverProvider(route.providerName, route.provider)
889
+ const affinityAccountId = genericSessionAccount(config, route.providerName, genericSessionKey, route.modelId);
890
+ const proposedAccountId = affinityAccountId
891
+ ?? preferredGenericSessionAccount(config, route.providerName, genericSessionKey, route.modelId)
892
+ ?? refusalAwareId ?? (isGenericFailoverProvider(route.providerName, route.provider)
825
893
  ? preferredInitialAccount(config, route.providerName, Date.now(), route.modelId)
826
894
  : null);
827
- let safetyAlternateId: string | null = refusalAwareId;
895
+ // An unchanged active choice must retain the ordinary resolver's terminal-refresh
896
+ // recovery (not the speculative-preference fallback that resolves the same dead grant).
897
+ const preferredAccountId = genericAffinityMode && proposedAccountId === oauthSelection?.accountId
898
+ ? null : proposedAccountId;
899
+ let safetyAlternateId: string | null = !affinityAccountId && preferredAccountId === refusalAwareId ? refusalAwareId : null;
828
900
  // Resolved account-scoped, NOT through failoverAccountSnapshot: that helper marks a
829
901
  // rotation site, and rotation sites must apply their credential through
830
902
  // applyFailoverSnapshot's pairing rules. This is initial resolution — the code below
@@ -926,7 +998,8 @@ export async function prepareResponsesTransport(
926
998
  // Advance the pool cursor only now that this account is actually admitted. The
927
999
  // helper returns immediately unless the kernel is on AND the strategy is
928
1000
  // round-robin, so quota and fill-first pools reach it without being touched.
929
- noteGenericPoolSelection(config, route.providerName, resolved.accountId, route.modelId);
1001
+ if (!affinityAccountId || affinityAccountId !== resolved.accountId)
1002
+ noteGenericPoolSelection(config, route.providerName, resolved.accountId, route.modelId);
930
1003
  }
931
1004
  // Anthropic is excluded from isGenericFailoverProvider -- its own pool owns affinity and
932
1005
  // a fail-closed local-cli credential rule -- so without this stamp its identity is
@@ -964,6 +1037,10 @@ export async function prepareResponsesTransport(
964
1037
  `${redactSecretString(err.message)}. Remove or reconfigure provider '${safeProviderName}' in the OpenCodex configuration.`,
965
1038
  );
966
1039
  }
1040
+ if (err instanceof OAuthTokenRefreshBusyError) {
1041
+ return options.abortSignal?.aborted || req.signal.aborted ? clientCancelledResponse()
1042
+ : formatErrorResponse(503, "server_error", err.message, { retryAfter: "1" });
1043
+ }
967
1044
  if (err instanceof OAuthAccountPausedError) {
968
1045
  return formatErrorResponse(403, "permission_error", publicOAuthAuthenticationErrorMessage(err));
969
1046
  }
@@ -89,6 +89,7 @@ export function createResponsesEffects(
89
89
  if (routedCompaction) {
90
90
  delete parsed.context.tools;
91
91
  delete parsed._webSearch;
92
+ delete parsed._anthropicServerTools;
92
93
  delete parsed.options.toolChoice;
93
94
  delete parsed.options.parallelToolCalls;
94
95
  // The compaction turn is a plain prose summary; a surviving structured-output format
@@ -30,7 +30,7 @@ import {
30
30
  rotateGenericOAuthAccountOn429,
31
31
  failoverAccountSnapshot,
32
32
  } from "../../oauth/generic-account-failover";
33
- import { OAuthAccountPausedError, publicOAuthAuthenticationErrorMessage, type OAuthAccessSnapshot } from "../../oauth/index";
33
+ import { OAuthAccountPausedError, OAuthTokenRefreshBusyError, publicOAuthAuthenticationErrorMessage, type OAuthAccessSnapshot } from "../../oauth/index";
34
34
  import { tryAlternateAfterTerminalRefresh } from "../../oauth/kiro-terminal-failover";
35
35
  import { resolveWireProtocolOverride } from "../adapter-resolve";
36
36
  import { formatErrorResponse, bridgeToResponsesSSE, buildResponseJSON } from "../../bridge";
@@ -49,12 +49,14 @@ import { rememberResponseState } from "../../responses/state";
49
49
  import { trackStreamLifetime } from "../lifecycle";
50
50
  import { awaitThoughtSignatureDurability } from "../../responses/thought-signature-replay";
51
51
  import { undeclaredToolCallMessage } from "../responses-undeclared-tool-guard";
52
+ import { emptyToolCatalogRefusal, guardEmptyToolCatalogEvents, guardEmptyToolCatalogStream } from "../../lib/empty-tool-catalog";
52
53
  // LOCAL PATCH (runturn-websearch)
53
54
  import { planWebSearch } from "../../web-search";
54
55
  import { runTurnWebSearchInitialParsed, runTurnWebSearchLoop } from "../../web-search/run-turn-loop";
55
56
  import { WEB_SEARCH_TOOL_NAME } from "../../web-search/synthetic-tool";
56
57
  import { orderDevinMessagesOutput } from "../../claude/devin-output-order";
57
58
  import { unboundPoolSpendRefusalMessage } from "../workflow-refusal";
59
+ import { claudeUsagePreflight } from "../../adapters/claude-cli/usage-admission";
58
60
 
59
61
  // LOCAL PATCH (runturn-websearch): top-level fields route binding or the
60
62
  // adapter itself may write during a turn. Iteration-local `turnParsed` objects
@@ -71,13 +73,16 @@ const RUNTURN_WS_ROUTE_STATE_KEYS = [
71
73
  "_providerContinuationOwner",
72
74
  "_providerContinuationCandidate",
73
75
  "_stripReasoningEncryptedContent",
76
+ "_stripNativeCompactionEncryptedContent",
77
+ "_nativeReasoningRetention",
78
+ "_nativeReasoningRetainedAcrossRoute",
74
79
  "_dropForeignReasoningItemIds",
75
80
  "_reasoningReplayScope",
76
81
  ] as const;
77
82
 
78
83
  /** One responsibility of the Responses request pipeline; state owners are explicit. */
79
84
  export async function executeResponsesRunTurn(
80
- requestContext: Pick<ResponsesRequestContext, "options" | "logCtx" | "config">,
85
+ requestContext: Pick<ResponsesRequestContext, "req" | "options" | "logCtx" | "config">,
81
86
  admissionState: ResponsesAdmissionState,
82
87
  requestState: Pick<
83
88
  PreparedResponsesRequest,
@@ -87,6 +92,7 @@ export async function executeResponsesRunTurn(
87
92
  | "translatorBudget"
88
93
  | "inboundWire"
89
94
  | "toolBridgeMaps"
95
+ | "clientToolCatalogRestriction"
90
96
  | "rememberKiroDeliveredFinalAnswer"
91
97
  | "responseStateOptions"
92
98
  >,
@@ -127,7 +133,7 @@ export async function executeResponsesRunTurn(
127
133
  >,
128
134
  completionPolicy: Pick<ResponsesCompletionPolicy, "emptyCompletionGuardEnabled">,
129
135
  ): Promise<Response> {
130
- const { options, logCtx, config } = requestContext;
136
+ const { req, options, logCtx, config } = requestContext;
131
137
  const {
132
138
  selectionIsCurrent,
133
139
  adapterBindings,
@@ -169,6 +175,33 @@ export async function executeResponsesRunTurn(
169
175
  const releaseSearchProbeLease = (): void => {
170
176
  sidecarState.openAiSidecar?.releaseProbeLease?.();
171
177
  };
178
+ // Publish the known subscription pause before headers or a CLI turn, preserving its reset
179
+ // across both Responses and translated Chat instead of presenting a retryable generic 502.
180
+ if (transportState.runTurnAdapter.name === "claude-cli") {
181
+ let usage: Awaited<ReturnType<typeof claudeUsagePreflight>>;
182
+ try {
183
+ usage = await claudeUsagePreflight(parsed.modelId);
184
+ } catch (error) {
185
+ // The outer cleanup does not own the sidecar's probe lease: hand it back before rethrowing.
186
+ releaseSearchProbeLease();
187
+ throw error;
188
+ }
189
+ if (usage.state === "exhausted") {
190
+ cancelResponseCompletion();
191
+ releaseSearchProbeLease();
192
+ const refusal = new Response(JSON.stringify({ error: {
193
+ type: "rate_limit_error", code: "claude_subscription_cooldown", param: null,
194
+ message: usage.message ?? "Claude subscription usage is exhausted.",
195
+ } }), {
196
+ status: 429,
197
+ headers: { "Content-Type": "application/json",
198
+ ...(usage.resetAt ? { "Retry-After": String(Math.max(1, Math.ceil((usage.resetAt - Date.now()) / 1000))) } : {}),
199
+ },
200
+ });
201
+ markResponseNonReplayable(refusal);
202
+ return refusal;
203
+ }
204
+ }
172
205
  // When Codex declared hosted web_search and a sidecar plan resolves, drive
173
206
  // the routed model through the same search interception the fetch-path loop
174
207
  // runs — injected as a function tool, calls intercepted, results appended to
@@ -186,7 +219,9 @@ export async function executeResponsesRunTurn(
186
219
  }
187
220
 
188
221
  const runTurnAbort = new AbortController();
189
- const cleanupRunTurnAbort = linkAbortSignal(runTurnAbort, options.abortSignal);
222
+ const cleanupRequestAbort = linkAbortSignal(runTurnAbort, req.signal);
223
+ const cleanupOptionAbort = linkAbortSignal(runTurnAbort, options.abortSignal);
224
+ const cleanupRunTurnAbort = (): void => { cleanupRequestAbort(); cleanupOptionAbort(); };
190
225
  const devinProducers = new Set<AbortController>();
191
226
  const orderMessagesOutput = (source: AsyncIterable<AdapterEvent>): AsyncIterable<AdapterEvent> =>
192
227
  inboundWire === "anthropic" && transportState.runTurnAdapter.name === "devin" && !routedCompaction
@@ -197,12 +232,13 @@ export async function executeResponsesRunTurn(
197
232
  const queue = createAdapterEventQueue({
198
233
  onBacklogExceeded: () => runTurnAbort.abort(),
199
234
  });
200
- const refreshRunTurnSelection = async (): Promise<void> => {
201
- if (selectionIsCurrent(adapterBindings.get(transportState.runTurnAdapter))) return;
235
+ const refreshRunTurnSelection = async (): Promise<boolean> => {
236
+ if (selectionIsCurrent(adapterBindings.get(transportState.runTurnAdapter))) return false;
202
237
  await refreshRunTurnAdapter(parsed);
203
238
  bindRouteReasoningReplayScope({ parsed, providerName: route.providerName, provider: route.provider,
204
239
  adapterName: transportState.runTurnAdapter.name, oauthCredentialSnapshot: transportState.replayOAuthCredentialSnapshot });
205
240
  sealRequestAttemptIdentity(logCtx.activeAttempt, logCtx.provider, transportState.runTurnAdapter.name, logCtx.accountLogLabel);
241
+ return true;
206
242
  };
207
243
  // Initial admission must settle before the streaming Response commits HTTP 200.
208
244
  // Let the outer Responses facade preserve the local retryable-429 contract.
@@ -256,13 +292,18 @@ export async function executeResponsesRunTurn(
256
292
  if (!pacingSlotAcquired) {
257
293
  pacingSlot = await waitForProviderRequestSlot(route.providerName, route.provider, route.modelId, runTurnAbort.signal);
258
294
  }
259
- await refreshRunTurnSelection();
295
+ const selectionChanged = await refreshRunTurnSelection();
260
296
  const selectedBinding = adapterBindings.get(transportState.runTurnAdapter);
261
297
  if (lastKeyFailure && selectedBinding?.kind === "api-key"
262
298
  && failedKeys.has(selectedBinding.provider.apiKey ?? "")) {
263
299
  emit(lastKeyFailure);
264
300
  return;
265
301
  }
302
+ if (selectionChanged) {
303
+ releaseProviderRequestSlot(pacingSlot);
304
+ pacingSlot = undefined;
305
+ pacingSlot = await waitForProviderRequestSlot(route.providerName, route.provider, route.modelId, runTurnAbort.signal);
306
+ }
266
307
  // LOCAL PATCH (runturn-websearch): refreshRunTurnSelection binds route
267
308
  // state onto `parsed`; mirror it onto the iteration-local copy the
268
309
  // adapter actually receives.
@@ -436,7 +477,19 @@ export async function executeResponsesRunTurn(
436
477
  // selected; the turn then moves to a surviving stored account, or, with none, the client
437
478
  // gets the login instruction instead of an opaque upstream 401.
438
479
  let oauth401ReplayAttempted = false;
439
- let pausedAuthRecovery = false;
480
+ let authRecoveryRefusal: Response | undefined;
481
+ const refuseLocalAuthRecovery = (error: Extract<AdapterEvent, { type: "error" }>, cause: unknown): boolean => {
482
+ const cancelled = runTurnAbort.signal.aborted;
483
+ if (!cancelled && !(cause instanceof OAuthTokenRefreshBusyError) && !(cause instanceof OAuthAccountPausedError)) return false;
484
+ const status = cancelled ? 499 : cause instanceof OAuthTokenRefreshBusyError ? 503 : 403;
485
+ const errorType = cancelled ? "client_cancelled" : status === 503 ? "server_error" : "permission_error";
486
+ const message = cancelled ? "Client cancelled request" : publicOAuthAuthenticationErrorMessage(cause);
487
+ // If a readiness heartbeat already committed SSE, the mapped event remains retryable
488
+ // capacity rather than retaining the rejected credential's authentication error/code.
489
+ Object.assign(error, { status, errorType, message, code: undefined, retryable: status === 503 });
490
+ authRecoveryRefusal = formatErrorResponse(status, errorType, message, status === 503 ? { retryAfter: "1" } : undefined);
491
+ return true;
492
+ };
440
493
  const recoverRunTurnAdapterOnPreflight401 = async (
441
494
  error: Extract<AdapterEvent, { type: "error" }>,
442
495
  ): Promise<boolean> => {
@@ -450,9 +503,7 @@ export async function executeResponsesRunTurn(
450
503
  try {
451
504
  admitted = await applyFailoverSnapshot(await refreshResolvedOAuthSelection(sent));
452
505
  } catch (err) {
453
- if (err instanceof OAuthAccountPausedError) {
454
- pausedAuthRecovery = true;
455
- Object.assign(error, { status: 403, errorType: "permission_error", message: publicOAuthAuthenticationErrorMessage(err) });
506
+ if (refuseLocalAuthRecovery(error, err)) {
456
507
  hop.permit?.release();
457
508
  return false;
458
509
  }
@@ -475,7 +526,9 @@ export async function executeResponsesRunTurn(
475
526
  } catch (err) {
476
527
  // Applying the alternate can still fail, e.g. when a newer manual selection points back
477
528
  // at the flagged account; the client then needs the login instruction, not the raw 401.
478
- Object.assign(error, { errorType: "authentication_error", message: publicOAuthAuthenticationErrorMessage(err) });
529
+ if (!refuseLocalAuthRecovery(error, err)) {
530
+ Object.assign(error, { errorType: "authentication_error", message: publicOAuthAuthenticationErrorMessage(err) });
531
+ }
479
532
  hop.permit?.release();
480
533
  return false;
481
534
  }
@@ -699,14 +752,19 @@ export async function executeResponsesRunTurn(
699
752
  return orderMessagesOutput(retryQueue.stream());
700
753
  };
701
754
 
702
- const { toolNsMap, declaredToolNames, toolParameterSchemas, freeformToolNames, bareCustomToolNames, toolSearchToolNames } = toolBridgeMaps;
755
+ const clientToolCatalog = requestState.clientToolCatalogRestriction && route.provider.authMode !== "forward"
756
+ ? requestState.clientToolCatalogRestriction : undefined;
757
+ const { toolNsMap, declaredToolNames, toolParameterSchemas, freeformToolNames, bareCustomToolNames, toolSearchToolNames } = clientToolCatalog
758
+ ? { ...toolBridgeMaps, ...clientToolCatalog } : toolBridgeMaps;
703
759
  const enforceDeclaredToolNames = inboundWire !== "chat" && inboundWire !== "anthropic";
704
760
  const classifyUndeclaredFirstTool = (
705
761
  event: AdapterEvent,
706
762
  ): Extract<AdapterEvent, { type: "error" }> | undefined => {
707
- if (!enforceDeclaredToolNames || event.type !== "tool_call_start") return undefined;
708
763
  // This tool is declared to the adapter by the private search loop.
709
- if (wsPlan && event.name === WEB_SEARCH_TOOL_NAME) return undefined;
764
+ if (wsPlan && event.type === "tool_call_start" && event.name === WEB_SEARCH_TOOL_NAME) return undefined;
765
+ const emptyCatalogRefusal = emptyToolCatalogRefusal(event, clientToolCatalog);
766
+ if (emptyCatalogRefusal) return emptyCatalogRefusal;
767
+ if (!enforceDeclaredToolNames || event.type !== "tool_call_start") return undefined;
710
768
  const effectiveName = normalizeDeclaredToolName(event.name, declaredToolNames, undefined, bareCustomToolNames);
711
769
  if (declaredToolNames.has(effectiveName)) return undefined;
712
770
  return {
@@ -752,13 +810,13 @@ export async function executeResponsesRunTurn(
752
810
  // Preflight holds only heartbeats and the first meaningful event. A first-event 429 can be
753
811
  // replayed transparently; after any output reaches the bridge, a later error stays terminal.
754
812
  eventSource = await preflightRunTurnFailover(eventSource, wsFirstParsed, preflightDeadlineAt);
755
- if (pausedAuthRecovery) {
813
+ if (authRecoveryRefusal) {
756
814
  cancelResponseCompletion();
757
815
  runTurnAbort.abort();
758
816
  queue.close();
759
817
  cleanupRunTurnAbort();
760
818
  releaseSearchProbeLease();
761
- return formatErrorResponse(403, "permission_error", publicOAuthAuthenticationErrorMessage(new OAuthAccountPausedError()));
819
+ return authRecoveryRefusal;
762
820
  }
763
821
  }
764
822
  if (grokDevinPreflight) {
@@ -776,9 +834,13 @@ export async function executeResponsesRunTurn(
776
834
  if (preflight.error || preflight.empty) {
777
835
  runTurnAbort.abort();
778
836
  queue.close();
779
- const message = preflight.error?.message ?? "Adapter ended before producing a response";
780
- const failure = formatErrorResponse(preflight.error?.code === SPEND_LEDGER_STORAGE_UNAVAILABLE_CODE ? 503 : 502,
781
- preflight.error?.code === SPEND_LEDGER_STORAGE_UNAVAILABLE_CODE ? SPEND_LEDGER_STORAGE_UNAVAILABLE_CODE : "upstream_error", redactSecretString(message));
837
+ const classified = adapterFailureFromEvent(preflight.error ?? {
838
+ type: "error", status: 502, message: "Adapter ended before producing a response",
839
+ });
840
+ const storageRefusal = preflight.error?.code === SPEND_LEDGER_STORAGE_UNAVAILABLE_CODE;
841
+ const failure = formatErrorResponse(storageRefusal ? 503 : classified.httpStatus,
842
+ storageRefusal ? SPEND_LEDGER_STORAGE_UNAVAILABLE_CODE : classified.error.type,
843
+ redactSecretString(classified.error.message), { code: classified.error.code, classified: true });
782
844
  // A replay-unsafe heartbeat means the adapter already ran a local side effect, so the
783
845
  // combo must not send this turn to another target: the failure stays with this child.
784
846
  if (preflight.replayUnsafe) markResponseNonReplayable(failure);
@@ -824,7 +886,7 @@ export async function executeResponsesRunTurn(
824
886
  console.warn(emptyCompletionNotice(route.providerName, route.modelId));
825
887
  });
826
888
  const sseStream = bridgeToResponsesSSE(
827
- guardedSource, parsed._responseModelId ?? parsed.modelId, toolNsMap, freeformToolNames, toolSearchToolNames,
889
+ guardEmptyToolCatalogStream(guardedSource, clientToolCatalog, translatorBudget), parsed._responseModelId ?? parsed.modelId, toolNsMap, freeformToolNames, toolSearchToolNames,
828
890
  () => {
829
891
  cancelResponseCompletion();
830
892
  runTurnAbort.abort();
@@ -840,9 +902,11 @@ export async function executeResponsesRunTurn(
840
902
  declaredToolNames,
841
903
  bareCustomToolNames,
842
904
  enforceDeclaredToolNames,
905
+ // Same wire split: only a Responses client (Codex) needs the refusal as a failure.
906
+ terminalRefusalAsFailed: enforceDeclaredToolNames,
843
907
  toolParameterSchemas,
844
908
  ...(options.onFirstOutput ? { onFirstOutput: options.onFirstOutput } : {}),
845
- ...(routedCompaction ? { compaction: true } : {}),
909
+ ...(routedCompaction ? { compaction: true, compactionRetention: parsed._compactionRetention } : {}),
846
910
  // grok-build's strict decoder dies on the typed response.heartbeat frame; its
847
911
  // eventsource layer tolerates comment keep-alives. Codex needs the opposite.
848
912
  ...(logCtx.surface === "grok" ? { heartbeatStyle: "comment" as const } : {}),
@@ -897,13 +961,13 @@ export async function executeResponsesRunTurn(
897
961
  for await (const event of await preflightRunTurnFailover(
898
962
  (async function* () { yield* firstAttemptEvents; })(),
899
963
  )) runTurnEvents.push(event);
900
- if (pausedAuthRecovery) {
964
+ if (authRecoveryRefusal) {
901
965
  cancelResponseCompletion();
902
966
  runTurnAbort.abort();
903
967
  queue.close();
904
968
  cleanupRunTurnAbort();
905
969
  releaseSearchProbeLease();
906
- return formatErrorResponse(403, "permission_error", publicOAuthAuthenticationErrorMessage(new OAuthAccountPausedError()));
970
+ return authRecoveryRefusal;
907
971
  }
908
972
  }
909
973
  if (grokDevinPreflight) {
@@ -961,6 +1025,7 @@ export async function executeResponsesRunTurn(
961
1025
  }
962
1026
  events = searched;
963
1027
  }
1028
+ events = guardEmptyToolCatalogEvents(events, clientToolCatalog, translatorBudget);
964
1029
  if (options.comboAttempt) {
965
1030
  const firstMeaningfulIndex = events.findIndex(event => event.type !== "heartbeat");
966
1031
  const firstMeaningful = firstMeaningfulIndex === -1 ? undefined : events[firstMeaningfulIndex];
@@ -974,12 +1039,13 @@ export async function executeResponsesRunTurn(
974
1039
  ? classifyUndeclaredFirstTool(firstMeaningful)
975
1040
  : undefined;
976
1041
  if (!firstMeaningful || firstMeaningful.type === "error" || classifiedError) {
977
- const message = classifiedError?.message ?? (firstMeaningful?.type === "error"
978
- ? firstMeaningful.message
979
- : "Adapter ended before producing a response");
1042
+ const classified = adapterFailureFromEvent(classifiedError ?? (firstMeaningful?.type === "error"
1043
+ ? firstMeaningful
1044
+ : { type: "error", status: 502, message: "Adapter ended before producing a response" }));
980
1045
  const storageRefusal = firstMeaningful?.type === "error" && firstMeaningful.code === SPEND_LEDGER_STORAGE_UNAVAILABLE_CODE;
981
- const failure = formatErrorResponse(storageRefusal ? 503 : 502,
982
- storageRefusal ? SPEND_LEDGER_STORAGE_UNAVAILABLE_CODE : "upstream_error", redactSecretString(message));
1046
+ const failure = formatErrorResponse(storageRefusal ? 503 : classified.httpStatus,
1047
+ storageRefusal ? SPEND_LEDGER_STORAGE_UNAVAILABLE_CODE : classified.error.type,
1048
+ redactSecretString(classified.error.message), { code: classified.error.code, classified: true });
983
1049
  if (replayUnsafe) markResponseNonReplayable(failure);
984
1050
  return failure;
985
1051
  }
@@ -997,7 +1063,7 @@ export async function executeResponsesRunTurn(
997
1063
  freeformToolNames,
998
1064
  bareCustomToolNames,
999
1065
  toolSearchToolNames,
1000
- ...(routedCompaction ? { compaction: true } : {}),
1066
+ ...(routedCompaction ? { compaction: true, compactionRetention: parsed._compactionRetention } : {}),
1001
1067
  onProviderState: state => { providerState = state; },
1002
1068
  onUsage: usage => {
1003
1069
  transportState.bindKeyUsageFromBridge(usage);