@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
@@ -0,0 +1,123 @@
1
+ import type { OcxProviderConfig, OcxReasoningReplayScopeRef } from "../types";
2
+ import type { TranslatorBudget } from "../lib/translator-budget";
3
+ import { chatReasoningReplayPolicy, isChatRecord } from "../adapters/openai-chat/reasoning-replay";
4
+ import { bindReasoningReplayScope, reasoningReplayDestinationIdentity,
5
+ reasoningReplayKeyCredentialIdentity, rememberReasoningForCall } from "../responses/reasoning-replay-cache";
6
+ import { reasoningReplayConversationIdFromResponsesRequest, sessionIdHeaderFromRequest } from "./request-log-conversation";
7
+
8
+ type Rec = Record<string, unknown>;
9
+
10
+ export function nativeChatReplayScope(headers: Headers): OcxReasoningReplayScopeRef | undefined {
11
+ const clientThreadId = reasoningReplayConversationIdFromResponsesRequest({
12
+ threadIdHeader: headers.get("thread-id"), sessionIdHeader: sessionIdHeaderFromRequest(headers),
13
+ });
14
+ return clientThreadId ? { clientThreadId } : undefined;
15
+ }
16
+
17
+ /** Rebind before EVERY build: a key rotation must not retrieve another credential's reasoning. */
18
+ export function bindNativeChatReplayScope(
19
+ scope: OcxReasoningReplayScopeRef | undefined,
20
+ provider: OcxProviderConfig,
21
+ providerName: string,
22
+ modelId: string,
23
+ ): void {
24
+ const providerDestinationIdentity = reasoningReplayDestinationIdentity(provider.baseUrl);
25
+ const credentialIdentity = reasoningReplayKeyCredentialIdentity(provider);
26
+ bindReasoningReplayScope(scope, providerDestinationIdentity && credentialIdentity ? {
27
+ providerName, modelId, adapterName: "openai-chat",
28
+ providerDestinationIdentity, credentialIdentity,
29
+ } : undefined);
30
+ }
31
+
32
+ /** Observe the native wire, never translate it or retain tool arguments/content. */
33
+ export function nativeChatReasoningCapture(
34
+ provider: OcxProviderConfig,
35
+ modelId: string,
36
+ scope: OcxReasoningReplayScopeRef | undefined,
37
+ budget: TranslatorBudget,
38
+ ) {
39
+ const choices = new Map<number, { text: string; calls: Map<number, string> }>();
40
+ const accounting = { kind: "retained_collectors" as const };
41
+ // This optional fallback cannot retain more than the cache it feeds (256 KiB / 64 calls).
42
+ const maxBytes = 256 * 1024;
43
+ let bytes = 0;
44
+ let callCount = 0;
45
+ let disabled = !scope?.current || !chatReasoningReplayPolicy(provider, modelId).preserve;
46
+ const discard = () => {
47
+ budget.releaseRetained(bytes, accounting);
48
+ bytes = 0;
49
+ choices.clear();
50
+ disabled = true;
51
+ };
52
+ const remember = (text: string, calls: Iterable<string>) => {
53
+ for (const id of calls) rememberReasoningForCall(id, text, scope);
54
+ };
55
+ const observe = (value: Rec) => {
56
+ if (disabled || !Array.isArray(value.choices)) return;
57
+ for (const [ordinal, raw] of value.choices.entries()) {
58
+ if (!isChatRecord(raw) || !isChatRecord(raw.delta)) continue;
59
+ const index = typeof raw.index === "number" ? raw.index : ordinal;
60
+ if (!choices.has(index)) {
61
+ if (choices.size >= 64) { discard(); return; }
62
+ choices.set(index, { text: "", calls: new Map() });
63
+ }
64
+ const state = choices.get(index)!;
65
+ const delta = raw.delta;
66
+ const append = (previous: string, fragment: string): string | undefined => {
67
+ const added = Buffer.byteLength(fragment) * 2;
68
+ if (bytes + added > maxBytes) { discard(); return undefined; }
69
+ // Account the new retained fragment before concatenation; the transient reservation
70
+ // covers the old string while its replacement is allocated.
71
+ const reservation = budget.reserveTransient(Buffer.byteLength(previous) * 2, accounting);
72
+ try {
73
+ budget.chargeRetained(added, accounting);
74
+ bytes += added;
75
+ return previous + fragment;
76
+ } finally { reservation.release(); }
77
+ };
78
+ if (typeof delta.reasoning_content === "string" && delta.reasoning_content.length > 0) {
79
+ const text = append(state.text, delta.reasoning_content);
80
+ if (text === undefined) return;
81
+ state.text = text;
82
+ }
83
+ if (Array.isArray(delta.tool_calls)) {
84
+ for (const [toolOrdinal, tool] of delta.tool_calls.entries()) {
85
+ if (!isChatRecord(tool) || typeof tool.id !== "string" || tool.id.length === 0) continue;
86
+ const toolIndex = typeof tool.index === "number" ? tool.index : toolOrdinal;
87
+ if (!state.calls.has(toolIndex)) {
88
+ if (callCount >= 64) { discard(); return; }
89
+ callCount += 1;
90
+ }
91
+ const previous = state.calls.get(toolIndex) ?? "";
92
+ if (previous === tool.id) continue;
93
+ const id = append(previous, tool.id);
94
+ if (id === undefined) return;
95
+ state.calls.set(toolIndex, id);
96
+ }
97
+ }
98
+ // The completed choice is valid even if a caller dispatches its tool before [DONE].
99
+ if (typeof raw.finish_reason === "string" && raw.finish_reason.length > 0) {
100
+ remember(state.text, state.calls.values());
101
+ }
102
+ }
103
+ };
104
+ return {
105
+ observe,
106
+ finish(success: boolean) {
107
+ if (success && !disabled) {
108
+ for (const state of choices.values()) remember(state.text, state.calls.values());
109
+ }
110
+ discard();
111
+ },
112
+ json(value: Rec) {
113
+ if (disabled || !Array.isArray(value.choices)) return;
114
+ for (const choice of value.choices) {
115
+ if (!isChatRecord(choice) || !isChatRecord(choice.message)) continue;
116
+ const message = choice.message;
117
+ if (typeof message.reasoning_content !== "string" || !Array.isArray(message.tool_calls)) continue;
118
+ remember(message.reasoning_content, message.tool_calls.filter(isChatRecord)
119
+ .flatMap(tool => typeof tool.id === "string" ? [tool.id] : []));
120
+ }
121
+ },
122
+ };
123
+ }
@@ -1,5 +1,5 @@
1
1
  import { chatCompletionsErrorBody } from "../chat/outbound";
2
- import { classifyError, cyberPolicyErrorType, CYBER_POLICY_ERROR_CODE, isCyberPolicyCode } from "../lib/errors";
2
+ import { classifyError, cyberPolicyErrorType, CYBER_POLICY_ERROR_CODE, isCyberPolicyCode, UNDECLARED_TOOL_CALL_ERROR_CODE } from "../lib/errors";
3
3
  import { redactSecretString } from "../lib/redact";
4
4
  import {
5
5
  isTranslatorBudgetExceededError,
@@ -16,6 +16,39 @@ function isRec(value: unknown): value is Rec {
16
16
  return value !== null && typeof value === "object" && !Array.isArray(value);
17
17
  }
18
18
 
19
+ export const UNDECLARED_CHAT_TOOL_MESSAGE = "Upstream emitted a client tool call outside this request's declared tool catalog.";
20
+
21
+ function* chatToolCalls(value: Rec): Generator<{ key: string; name: string; snapshot: boolean; invalid?: boolean }> {
22
+ const choices = Array.isArray(value.choices) ? value.choices : [];
23
+ for (const [position, choice] of choices.entries()) {
24
+ if (!isRec(choice)) continue;
25
+ const choiceIndex = typeof choice.index === "number" ? choice.index : position;
26
+ for (const [payload, snapshot] of [[choice.delta, false], [choice.message, true]] as const) {
27
+ if (!isRec(payload)) continue;
28
+ const tools = Array.isArray(payload.tool_calls) ? payload.tool_calls : [];
29
+ for (const [index, tool] of tools.entries()) {
30
+ const fn = isRec(tool) && isRec(tool.function) ? tool.function : {};
31
+ const toolIndex = isRec(tool) && typeof tool.index === "number" ? tool.index : index;
32
+ yield {
33
+ key: `${choiceIndex}:tool:${toolIndex}`, name: typeof fn.name === "string" ? fn.name : "", snapshot,
34
+ invalid: !isRec(tool) || (tool.type !== undefined && tool.type !== "function"),
35
+ };
36
+ }
37
+ if (payload.function_call !== undefined && payload.function_call !== null) {
38
+ const fn = isRec(payload.function_call) ? payload.function_call : {};
39
+ yield { key: `${choiceIndex}:legacy`, name: typeof fn.name === "string" ? fn.name : "", snapshot };
40
+ }
41
+ }
42
+ }
43
+ }
44
+
45
+ export function chatResponseHasUndeclaredToolCalls(value: Rec, declared: ReadonlySet<string>): boolean {
46
+ for (const call of chatToolCalls(value)) {
47
+ if (call.invalid || !declared.has(call.name)) return true;
48
+ }
49
+ return false;
50
+ }
51
+
19
52
  export function usageFromChat(value: unknown): OcxUsage | undefined {
20
53
  if (!isRec(value)) return undefined;
21
54
  const prompt = typeof value.prompt_tokens === "number" ? value.prompt_tokens : undefined;
@@ -134,8 +167,11 @@ interface NativeChatSseOptions {
134
167
  * `stallTimeoutSec` then resolves to disabled so a slow local model is not cut mid-turn.
135
168
  */
136
169
  localUpstream?: boolean;
170
+ clientDeclaredEmptyToolCatalog?: boolean;
171
+ declaredLegacyFunctionNames?: ReadonlySet<string>;
137
172
  onFirstOutput?: () => void;
138
173
  onUsage: (usage: OcxUsage) => void;
174
+ onChunk?: (value: Rec) => void;
139
175
  onTerminal?: (status: number, message?: string) => void;
140
176
  onCancel?: () => void;
141
177
  }
@@ -154,6 +190,29 @@ export function nativeChatSse(
154
190
  }
155
191
  });
156
192
  let queuedBytes = 0;
193
+ const declared = options.declaredLegacyFunctionNames ?? new Set<string>();
194
+ const toolNames = new Map<string, string>();
195
+ const pendingToolNames = new Set<string>();
196
+ const heldFrames: string[] = [];
197
+ let heldBytes = 0;
198
+ let toolNameBytes = 0;
199
+ const hasPendingToolName = () => pendingToolNames.size > 0;
200
+ const inspectToolCalls = (value: Rec): boolean => {
201
+ for (const call of chatToolCalls(value)) {
202
+ if (call.invalid) return false;
203
+ const previous = toolNames.get(call.key);
204
+ const name = call.snapshot ? call.name : (previous ?? "") + call.name;
205
+ if (!declared.has(name) && ![...declared].some(allowed => allowed.startsWith(name))) return false;
206
+ const bytes = previous === undefined ? 128 + Buffer.byteLength(call.key) * 2 + Buffer.byteLength(name) * 2
207
+ : Math.max(0, Buffer.byteLength(name) - Buffer.byteLength(previous)) * 2;
208
+ options.translatorBudget.chargeRetained(bytes, scope);
209
+ toolNameBytes += bytes;
210
+ toolNames.set(call.key, name);
211
+ if (declared.has(name)) pendingToolNames.delete(call.key);
212
+ else pendingToolNames.add(call.key);
213
+ }
214
+ return true;
215
+ };
157
216
  let sawFinish = false;
158
217
  let sawDone = false;
159
218
  let firstOutput = false;
@@ -191,7 +250,31 @@ export function nativeChatSse(
191
250
  throw error;
192
251
  }
193
252
  };
194
- const releaseBuffer = () => buffer.clear();
253
+ const releaseHeld = () => {
254
+ options.translatorBudget.releaseRetained(heldBytes, scope);
255
+ heldBytes = 0;
256
+ heldFrames.length = 0;
257
+ };
258
+ const relay = (controller: ReadableStreamDefaultController<Uint8Array>, text: string) => {
259
+ if (hasPendingToolName()) {
260
+ const bytes = Buffer.byteLength(text) * 2;
261
+ options.translatorBudget.chargeRetained(bytes, scope);
262
+ heldBytes += bytes;
263
+ heldFrames.push(text);
264
+ return;
265
+ }
266
+ for (const frame of heldFrames) enqueue(controller, frame);
267
+ releaseHeld();
268
+ enqueue(controller, text);
269
+ };
270
+ const releaseBuffer = () => {
271
+ buffer.clear();
272
+ releaseHeld();
273
+ options.translatorBudget.releaseRetained(toolNameBytes, scope);
274
+ toolNameBytes = 0;
275
+ toolNames.clear();
276
+ pendingToolNames.clear();
277
+ };
195
278
  const settle = (status: number, message?: string) => {
196
279
  if (settled) return;
197
280
  settled = true;
@@ -206,12 +289,12 @@ export function nativeChatSse(
206
289
  type = "upstream_error",
207
290
  ) => {
208
291
  const safeMessage = redactSecretString(message);
292
+ releaseBuffer();
209
293
  try {
210
294
  const frame = `data: ${JSON.stringify(chatCompletionsErrorBody(status, safeMessage, type, code))}\n\n`;
211
295
  if (code === "translation_buffer_limit") controller.enqueue(encoder.encode(frame));
212
296
  else enqueue(controller, frame);
213
297
  } catch { /* controller may already be closed */ }
214
- releaseBuffer();
215
298
  settle(status, safeMessage);
216
299
  try { void reader.cancel(new Error(safeMessage)).catch(() => {}); } catch { /* already closed */ }
217
300
  try { controller.close(); } catch { /* already closed */ }
@@ -224,16 +307,20 @@ export function nativeChatSse(
224
307
  const payload = sseDataPayload(block);
225
308
  if (payload === null) {
226
309
  if (performance.now() >= pullDeadline) throw stalled;
227
- enqueue(controller, block + delimiter);
310
+ relay(controller, block + delimiter);
228
311
  return;
229
312
  }
230
313
  const trimmed = payload.trim();
231
314
  if (trimmed === "") {
232
315
  if (performance.now() >= pullDeadline) throw stalled;
233
- enqueue(controller, block + delimiter);
316
+ relay(controller, block + delimiter);
234
317
  return;
235
318
  }
236
319
  if (trimmed === "[DONE]") {
320
+ if (hasPendingToolName()) {
321
+ fail(controller, UNDECLARED_CHAT_TOOL_MESSAGE, UNDECLARED_TOOL_CALL_ERROR_CODE);
322
+ return;
323
+ }
237
324
  sawDone = true;
238
325
  enqueue(controller, replaceSseDataPayload(block, "[DONE]") + delimiter);
239
326
  releaseBuffer();
@@ -272,9 +359,20 @@ export function nativeChatSse(
272
359
  return;
273
360
  }
274
361
  const usage = usageFromChat(parsed.usage);
362
+ options.onChunk?.(parsed);
275
363
  if (usage) options.onUsage(usage);
364
+ if (options.clientDeclaredEmptyToolCatalog && !inspectToolCalls(parsed)) {
365
+ fail(controller, UNDECLARED_CHAT_TOOL_MESSAGE, UNDECLARED_TOOL_CALL_ERROR_CODE);
366
+ return;
367
+ }
276
368
  const choices = Array.isArray(parsed.choices) ? parsed.choices : [];
277
369
  const finished = choices.some(choice => isRec(choice) && typeof choice.finish_reason === "string" && choice.finish_reason.length > 0);
370
+ if (hasPendingToolName() && choices.some((choice, position) => isRec(choice)
371
+ && typeof choice.finish_reason === "string" && choice.finish_reason.length > 0
372
+ && [...pendingToolNames].some(key => key.startsWith(`${typeof choice.index === "number" ? choice.index : position}:`)))) {
373
+ fail(controller, UNDECLARED_CHAT_TOOL_MESSAGE, UNDECLARED_TOOL_CALL_ERROR_CODE);
374
+ return;
375
+ }
278
376
  if (finished) sawFinish = true;
279
377
  const hasText = choices.some(choice => {
280
378
  if (!isRec(choice) || !isRec(choice.delta)) return false;
@@ -302,11 +400,11 @@ export function nativeChatSse(
302
400
  const hasProgress = hasText || hasTool;
303
401
  if (hasProgress || finished) pullDeadline = performance.now() + stallMs;
304
402
  else if (performance.now() >= pullDeadline) throw stalled;
305
- if (!firstOutput && hasProgress) {
403
+ if (!firstOutput && hasProgress && !hasPendingToolName()) {
306
404
  firstOutput = true;
307
405
  options.onFirstOutput?.();
308
406
  }
309
- enqueue(controller, replaceSseDataPayload(block, JSON.stringify(normalizedChunk(parsed, options.requestedModel))) + delimiter);
407
+ relay(controller, replaceSseDataPayload(block, JSON.stringify(normalizedChunk(parsed, options.requestedModel))) + delimiter);
310
408
  };
311
409
 
312
410
  function onAbort() {
@@ -336,6 +434,7 @@ export function nativeChatSse(
336
434
  if (next) {
337
435
  buffer.compact();
338
436
  processBlock(controller, next.block, next.delimiter);
437
+ if (heldFrames.length > 0 && !settled) continue;
339
438
  return;
340
439
  }
341
440
  if (performance.now() >= pullDeadline) throw stalled;
@@ -370,6 +469,10 @@ export function nativeChatSse(
370
469
  continue;
371
470
  }
372
471
  buffer.append(decoder.decode());
472
+ if (hasPendingToolName()) {
473
+ fail(controller, UNDECLARED_CHAT_TOOL_MESSAGE, UNDECLARED_TOOL_CALL_ERROR_CODE);
474
+ return;
475
+ }
373
476
  if (buffer.tail().trim().length > 0) {
374
477
  fail(controller, "upstream SSE ended with an unterminated event", "upstream_sse_unterminated");
375
478
  return;
@@ -1,5 +1,6 @@
1
1
  import { createPhysicalSendReporter } from "../lib/request-execution-budget";
2
2
  import { createInferenceSendBudget } from "./inference/context";
3
+ import { observeAdvisorNativeReasoning } from "./inference/advisor-diagnostics";
3
4
  import { buildOpenAIChatPassthroughRequest, createOpenAIChatAdapter } from "../adapters/openai-chat";
4
5
  import type { AdapterRequest, ProviderAdapter } from "../adapters/base";
5
6
  import { isNativeChatRouteEligible } from "./chat-native-eligibility";
@@ -18,6 +19,7 @@ import {
18
19
  isCyberPolicyCode,
19
20
  isCyberPolicyMessage,
20
21
  SEND_BUDGET_EXHAUSTED_CODE,
22
+ UNDECLARED_TOOL_CALL_ERROR_CODE,
21
23
  } from "../lib/errors";
22
24
  import { reportDispatchSends, type RequestExecutionBudget, type SingleUseDispatchPermit } from "../lib/request-execution-budget";
23
25
  import type { AdmissionLease } from "../lib/admission";
@@ -54,10 +56,13 @@ import {
54
56
  rateLimitRetryDelayMs,
55
57
  rateLimitRetryPolicyFor,
56
58
  rotateProviderTransportOn429,
59
+ rotateProviderTransportOn401,
57
60
  transientRetryPolicyFor,
58
61
  } from "../providers/key-failover";
59
62
  import { fastPolicyForModel } from "../providers/service-tier";
63
+ import { RequestPacingQueueOverloadError } from "../providers/request-pacing";
60
64
  import { stampApiKeyAccountLabel } from "../providers/label";
65
+ import { distributedKeyPhysicalFetch, distributedKeyResponseOwner } from "../providers/api-key-response-ownership";
61
66
  import { providerApiKeySelectionIsCurrent, resolveCurrentProviderApiKeyTransport } from "../providers/api-key-selection";
62
67
  import { enrichOpenCodeZenFreeTierMessage } from "../providers/opencode-zen-rate-limit";
63
68
  import type { OcxProviderTransport } from "../providers/xai-transport";
@@ -74,7 +79,7 @@ import {
74
79
  recordAttemptCredentialSource,
75
80
  type RequestLogContext,
76
81
  } from "./request-log";
77
- import { jsonCompletionSse, nativeChatSse, structuredError, usageFromChat } from "./chat-native-sse";
82
+ import { chatResponseHasUndeclaredToolCalls, UNDECLARED_CHAT_TOOL_MESSAGE, jsonCompletionSse, nativeChatSse, structuredError, usageFromChat } from "./chat-native-sse";
78
83
  import { beginInferenceAttempt, type InferenceAttempt } from "./inference/attempt";
79
84
  import { createFinalRequestLog } from "./inference/final-log";
80
85
  import { registerTurn, unregisterTurn } from "./lifecycle";
@@ -82,6 +87,7 @@ import { attachRequestSpendTracker } from "./responses/request-spend";
82
87
  import { workflowRefusalResponse } from "./workflow-refusal";
83
88
  import type { ComboProtocolSource } from "./responses/core-combo-native";
84
89
  import type { ProtocolEnvelope } from "../protocols/envelope";
90
+ import { nativeChatReplayScope, bindNativeChatReplayScope, nativeChatReasoningCapture } from "./chat-native-reasoning";
85
91
 
86
92
  export { isNativeChatRouteEligible, nativeChatDeclineReason } from "./chat-native-eligibility";
87
93
 
@@ -275,9 +281,15 @@ export async function runNativeChatAttempt(
275
281
  ): Promise<Response> {
276
282
  const { req, config, logCtx, logIds, route, requestedModel, requestedStream, translatorBudget, finishLog } = execution;
277
283
  const { sendBudget } = execution;
284
+ const clientDeclaredEmptyToolCatalog = Array.isArray(execution.chatBody.tools) && execution.chatBody.tools.length === 0;
285
+ const declaredLegacyFunctionNames = new Set<string>(clientDeclaredEmptyToolCatalog && Array.isArray(execution.chatBody.functions)
286
+ ? execution.chatBody.functions.flatMap(fn => isRec(fn) && typeof fn.name === "string" && fn.name.length > 0 ? [fn.name] : [])
287
+ : []);
278
288
  const { attempt } = attemptHandle;
279
- const onFirstOutput = execution.onFirstOutput
289
+ let outputDelivered = false;
290
+ const notifyFirstOutput = execution.onFirstOutput
280
291
  ?? (logIds ? () => recordFirstOutput(logCtx, logIds.start) : undefined);
292
+ const onFirstOutput = () => { outputDelivered = true; notifyFirstOutput?.(); };
281
293
  const fail = (status: number, message: string, type?: string, code?: string | null): Response => {
282
294
  const safeMessage = redactSecretString(message);
283
295
  finishLog(status, safeMessage);
@@ -313,14 +325,23 @@ export async function runNativeChatAttempt(
313
325
  // assigned before the transport pin in core.ts.
314
326
  // Transport variant: the bare picker answers with the persisted row, which for a built-in
315
327
  // provider carries no adapter id or base URL until routedProviderConfig backfills it.
316
- const proactiveKeyProvider = selectProactiveApiKeyTransport(config, route.providerName, route.provider);
317
- if (proactiveKeyProvider) route.provider = proactiveKeyProvider;
328
+ try {
329
+ const proactiveKeyProvider = selectProactiveApiKeyTransport(config, route.providerName, route.provider);
330
+ if (proactiveKeyProvider) route.provider = proactiveKeyProvider;
331
+ } catch (error) {
332
+ cleanupAbort(); upstream.abort();
333
+ if (!(error instanceof RequestPacingQueueOverloadError)) throw error;
334
+ const refusal = fail(429, error.message, "rate_limit_error");
335
+ refusal.headers.set("Retry-After", String(error.retryAfterSeconds));
336
+ return refusal;
337
+ }
318
338
  let activeProvider: OcxProviderConfig = route.provider;
319
339
  stampApiKeyAccountLabel(logCtx, route.providerName, activeProvider);
320
340
  const spendTracker = sendBudget ? undefined : attachRequestSpendTracker(req, logCtx);
321
341
  const physicalBudget = sendBudget ?? createInferenceSendBudget(req, logCtx);
322
342
  let activeAdapter: ProviderAdapter = createOpenAIChatAdapter(activeProvider);
323
343
  let activeRequest: AdapterRequest;
344
+ const replayScope = nativeChatReplayScope(req.headers);
324
345
  let retainedRequestBytes = 0;
325
346
  const releaseRetainedRequest = () => {
326
347
  if (retainedRequestBytes === 0) return;
@@ -333,8 +354,9 @@ export async function runNativeChatAttempt(
333
354
  retainedRequestBytes = bytes;
334
355
  };
335
356
  const buildActiveRequest = () => {
357
+ bindNativeChatReplayScope(replayScope, activeProvider, route.providerName, route.modelId);
336
358
  recordAttemptCredentialSource(attempt, route.providerName, activeProvider, activeAdapter.name);
337
- return buildOpenAIChatPassthroughRequest(
359
+ const request = buildOpenAIChatPassthroughRequest(
338
360
  activeProvider,
339
361
  execution.chatBody,
340
362
  route.modelId,
@@ -342,7 +364,10 @@ export async function runNativeChatAttempt(
342
364
  fastPolicyForModel(activeProvider, route.modelId, route.providerName, "chat"),
343
365
  config.fastMode,
344
366
  route.providerName,
367
+ replayScope,
345
368
  );
369
+ observeAdvisorNativeReasoning(req.headers, logCtx, request.body);
370
+ return request;
346
371
  };
347
372
  try {
348
373
  activeRequest = buildActiveRequest();
@@ -421,9 +446,10 @@ export async function runNativeChatAttempt(
421
446
  } else if (!spendTracker?.charge()) throw new NativeChatSpendRefusal();
422
447
  };
423
448
 
449
+ let responseOwnsDistributedKey = true;
424
450
  const send = async (
425
451
  request: AdapterRequest,
426
- recovery?: "rate-limit-429" | "key-429" | UpstreamSendRecovery,
452
+ recovery?: "rate-limit-429" | "key-429" | "key-401" | UpstreamSendRecovery,
427
453
  singleShot = false,
428
454
  ): Promise<Response> => {
429
455
  try {
@@ -453,9 +479,16 @@ export async function runNativeChatAttempt(
453
479
  onPhysicalDispatch: comboReceipt
454
480
  ? () => { if (!physicalBudget.spendEnforced) settleNonEnforcedSend(request.url); }
455
481
  : undefined,
456
- dispatchOverride: async (_input, init, execute) => {
457
- if (!providerApiKeySelectionIsCurrent(config, route.providerName, activeProvider)) {
458
- const current = resolveCurrentProviderApiKeyTransport(config, route.providerName, activeProvider);
482
+ dispatchOverride: async (_input, init, execute, reacquirePacing) => {
483
+ for (let attempt = 0; !providerApiKeySelectionIsCurrent(config, route.providerName, activeProvider); attempt++) {
484
+ if (outputDelivered) throw new Error("Provider key selection is unavailable during the native Chat turn");
485
+ if (attempt >= 3) throw new Error("Provider key selection changed repeatedly before native Chat dispatch");
486
+ let current = resolveCurrentProviderApiKeyTransport(config, route.providerName, activeProvider);
487
+ if (!current && activeProvider.requestPacing?.enabled
488
+ && (activeProvider.requestPacing.keys || activeProvider.requestPacing.keySelection === "round-robin")) {
489
+ const fresh = resolveCurrentProviderApiKeyTransport(config, route.providerName, { ...activeProvider, _apiKeyAttempt: undefined });
490
+ if (fresh) current = selectProactiveApiKeyTransport(config, route.providerName, fresh) ?? fresh;
491
+ }
459
492
  if (!current || !isNativeChatRouteEligible({ ...route, provider: current }, execution.chatBody, config)) {
460
493
  throw new Error("Provider key selection is no longer available for native Chat");
461
494
  }
@@ -467,6 +500,11 @@ export async function runNativeChatAttempt(
467
500
  activeRequest = buildActiveRequest();
468
501
  try { retainRequest(activeRequest); }
469
502
  catch (error) { activeRequest.releaseBodyObservation?.(); throw error; }
503
+ if (current.requestPacing?.enabled
504
+ && (current.requestPacing.keys || current.requestPacing.keySelection === "round-robin")) {
505
+ if (!reacquirePacing) throw new Error("Refreshed native Chat dispatch requires pacing readmission");
506
+ await reacquirePacing(current, route.modelId);
507
+ }
470
508
  }
471
509
  // The retry closure may still hold a pre-pacing request. Replace its entire
472
510
  // wire shape, not just Authorization, and retain transport recovery flags.
@@ -495,7 +533,8 @@ export async function runNativeChatAttempt(
495
533
  // A reselected provider transport is still a physical send: the connection policy
496
534
  // and manual-redirect ownership wrap the selected implementation (#4992).
497
535
  dispatched = await sendWithConnectionPolicy(
498
- (activeProvider as OcxProviderTransport).fetch ?? execute,
536
+ distributedKeyPhysicalFetch((activeProvider as OcxProviderTransport).fetch ?? execute,
537
+ route.providerName, activeProvider, request.url),
499
538
  request.url,
500
539
  applyUpstreamRecoveryInit({
501
540
  ...init, method: request.method, headers, body: request.body,
@@ -510,6 +549,8 @@ export async function runNativeChatAttempt(
510
549
  try { if (physicalBudget.spendEnforced) spendReport(1); }
511
550
  finally { spendReport.close?.(); }
512
551
  }
552
+ responseOwnsDistributedKey = !activeProvider._apiKeyAttempt?.distributed
553
+ || distributedKeyResponseOwner(dispatched, config, route.providerName) !== null;
513
554
  if (!dispatched.ok) await recordKeyAttemptFailure(logCtx, dispatched, init.signal ?? upstream.signal);
514
555
  return dispatched;
515
556
  },
@@ -566,8 +607,11 @@ export async function runNativeChatAttempt(
566
607
  }
567
608
  // Same reason as above, plus a second one: rotating here would write a cooldown against
568
609
  // a key that rate-limited nothing, and that false signal outlives the request.
569
- while (response.status === 429 && !isNonReplayableResponse(response) && hasKeyPoolFailover(activeProvider)) {
570
- const rotated = rotateProviderTransportOn429(config, route.providerName, activeProvider, {
610
+ while ((response.status === 429 || (response.status === 401 && activeProvider._apiKeyAttempt?.distributed))
611
+ && responseOwnsDistributedKey && !isNonReplayableResponse(response) && hasKeyPoolFailover(activeProvider)) {
612
+ const failureStatus = response.status;
613
+ const rotate = failureStatus === 401 ? rotateProviderTransportOn401 : rotateProviderTransportOn429;
614
+ const rotated = rotate(config, route.providerName, activeProvider, {
571
615
  retryAfter: response.headers.get("retry-after"),
572
616
  now: Date.now(),
573
617
  attemptedKey: activeProvider.apiKey,
@@ -586,7 +630,7 @@ export async function runNativeChatAttempt(
586
630
  releaseRetainedRequest();
587
631
  activeRequest = buildActiveRequest();
588
632
  retainRequest(activeRequest);
589
- response = await send(activeRequest, "key-429");
633
+ response = await send(activeRequest, failureStatus === 401 ? "key-401" : "key-429");
590
634
  }
591
635
  } catch (error) {
592
636
  releaseRetainedRequest();
@@ -594,6 +638,11 @@ export async function runNativeChatAttempt(
594
638
  upstream.abort();
595
639
  if (req.signal.aborted) return fail(499, "Client cancelled request", "client_cancelled");
596
640
  const sendError = error instanceof UpstreamRetryEvidenceError ? error.cause : error;
641
+ if (sendError instanceof RequestPacingQueueOverloadError) {
642
+ const refusal = fail(429, sendError.message, "rate_limit_error");
643
+ refusal.headers.set("Retry-After", String(sendError.retryAfterSeconds));
644
+ return refusal;
645
+ }
597
646
  if (sendError instanceof SendBudgetExhaustedError) {
598
647
  // A decision this process made, answered as the Responses path answers it: 429, not 502.
599
648
  return fail(429, sendError.message, SEND_BUDGET_EXHAUSTED_CODE, SEND_BUDGET_EXHAUSTED_CODE);
@@ -704,6 +753,7 @@ export async function runNativeChatAttempt(
704
753
  }
705
754
 
706
755
  const contentType = response.headers.get("content-type")?.toLowerCase() ?? "";
756
+ const reasoningCapture = nativeChatReasoningCapture(activeProvider, route.modelId, replayScope, translatorBudget);
707
757
  if (contentType.includes("text/event-stream") && response.body) {
708
758
  if (requestedStream) transferTurnToStream();
709
759
  let terminalStatus: number | undefined;
@@ -740,7 +790,10 @@ export async function runNativeChatAttempt(
740
790
  signal: upstream.signal,
741
791
  stallTimeoutSec: config.stallTimeoutSec,
742
792
  localUpstream: isLocalUpstream(activeProvider.baseUrl),
793
+ clientDeclaredEmptyToolCatalog,
794
+ declaredLegacyFunctionNames,
743
795
  onFirstOutput,
796
+ onChunk: reasoningCapture.observe,
744
797
  onUsage: usage => {
745
798
  if (!recordKeyWireAttemptUsage(logCtx, usage)) {
746
799
  logCtx.usage = usage;
@@ -748,6 +801,7 @@ export async function runNativeChatAttempt(
748
801
  }
749
802
  },
750
803
  onTerminal: (status: number, message?: string) => {
804
+ reasoningCapture.finish(status < 400);
751
805
  terminalStatus = status;
752
806
  if (!requestedStream) return;
753
807
  try {
@@ -758,8 +812,9 @@ export async function runNativeChatAttempt(
758
812
  releaseStreamTurn();
759
813
  }
760
814
  },
761
- ...(requestedStream ? {
762
- onCancel: () => {
815
+ onCancel: () => {
816
+ reasoningCapture.finish(false);
817
+ if (requestedStream) {
763
818
  try {
764
819
  cleanupAbort();
765
820
  upstream.abort();
@@ -767,8 +822,8 @@ export async function runNativeChatAttempt(
767
822
  } finally {
768
823
  releaseStreamTurn();
769
824
  }
770
- },
771
- } : {}),
825
+ }
826
+ },
772
827
  });
773
828
  if (requestedStream) {
774
829
  return new Response(stream, {
@@ -833,6 +888,7 @@ export async function runNativeChatAttempt(
833
888
  if (error) return fail(error.status ?? 502, error.message, error.type, error.code);
834
889
  const completion = chatCompletionJson(parsedJson);
835
890
  if (!completion) return fail(502, "upstream response contained no choices", "upstream_error");
891
+ reasoningCapture.json(completion);
836
892
  const usage = usageFromChat(completion.usage);
837
893
  if (usage) {
838
894
  if (!recordKeyWireAttemptUsage(logCtx, usage)) {
@@ -840,6 +896,9 @@ export async function runNativeChatAttempt(
840
896
  attempt.usage = usage;
841
897
  }
842
898
  }
899
+ if (clientDeclaredEmptyToolCatalog && chatResponseHasUndeclaredToolCalls(completion, declaredLegacyFunctionNames)) {
900
+ return fail(502, UNDECLARED_CHAT_TOOL_MESSAGE, "upstream_error", UNDECLARED_TOOL_CALL_ERROR_CODE);
901
+ }
843
902
  onFirstOutput?.();
844
903
  try {
845
904
  const serialized = requestedStream