@bitkyc08/opencodex 2.59.0 → 2.61.0-preview.20260922

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (374) hide show
  1. package/AGENTS_INSTALL.md +64 -0
  2. package/README.md +28 -1
  3. package/bin/ocx.mjs +382 -209
  4. package/gui/dist/assets/App-E64Rzjap.js +50 -0
  5. package/gui/dist/assets/Tray-_nfzD8k4.js +1 -0
  6. package/gui/dist/assets/index-DpdfZWMK.js +86 -0
  7. package/gui/dist/assets/index-_bpvxJu0.css +1 -0
  8. package/gui/dist/assets/usage-companion-chart-DtoK7T6h.js +1 -0
  9. package/gui/dist/favicon.png +0 -0
  10. package/gui/dist/index.html +2 -2
  11. package/gui/dist/provider-icons/stepfun-color.svg +1 -0
  12. package/package.json +8 -1
  13. package/src/adapters/anthropic-image-codec.ts +16 -2
  14. package/src/adapters/anthropic-image-normalize.ts +49 -2
  15. package/src/adapters/anthropic.ts +20 -1
  16. package/src/adapters/base.ts +23 -0
  17. package/src/adapters/coding-agent/protocol.ts +36 -6
  18. package/src/adapters/coding-agent/turn.ts +32 -4
  19. package/src/adapters/command-code.ts +52 -4
  20. package/src/adapters/cursor/catalog.ts +51 -7
  21. package/src/adapters/cursor/checkpoint-store.ts +3 -0
  22. package/src/adapters/cursor/discovery.ts +11 -8
  23. package/src/adapters/cursor/live-transport.ts +26 -9
  24. package/src/adapters/cursor/protobuf-request.ts +6 -3
  25. package/src/adapters/cursor/request-builder.ts +20 -4
  26. package/src/adapters/cursor/transport.ts +19 -0
  27. package/src/adapters/cursor.ts +14 -3
  28. package/src/adapters/declaration-carrier.ts +45 -0
  29. package/src/adapters/devin/cloud-direct/chat.ts +3 -1
  30. package/src/adapters/devin/cloud-direct/stated-reset-retry.ts +42 -5
  31. package/src/adapters/devin.ts +125 -36
  32. package/src/adapters/google-antigravity-replay.ts +1 -1
  33. package/src/adapters/google-antigravity-wire.ts +34 -7
  34. package/src/adapters/google-errors.ts +7 -1
  35. package/src/adapters/google-http.ts +49 -10
  36. package/src/adapters/google-tool-schema.ts +595 -31
  37. package/src/adapters/google-wire-compiler.ts +93 -10
  38. package/src/adapters/google-wire-shape.ts +461 -0
  39. package/src/adapters/google.ts +66 -11
  40. package/src/adapters/image.ts +4 -1
  41. package/src/adapters/input-media-guard.ts +21 -9
  42. package/src/adapters/kiro/usage.ts +3 -2
  43. package/src/adapters/kiro-tool-fallback.ts +1 -1
  44. package/src/adapters/ollama-native.ts +6 -0
  45. package/src/adapters/openai-chat/developer-role.ts +61 -0
  46. package/src/adapters/openai-chat/messages.ts +46 -27
  47. package/src/adapters/openai-chat/parallel-tool-calls.ts +32 -0
  48. package/src/adapters/openai-chat/passthrough.ts +33 -9
  49. package/src/adapters/openai-chat/reasoning-wire.ts +89 -0
  50. package/src/adapters/openai-chat-images.ts +3 -1
  51. package/src/adapters/openai-chat.ts +23 -58
  52. package/src/adapters/openai-responses/image-gen.ts +8 -6
  53. package/src/adapters/openai-responses/passthrough.ts +17 -3
  54. package/src/adapters/openai-responses/reasoning.ts +7 -0
  55. package/src/adapters/opencode-go-additional-tools.ts +12 -2
  56. package/src/adapters/registry.ts +3 -2
  57. package/src/adapters/run-turn-queue.ts +178 -29
  58. package/src/adapters/xai-web-search.ts +16 -1
  59. package/src/bridge/errors.ts +8 -2
  60. package/src/bridge/response-json.ts +9 -1
  61. package/src/bridge/sse.ts +13 -151
  62. package/src/chat/inbound.ts +141 -5
  63. package/src/claude/desktop-3p.ts +7 -1
  64. package/src/claude/desktop-first-party.ts +183 -0
  65. package/src/claude/desktop-gateway-state.ts +41 -0
  66. package/src/claude/inbound-content-options.ts +6 -0
  67. package/src/claude/inbound.ts +32 -6
  68. package/src/claude/intercept/connect-proxy.ts +179 -0
  69. package/src/claude/intercept/listener.ts +122 -0
  70. package/src/claude/intercept/local-ca.ts +298 -0
  71. package/src/claude/intercept/runtime.ts +98 -0
  72. package/src/claude/intercept/settings.ts +189 -0
  73. package/src/cli/access.ts +87 -0
  74. package/src/cli/account-auth.ts +19 -0
  75. package/src/cli/account-extended.ts +4 -4
  76. package/src/cli/capabilities.ts +31 -0
  77. package/src/cli/claude-desktop.ts +206 -16
  78. package/src/cli/codex-shim-autorestore.ts +3 -0
  79. package/src/cli/companion.ts +56 -0
  80. package/src/cli/dispatch.ts +46 -7
  81. package/src/cli/doctor.ts +28 -9
  82. package/src/cli/ensure-desired-integrations.ts +43 -5
  83. package/src/cli/help.ts +7 -9
  84. package/src/cli/hub.ts +3 -2
  85. package/src/cli/index.ts +203 -59
  86. package/src/cli/init.ts +8 -0
  87. package/src/cli/integrations.ts +7 -1
  88. package/src/cli/opencode.ts +2 -2
  89. package/src/cli/provider.ts +13 -1
  90. package/src/cli/registry.ts +41 -2
  91. package/src/cli/resolve.ts +230 -0
  92. package/src/cli/root.ts +24 -1
  93. package/src/cli/start-ownership-publication.ts +56 -0
  94. package/src/cli/status-probes.ts +2 -18
  95. package/src/cli/status.ts +62 -0
  96. package/src/cli/stop-report.ts +143 -0
  97. package/src/cli/uninstall-plan.ts +9 -0
  98. package/src/client/machine-api.ts +2 -2
  99. package/src/client/machine-listener.ts +4 -7
  100. package/src/client/runtime.ts +26 -2
  101. package/src/clients/aside-profiles.ts +4 -0
  102. package/src/clients/config-export/zcode-store.ts +157 -0
  103. package/src/clients/config-export.ts +36 -0
  104. package/src/codex/account-store.ts +65 -0
  105. package/src/codex/app-server-processes.ts +72 -40
  106. package/src/codex/auth-api/account-list.ts +19 -11
  107. package/src/codex/auth-api/login-flow.ts +6 -1
  108. package/src/codex/auth-api/pool-quota-probe.ts +30 -7
  109. package/src/codex/autostart-health.ts +28 -0
  110. package/src/codex/catalog/build-entries.ts +2 -2
  111. package/src/codex/catalog/effort.ts +3 -3
  112. package/src/codex/catalog/gather-capture.ts +21 -2
  113. package/src/codex/catalog/model-hints.ts +29 -28
  114. package/src/codex/catalog/parsing.ts +7 -0
  115. package/src/codex/catalog/provider-models.ts +43 -17
  116. package/src/codex/catalog/retained-sync.ts +24 -28
  117. package/src/codex/catalog/routed-gather.ts +19 -0
  118. package/src/codex/context-compat.ts +5 -2
  119. package/src/codex/convergence.ts +2 -2
  120. package/src/codex/desired-state.ts +4 -1
  121. package/src/codex/history-job.ts +6 -6
  122. package/src/codex/history-provider.ts +31 -166
  123. package/src/codex/history-rollout-read.ts +174 -0
  124. package/src/codex/inject/config-toml.ts +41 -6
  125. package/src/codex/inject/paginated-openai-compat.ts +90 -0
  126. package/src/codex/inject.ts +18 -15
  127. package/src/codex/injected-marker.ts +18 -0
  128. package/src/codex/internal/catalog-writer.ts +33 -1
  129. package/src/codex/main-account.ts +6 -0
  130. package/src/codex/model-cache.ts +99 -6
  131. package/src/codex/model-entitlement-admission.ts +59 -0
  132. package/src/codex/model-entitlements.ts +116 -54
  133. package/src/codex/native-main-admission.ts +83 -0
  134. package/src/codex/observed-model-denials.ts +101 -8
  135. package/src/codex/prompt-text-probe.ts +9 -6
  136. package/src/codex/routing/health-store.ts +39 -0
  137. package/src/codex/routing/selection.ts +37 -1
  138. package/src/codex/routing.ts +12 -42
  139. package/src/codex/shim-templates.ts +29 -3
  140. package/src/codex/shim.ts +1 -1
  141. package/src/codex/subagent-model-fallback.ts +22 -4
  142. package/src/combos/failover.ts +3 -0
  143. package/src/companion/settings.ts +132 -0
  144. package/src/config/admitted-identity.ts +222 -0
  145. package/src/config/atomic-write.ts +117 -5
  146. package/src/config/diagnostics.ts +22 -1
  147. package/src/config/feature-flags.ts +5 -0
  148. package/src/config/load-degrade.ts +52 -7
  149. package/src/config/process-state.ts +1 -1
  150. package/src/config/proxy-env.ts +8 -2
  151. package/src/config/schema/compaction-triggers.ts +11 -0
  152. package/src/config/schema/config-schema.ts +31 -1
  153. package/src/config/schema/leaf-validators.ts +59 -0
  154. package/src/config.ts +3 -3
  155. package/src/generated/compatibility-version.json +604 -280
  156. package/src/grok/reset-coupons.ts +38 -19
  157. package/src/images/loop.ts +6 -1
  158. package/src/integrations/aside-profile-context.ts +37 -3
  159. package/src/integrations/aside-profile-journal.ts +68 -3
  160. package/src/integrations/aside-profiles.ts +128 -3
  161. package/src/integrations/config-io.ts +44 -10
  162. package/src/integrations/merge.ts +120 -13
  163. package/src/integrations/mutation-plan.ts +921 -0
  164. package/src/integrations/registry.ts +38 -0
  165. package/src/integrations/state.ts +78 -45
  166. package/src/integrations/target.ts +208 -0
  167. package/src/integrations/writer.ts +134 -110
  168. package/src/lab/conformance/fixture-provider.ts +5 -0
  169. package/src/lab/live/transport.ts +4 -0
  170. package/src/lab/live/types.ts +5 -0
  171. package/src/lab/subject/behavior-fingerprint.ts +1 -1
  172. package/src/lib/admin-secrets.ts +9 -1
  173. package/src/lib/browser-launch-notice.ts +59 -0
  174. package/src/lib/bun-runtime.ts +6 -2
  175. package/src/lib/debug-log-buffer.ts +6 -1
  176. package/src/lib/debug.ts +63 -0
  177. package/src/lib/errors.ts +79 -0
  178. package/src/lib/http-response-semantics.ts +57 -0
  179. package/src/lib/lab-live-pinned-sender.ts +26 -12
  180. package/src/lib/open-url.ts +51 -7
  181. package/src/lib/package-tree-integrity.ts +2 -1
  182. package/src/lib/package-version.ts +8 -0
  183. package/src/lib/pinned-http.ts +142 -2
  184. package/src/lib/plain-data.ts +103 -0
  185. package/src/lib/process-control.ts +13 -5
  186. package/src/lib/provider-egress.ts +310 -0
  187. package/src/lib/provider-outbound.ts +109 -16
  188. package/src/lib/proxy-env.ts +82 -7
  189. package/src/lib/request-execution-budget.ts +72 -0
  190. package/src/lib/request-failure-attribution.ts +183 -0
  191. package/src/lib/request-failure-model.ts +236 -0
  192. package/src/lib/request-resend-gate.ts +138 -0
  193. package/src/lib/socks5-fetch.ts +136 -26
  194. package/src/lib/spend-ledger-owner.ts +364 -0
  195. package/src/lib/spend-reservation-ledger.ts +218 -27
  196. package/src/lib/standalone.ts +16 -0
  197. package/src/lib/upstream-retry.ts +167 -16
  198. package/src/lib/windows-system-proxy.ts +16 -11
  199. package/src/lib/winsw.ts +2 -2
  200. package/src/oauth/callback-server.ts +4 -3
  201. package/src/oauth/generic-account-failover.ts +1 -0
  202. package/src/oauth/health.ts +12 -1
  203. package/src/oauth/index.ts +27 -108
  204. package/src/oauth/login-cli.ts +80 -29
  205. package/src/oauth/login-flow-state.ts +127 -0
  206. package/src/providers/api-key-resolve.ts +133 -0
  207. package/src/providers/api-key-selection.ts +5 -1
  208. package/src/providers/derive.ts +34 -17
  209. package/src/providers/devin-cli-authmode-migration.ts +14 -10
  210. package/src/providers/key-failover.ts +97 -19
  211. package/src/providers/key-store.ts +34 -110
  212. package/src/providers/model-rename-fields.ts +147 -0
  213. package/src/providers/model-rename-migration.ts +179 -38
  214. package/src/providers/model-rename-startup.ts +7 -5
  215. package/src/providers/openai-virtual-models.ts +42 -2
  216. package/src/providers/quota/antigravity.ts +22 -2
  217. package/src/providers/quota/vendor-probes-key.ts +38 -23
  218. package/src/providers/reasoning-metadata.ts +43 -18
  219. package/src/providers/registry/entries-core.ts +47 -20
  220. package/src/providers/registry/entries-extended.ts +63 -4
  221. package/src/providers/registry/model-ids.ts +168 -0
  222. package/src/providers/registry/model-seeds.ts +56 -10
  223. package/src/providers/registry/types.ts +2 -0
  224. package/src/providers/resolved-model-policy-merge.ts +167 -0
  225. package/src/providers/resolved-model-policy.ts +406 -0
  226. package/src/providers/stale-vision-classification-migration.ts +137 -0
  227. package/src/providers/xai-transport.ts +12 -1
  228. package/src/reasoning-effort.ts +8 -0
  229. package/src/responses/apply-patch-envelope.ts +0 -12
  230. package/src/responses/freeform-wrapper-scan.ts +279 -0
  231. package/src/responses/function-call-compat.ts +38 -1
  232. package/src/responses/inline-document.ts +65 -0
  233. package/src/responses/input-media.ts +42 -8
  234. package/src/responses/legacy-dotted-tool-name-repair.ts +134 -0
  235. package/src/responses/muse-tool-name-alias.ts +19 -0
  236. package/src/responses/parser-content.ts +8 -2
  237. package/src/responses/parser-tools.ts +3 -0
  238. package/src/responses/parser.ts +3 -1
  239. package/src/responses/progressive-freeform-input.ts +130 -0
  240. package/src/responses/reasoning-envelope.ts +30 -0
  241. package/src/responses/schema.ts +3 -0
  242. package/src/responses/state.ts +5 -12
  243. package/src/responses/tool-name-aliases.ts +15 -1
  244. package/src/router.ts +108 -117
  245. package/src/routing/compatibility/behavior.ts +9 -0
  246. package/src/routing/compatibility/subject.ts +16 -1
  247. package/src/server/adapter-resolve.ts +9 -0
  248. package/src/server/admission-model-scope.ts +219 -0
  249. package/src/server/audio-live.ts +9 -3
  250. package/src/server/audio-upstream.ts +18 -0
  251. package/src/server/auth-cors.ts +29 -0
  252. package/src/server/chat-completions.ts +60 -4
  253. package/src/server/chat-native.ts +19 -4
  254. package/src/server/claude-messages.ts +61 -20
  255. package/src/server/effort-row.ts +11 -3
  256. package/src/server/grok-responses-control-frame.ts +160 -1
  257. package/src/server/grok-responses-snapshot-repair.ts +113 -11
  258. package/src/server/gui-freshness.ts +103 -0
  259. package/src/server/gui-static.ts +7 -9
  260. package/src/server/images.ts +59 -6
  261. package/src/server/index/claude-intercept-lifecycle.ts +49 -0
  262. package/src/server/index/serve-options.ts +142 -39
  263. package/src/server/index/spend-ledger-lifecycle.ts +92 -0
  264. package/src/server/index/startup-warnings.ts +24 -0
  265. package/src/server/index/websocket-handler.ts +6 -1
  266. package/src/server/index.ts +34 -38
  267. package/src/server/lifecycle.ts +4 -4
  268. package/src/server/live-call-bindings.ts +6 -0
  269. package/src/server/live.ts +88 -3
  270. package/src/server/management/agent-settings-routes.ts +121 -36
  271. package/src/server/management/aside-profile-routes.ts +266 -7
  272. package/src/server/management/companion-routes.ts +77 -0
  273. package/src/server/management/config-routes.ts +18 -2
  274. package/src/server/management/context.ts +3 -0
  275. package/src/server/management/integration-routes.ts +287 -5
  276. package/src/server/management/logs-usage-routes.ts +19 -0
  277. package/src/server/management/metrics-routes.ts +20 -0
  278. package/src/server/management/model-rows.ts +224 -12
  279. package/src/server/management/native-integration-routes.ts +103 -6
  280. package/src/server/management/oauth-account-routes.ts +45 -7
  281. package/src/server/management/route-registry.ts +20 -0
  282. package/src/server/management/shared.ts +28 -4
  283. package/src/server/management/system-restart.ts +7 -2
  284. package/src/server/management/system-routes.ts +2 -0
  285. package/src/server/management/usage-aggregate-cache.ts +4 -0
  286. package/src/server/management/usage-timeline-routes.ts +44 -0
  287. package/src/server/management-api.ts +10 -9
  288. package/src/server/management-auth.ts +15 -1
  289. package/src/server/proxy-liveness.ts +75 -0
  290. package/src/server/readiness.ts +29 -10
  291. package/src/server/relay-eager.ts +24 -2
  292. package/src/server/relay.ts +138 -12
  293. package/src/server/request-log-failure-attribution.ts +99 -0
  294. package/src/server/request-log.ts +169 -2
  295. package/src/server/request-metrics.ts +298 -0
  296. package/src/server/responses/adapter-continuation.ts +3 -3
  297. package/src/server/responses/adapter-dispatch.ts +11 -6
  298. package/src/server/responses/codex-ws-wire.ts +34 -8
  299. package/src/server/responses/combo-stream-preflight.ts +168 -6
  300. package/src/server/responses/compact.ts +43 -10
  301. package/src/server/responses/compaction-routing.ts +111 -0
  302. package/src/server/responses/core-codex-account.ts +8 -3
  303. package/src/server/responses/core-combo.ts +7 -7
  304. package/src/server/responses/core-normalize.ts +6 -12
  305. package/src/server/responses/core-opaque-recovery.ts +91 -0
  306. package/src/server/responses/core-options.ts +4 -0
  307. package/src/server/responses/encrypted-payload.ts +20 -2
  308. package/src/server/responses/fetch-helpers.ts +124 -8
  309. package/src/server/responses/input-admission.ts +10 -0
  310. package/src/server/responses/passthrough-delivery.ts +45 -15
  311. package/src/server/responses/passthrough-dispatch.ts +215 -44
  312. package/src/server/responses/passthrough-error.ts +27 -8
  313. package/src/server/responses/policy-fallback.ts +5 -13
  314. package/src/server/responses/request-prepare.ts +109 -18
  315. package/src/server/responses/request-send-budget.ts +17 -1
  316. package/src/server/responses/request-sidecar-auth.ts +1 -1
  317. package/src/server/responses/request-transport.ts +26 -6
  318. package/src/server/responses/reset-replay.ts +108 -0
  319. package/src/server/responses/run-turn-execution.ts +25 -3
  320. package/src/server/responses/sidecar-execution.ts +17 -2
  321. package/src/server/responses/ws-upstream.ts +14 -27
  322. package/src/server/responses-custom-tool-repair.ts +27 -54
  323. package/src/server/responses-request-tool-scope.ts +214 -0
  324. package/src/server/responses-undeclared-tool-guard.ts +35 -2
  325. package/src/server/search.ts +25 -1
  326. package/src/server/sse-payload-rewrite.ts +1 -1
  327. package/src/server/usage-ledger-retention.ts +73 -0
  328. package/src/service/cli.ts +48 -2
  329. package/src/service/health.ts +3 -2
  330. package/src/service/install-state-contract.d.mts +27 -0
  331. package/src/service/install-state-contract.mjs +34 -0
  332. package/src/service/launchd.ts +1 -1
  333. package/src/service/orchestration.ts +2 -4
  334. package/src/service/ownership-compatibility.ts +164 -0
  335. package/src/service/ownership-mutation-lease.d.mts +32 -0
  336. package/src/service/ownership-mutation-lease.mjs +211 -0
  337. package/src/service/repair.ts +45 -1
  338. package/src/service/state-lock.ts +269 -0
  339. package/src/service/state-record.d.mts +36 -0
  340. package/src/service/state-record.mjs +138 -0
  341. package/src/service/state.ts +582 -68
  342. package/src/service/windows-taskxml.ts +11 -10
  343. package/src/service.ts +7 -3
  344. package/src/tray/windows-tray.ps1 +156 -4
  345. package/src/types/config.ts +48 -3
  346. package/src/types/provider.ts +91 -0
  347. package/src/types/request.ts +30 -2
  348. package/src/types/tools.ts +33 -0
  349. package/src/types.ts +4 -0
  350. package/src/update/index.ts +207 -63
  351. package/src/update/job.ts +9 -5
  352. package/src/update/ownership-transaction.ts +47 -0
  353. package/src/update/restart-ownership.ts +54 -0
  354. package/src/update/runtime-ownership.d.mts +40 -0
  355. package/src/update/runtime-ownership.mjs +122 -0
  356. package/src/usage/attempt-delivery.ts +198 -0
  357. package/src/usage/cache-diagnostic.ts +305 -0
  358. package/src/usage/failure-fingerprint.ts +118 -0
  359. package/src/usage/failure-projection-cache.ts +174 -0
  360. package/src/usage/failure-projection.ts +174 -0
  361. package/src/usage/ledger-retention.ts +165 -0
  362. package/src/usage/log.ts +126 -79
  363. package/src/usage/request-outcome.ts +150 -0
  364. package/src/usage/retention-contract.ts +28 -0
  365. package/src/usage/summary.ts +2 -2
  366. package/src/usage/telemetry-contract.ts +237 -0
  367. package/src/usage/timeline.ts +236 -0
  368. package/src/vision/eligibility.ts +88 -9
  369. package/src/vision/plan.ts +34 -10
  370. package/src/web-search/alpha-search.ts +21 -1
  371. package/src/web-search/executor.ts +41 -2
  372. package/src/web-search/loop.ts +6 -1
  373. package/gui/dist/assets/index-C5IebErG.js +0 -136
  374. package/gui/dist/assets/index-OESInAjC.css +0 -1
@@ -41,7 +41,6 @@ import {
41
41
  import {
42
42
  copyPreviousResponseReplayProvenance,
43
43
  expandPreviousResponseInput,
44
- previousResponseScopeMismatch,
45
44
  previousResponseReplayFailure,
46
45
  markBodyNonPersistable,
47
46
  previousResponseProviderState,
@@ -53,9 +52,10 @@ import { parseRequest } from "../../responses/parser";
53
52
  import { anthropicSessionKeyFromParts } from "../../oauth/anthropic-routing";
54
53
  import { isTranslatorBudgetExceededError } from "../../lib/translator-budget";
55
54
  import { bindTurnTerminationScope, rememberDeliveredFinalAnswer } from "../../responses/turn-termination";
56
- import { requestLogSpeedLabel, readConfiguredCodexServiceTier } from "../request-log";
55
+ import { observeCacheDiagnosticInbound, rebindCacheDiagnosticBody, requestLogSpeedLabel, readConfiguredCodexServiceTier } from "../request-log";
57
56
  import type { RouteResult } from "../../router";
58
57
  import {
58
+ captureRouteStaticPolicy,
59
59
  routeConcreteModel,
60
60
  routeCompactionModel,
61
61
  routeModel,
@@ -110,6 +110,13 @@ import {
110
110
  CODEX_RESERVE_OPT_IN_REQUIRED_MESSAGE,
111
111
  } from "../../codex/loopback-target";
112
112
  import { checkComboTargetInputAdmission, checkInputAdmission } from "./input-admission";
113
+ import {
114
+ admissionModelDeniedResponse,
115
+ AdmissionModelDeniedError,
116
+ assertRouteAllowedByScope,
117
+ resolveAdmissionModelScope,
118
+ routeAllowedByScope,
119
+ } from "../admission-model-scope";
113
120
  import { nativeContextLimits } from "../../codex/catalog";
114
121
  import { streamingContextOverflowResponse } from "./context-overflow";
115
122
  import {
@@ -119,6 +126,7 @@ import {
119
126
  codexLogAccountId,
120
127
  } from "./core-codex-account";
121
128
  import { acquireUpstreamHostAdmission } from "../../codex/upstream-host-health";
129
+ import { applyCompactionRoutingOverride, compactionRoutingKeepsProviderIdentity } from "./compaction-routing";
122
130
  import { codexAuthContextLogLabel } from "../../codex/account-label";
123
131
  import {
124
132
  conversationStateBindingFromAuth,
@@ -149,6 +157,21 @@ export async function prepareResponsesRequest(
149
157
  }
150
158
  return decodeRequestErrorResponse(err, "responses");
151
159
  }
160
+ observeCacheDiagnosticInbound(
161
+ logCtx,
162
+ body,
163
+ req.headers,
164
+ options.promptCacheKeyIsSharedCohort === true
165
+ ? "system-derived"
166
+ : options.promptCacheKeyIsSharedCohort === false ? "metadata-derived" : "caller",
167
+ );
168
+ if (!options.comboAttempt && !options.compactionRoutingOverride && inboundWire === "responses") {
169
+ options.compactionRoutingOverride = applyCompactionRoutingOverride(body, req.headers, config, {
170
+ endpoint: "responses",
171
+ transport: options.inboundTransport,
172
+ });
173
+ }
174
+ options.onRequestBodyParsed?.(body);
152
175
  // An effort row naming a table-less combo (`combo/x--high`) must reach the combo dispatcher
153
176
  // as its base id, so the selector is normalized here, before comboIdFromRawBody reads model.
154
177
  const comboRows = !options.comboAttempt && body && typeof body === "object" && !Array.isArray(body)
@@ -180,7 +203,7 @@ export async function prepareResponsesRequest(
180
203
  }
181
204
  // Compaction may send the last client-visible bare model after a combo switch.
182
205
  // Configured selectors take precedence; otherwise recall before combo dispatch (#3891).
183
- if (!options.comboAttempt && body && typeof body === "object" && !Array.isArray(body)) {
206
+ if (!options.comboAttempt && !options.compactionRoutingOverride && body && typeof body === "object" && !Array.isArray(body)) {
184
207
  const rawModel = (body as { model?: unknown }).model;
185
208
  const rawInput = (body as { input?: unknown }).input;
186
209
  const isCompactionTrigger = Array.isArray(rawInput)
@@ -202,7 +225,7 @@ export async function prepareResponsesRequest(
202
225
  // hops — which only exist inside that loop — are unreachable (#4129). Rewrite the selector
203
226
  // here instead, before comboIdFromRawBody reads `model`, and identify the combo by CONFIG
204
227
  // LOOKUP so the check can never observe a one-candidate collapse.
205
- if (!options.comboAttempt && body && typeof body === "object" && !Array.isArray(body)) {
228
+ if (!options.comboAttempt && !options.compactionRoutingOverride && body && typeof body === "object" && !Array.isArray(body)) {
206
229
  const shadowIntercept = config.shadowCallIntercept;
207
230
  const rawShadowModel = (body as { model?: unknown }).model;
208
231
  if (shadowIntercept?.enabled && shadowIntercept.model && typeof rawShadowModel === "string"
@@ -243,10 +266,13 @@ export async function prepareResponsesRequest(
243
266
  copyPreviousResponseReplayProvenance(options.comboReplaySnapshot.sourceBody, body);
244
267
  } else {
245
268
  body = expandPreviousResponseInput(body, inboundClientThreadId);
246
- if (previousResponseScopeMismatch(body)) {
247
- console.warn("[opencodex] dropped a previous_response_id with a mismatched client task scope; continuing fresh");
269
+ const replayFailure = previousResponseReplayFailure(body);
270
+ if (replayFailure?.reason === "scope_mismatch") {
271
+ // Bounded and content-free: no task scope and nothing about the retained entry.
272
+ console.warn("[opencodex] refusing continuation because the client task scope does not match replay state");
248
273
  }
249
- if (previousResponseReplayFailure(body)) {
274
+ // Local replay failures require full client replay.
275
+ if (replayFailure) {
250
276
  return formatErrorResponse(
251
277
  400,
252
278
  "previous_response_not_found",
@@ -262,10 +288,14 @@ export async function prepareResponsesRequest(
262
288
  // encrypted_content slots as plaintext. Rewrite them to input_text on the RAW body BEFORE
263
289
  // parsing so every consumer sees the payload: parseRequest (routed/translated providers read
264
290
  // the parsed messages) and the native passthrough (_rawBody is this same object, serialized
265
- // verbatim). Genuine backend ciphertext is left byte-identical (looksLikeBackendCiphertext).
291
+ // verbatim). Structurally valid backend ciphertext stays byte-identical; encoded-looking unknown
292
+ // slots remain opaque only until final-route handling can preserve or strip them safely.
266
293
  {
267
294
  const rewritten = sanitizeEncryptedContentInPlace(
268
295
  (body as { input?: unknown } | undefined)?.input,
296
+ // The final destination is not known yet. Keep ambiguous encoded slots opaque until route
297
+ // selection can either strip them for a third party or apply strict native classification.
298
+ { preserveUnknownOpaqueSlots: true },
269
299
  );
270
300
  if (rewritten > 0)
271
301
  console.warn(
@@ -278,6 +308,10 @@ export async function prepareResponsesRequest(
278
308
  try {
279
309
  parsed = parseRequest(body);
280
310
  parsed._promptCacheKeyIsSharedCohort = options.promptCacheKeyIsSharedCohort;
311
+ // The body may have been rebuilt since the inbound observation (previous-response
312
+ // expansion); alias the parsed raw body to the same draft so the outbound
313
+ // observation at the adapter seam still finds it.
314
+ rebindCacheDiagnosticBody(parsed._rawBody, logCtx.cacheDiagnosticDraft);
281
315
  // Captured before any parser mutates it, so both grammars see the client's id.
282
316
  const { fastRow, effortRow } = parseSyntheticRowId(parsed.modelId, config);
283
317
  if (fastRow) {
@@ -378,7 +412,7 @@ export async function prepareResponsesRequest(
378
412
  if (!logCtx.conversationId) {
379
413
  logCtx.conversationId = resolvedConversationId;
380
414
  }
381
- logCtx.requestedModel = parsed.modelId;
415
+ logCtx.requestedModel = options.compactionRoutingOverride?.sourceModel ?? parsed.modelId;
382
416
  logCtx.requestedEffort = parsed.options.reasoning;
383
417
  // What this request may spend beyond its input, for the durable spend reservation (#4707).
384
418
  // Read from the caller rather than from the adapter's serialized body, because the
@@ -396,19 +430,40 @@ export async function prepareResponsesRequest(
396
430
 
397
431
  let route: RouteResult;
398
432
  let credentialDomainWasRewritten = false;
433
+ // The selector the caller actually sent, captured before shadow interception
434
+ // or a subagent fallback rewrites it, so a refusal names the client's own
435
+ // request rather than a destination it never asked for.
436
+ const inboundSelector = parsed.modelId;
437
+ const admissionScope = resolveAdmissionModelScope(config, options.admission);
438
+ const captureInboundRoutePolicy = (candidate: RouteResult): RouteResult => {
439
+ // Every route this request path produces passes through here: the direct
440
+ // name, an alias, a policy or combo selection, a compaction override, a
441
+ // shadow-intercept target and both subagent-fallback re-routes. Checking
442
+ // the key's scope at this one point is what stops a rewrite from reaching
443
+ // a destination the front door would have refused.
444
+ assertRouteAllowedByScope(admissionScope, inboundSelector, candidate);
445
+ candidate.staticPolicy = captureRouteStaticPolicy(
446
+ candidate.providerName,
447
+ candidate.modelId,
448
+ candidate.provider,
449
+ candidate.staticPolicy.effectiveAlias,
450
+ inboundWire,
451
+ );
452
+ return candidate;
453
+ };
399
454
  try {
400
455
  // A `compaction_trigger` turn may name a bare native model the operator has
401
456
  // no canonical OpenAI route for (#2901). Only the initial compaction route
402
457
  // may fall back to the configured default provider; combo attempts and the
403
458
  // later fallback/recovery re-routes keep the ordinary reservation.
404
- const resolveRoute = (modelId: string) => options.comboAttempt
459
+ const resolveRoute = (modelId: string) => captureInboundRoutePolicy(options.comboAttempt
405
460
  ? routeConcreteModel(config, modelId)
406
461
  : parsed._compactionRequest === true
407
462
  ? routeCompactionModel(config, modelId, evidenceFromBody(parsed._rawBody))
408
- : routeModel(config, modelId, evidenceFromBody(parsed._rawBody));
463
+ : routeModel(config, modelId, evidenceFromBody(parsed._rawBody)));
409
464
  const _sci = config.shadowCallIntercept;
410
465
  let shadowRoute: RouteResult | undefined;
411
- if (_sci?.enabled && _sci.model && isShadowSourceModel(parsed.modelId, _sci.sourceModels)) {
466
+ if (!options.compactionRoutingOverride && _sci?.enabled && _sci.model && isShadowSourceModel(parsed.modelId, _sci.sourceModels)) {
412
467
  const sourcePrefix = shadowSourceModelPrefix(parsed.modelId, _sci.sourceModels)!;
413
468
  let sourceIdentity = { providerName: OPENAI_CODEX_PROVIDER_ID, modelId: sourcePrefix };
414
469
  try {
@@ -436,10 +491,20 @@ export async function prepareResponsesRequest(
436
491
  shadowRoute = targetRoute;
437
492
  }
438
493
  }
439
- if (parsed._compactionRequest === true) parsed._cursorIsolateConversation = true;
494
+ if (parsed._compactionRequest === true || options.compactionRoutingOverride) parsed._cursorIsolateConversation = true;
440
495
  route = shadowRoute ?? resolveRoute(parsed.modelId);
496
+ if (options.compactionRoutingOverride && !compactionRoutingKeepsProviderIdentity(config, options.compactionRoutingOverride, route)) {
497
+ credentialDomainWasRewritten = true;
498
+ // The destination does not share the conversation's credential domain, so it can neither
499
+ // verify the source backend's reasoning ciphertext nor decode its native compaction blob.
500
+ // This is the same condition an account change already reports (account-change-state.ts),
501
+ // and the serializer turns a stored summary into readable text instead of dropping it.
502
+ parsed._stripReasoningEncryptedContent = true;
503
+ if (parsed._compactionRequest === true) parsed._portableCompaction = true;
504
+ }
441
505
  logCtx.routeDecision = route.routeDecision;
442
506
  } catch (err) {
507
+ if (err instanceof AdmissionModelDeniedError) return admissionModelDeniedResponse(err);
443
508
  if (err instanceof NoAvailableComboTargetsError) {
444
509
  return comboUnavailable(err.comboId);
445
510
  }
@@ -633,10 +698,13 @@ export async function prepareResponsesRequest(
633
698
 
634
699
  if (fallback?.to && !slugsEquivalent(fallback.to, route.modelId)) {
635
700
  try {
636
- route = routeModel(config, fallback.to, evidenceFromBody(parsed._rawBody));
701
+ route = captureInboundRoutePolicy(
702
+ routeModel(config, fallback.to, evidenceFromBody(parsed._rawBody)),
703
+ );
637
704
  credentialDomainWasRewritten = true;
638
705
  logCtx.routeDecision = route.routeDecision;
639
706
  } catch (err) {
707
+ if (err instanceof AdmissionModelDeniedError) return admissionModelDeniedResponse(err);
640
708
  if (err instanceof NoAvailableComboTargetsError) {
641
709
  return comboUnavailable(err.comboId);
642
710
  }
@@ -831,10 +899,13 @@ export async function prepareResponsesRequest(
831
899
 
832
900
  if (fallback?.to && !slugsEquivalent(fallback.to, route.modelId)) {
833
901
  try {
834
- route = routeModel(config, fallback.to, evidenceFromBody(parsed._rawBody));
902
+ route = captureInboundRoutePolicy(
903
+ routeModel(config, fallback.to, evidenceFromBody(parsed._rawBody)),
904
+ );
835
905
  credentialDomainWasRewritten = true;
836
906
  logCtx.routeDecision = route.routeDecision;
837
907
  } catch (err) {
908
+ if (err instanceof AdmissionModelDeniedError) return admissionModelDeniedResponse(err);
838
909
  if (err instanceof NoAvailableComboTargetsError) {
839
910
  return comboUnavailable(err.comboId);
840
911
  }
@@ -857,6 +928,17 @@ export async function prepareResponsesRequest(
857
928
 
858
929
  if (options.abortSignal?.aborted) return clientCancelledResponse();
859
930
 
931
+ if (inboundWire === "responses" && isCanonicalOpenAiForwardProvider(route.provider)) {
932
+ const rewritten = sanitizeEncryptedContentInPlace(
933
+ (body as { input?: unknown } | undefined)?.input,
934
+ );
935
+ if (rewritten > 0) {
936
+ console.warn(
937
+ `[opencodex] rewrote ${rewritten} non-Fernet encrypted_content part(s) before canonical native replay`,
938
+ );
939
+ }
940
+ }
941
+
860
942
  // Encrypted child tasks may reach the canonical native backend or an explicitly trusted
861
943
  // direct Responses route. This runs against the FINAL route so native-only fallback can
862
944
  // rescue an incompatible primary without weakening combo behavior.
@@ -902,6 +984,7 @@ export async function prepareResponsesRequest(
902
984
  route.modelId,
903
985
  route.provider,
904
986
  inboundWire,
987
+ route.staticPolicy,
905
988
  );
906
989
  if (wireProvider.adapter === "openai-responses" && !isCanonicalOpenAiForwardProvider(wireProvider)) {
907
990
  const repaired = stripAgentMessageCiphertextInPlace((body as { input?: unknown } | undefined)?.input);
@@ -925,12 +1008,12 @@ export async function prepareResponsesRequest(
925
1008
  return formatErrorResponse(
926
1009
  400,
927
1010
  "previous_response_not_found",
928
- "OpenAI forward continuation state is unavailable or expired; resend the full conversation without previous_response_id.",
1011
+ "Continuation state is unavailable or corrupt; resend the full conversation without previous_response_id.",
929
1012
  );
930
1013
  }
931
1014
 
932
1015
  if (hasUnexpandedPreviousResponse) {
933
- const continuationProvider = resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire);
1016
+ const continuationProvider = resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire, route.staticPolicy);
934
1017
  // Can the DESTINATION see the history this process failed to restore? Only the native
935
1018
  // Responses passthrough can: it forwards previous_response_id to a backend that stored the
936
1019
  // chain. Every translated wire rebuilds the conversation from this request's input alone —
@@ -951,7 +1034,7 @@ export async function prepareResponsesRequest(
951
1034
  return formatErrorResponse(
952
1035
  400,
953
1036
  "previous_response_not_found",
954
- "Routed continuation requires unavailable local history; resend the full conversation without previous_response_id.",
1037
+ "Continuation state is unavailable or corrupt; resend the full conversation without previous_response_id.",
955
1038
  );
956
1039
  }
957
1040
  }
@@ -971,6 +1054,14 @@ export async function prepareResponsesRequest(
971
1054
  inboundTransport: options.inboundTransport,
972
1055
  claudeGoAffinity: options.claudeGoAffinity,
973
1056
  });
1057
+ // Normalization is the last thing that can move the destination: resolving an
1058
+ // OpenAI virtual model rewrites route.modelId to the wire id that will
1059
+ // actually be billed. A scope checked only before this would authorize the
1060
+ // public selector and send the wire model, so the settled route is checked
1061
+ // once more here.
1062
+ if (!routeAllowedByScope(admissionScope, route)) {
1063
+ return admissionModelDeniedResponse(new AdmissionModelDeniedError(inboundSelector, route));
1064
+ }
974
1065
  // Attribute local auth/cooldown failures to the public selector too; exact auth may fail before
975
1066
  // the normal post-resolution provider label is assigned.
976
1067
  if (route.codexAccountNamespace) {
@@ -109,8 +109,12 @@ export function createResponsesSendBudget(
109
109
  const noteAdapterPhysicalSend = (
110
110
  inputTokens: number | undefined,
111
111
  send: { ordinal: number; recovery?: AttemptRecoveryKind },
112
+ options: { readonly includeFirst?: boolean } = {},
112
113
  ): void => {
113
- if (send.ordinal <= 1) return;
114
+ // Ordinal 1 is skipped because the caller normally records it before dispatch. An adapter
115
+ // that reports every send asks for it to be counted here instead, so that the first send is
116
+ // logged where it actually happens rather than before admission could still refuse it.
117
+ if (send.ordinal <= 1 && options.includeFirst !== true) return;
114
118
  noteAttemptSend(logCtx.activeAttempt, inputTokens, send.recovery);
115
119
  };
116
120
  /**
@@ -135,6 +139,16 @@ export function createResponsesSendBudget(
135
139
  */
136
140
  const sendBudgetExhausted = (cap: number = TRANSIENT_RETRY_MAX_ATTEMPTS): boolean =>
137
141
  remainingTransientSendBudget(cap) === 0;
142
+ /**
143
+ * Spend one operator-granted replacement for an ambiguous failure of THIS logical request.
144
+ *
145
+ * The counter is the execution budget's, so a combo child that derives its own scope draws on
146
+ * the same grant. A budget that predates it -- a stub, or a caller that passed the narrow
147
+ * holder -- cannot grant anything, and refusing is the fail-closed answer for a send whose
148
+ * upstream state is unknown.
149
+ */
150
+ const claimAmbiguousResend = (limit: number): boolean =>
151
+ isRequestExecutionBudget(sendBudget) && sendBudget.claimAmbiguousResend?.(limit) === true;
138
152
  /**
139
153
  * A credential hop reserves the send its own replay will make, and that replay is a recovery
140
154
  * leg. The leg must SPEND the hop's reservation instead of taking a second one: the
@@ -259,6 +273,7 @@ export function createResponsesSendBudget(
259
273
  noteAdapterPhysicalSend,
260
274
  noteAdapterRecoveryWithheld,
261
275
  sendBudgetExhausted,
276
+ claimAmbiguousResend,
262
277
  get pendingHopPermit(): SingleUseDispatchPermit | undefined {
263
278
  return pendingHopPermit;
264
279
  },
@@ -297,6 +312,7 @@ function adapterDispatchBudgetView(
297
312
  get targetTransitions(): number { return budget.targetTransitions; },
298
313
  get lastTargetKey(): string | undefined { return budget.lastTargetKey; },
299
314
  remainingBaseSends: (cap: number): number => budget.remainingBaseSends(cap),
315
+ claimAmbiguousResend: (limit: number): boolean => budget.claimAmbiguousResend?.(limit) === true,
300
316
  reserveDispatch(intent: DispatchIntent): DispatchDecision {
301
317
  // A dispatch whose upstream state is unknown is refused on its own merits. A hop that
302
318
  // already paid does not make an unsafe replay safe, so that check stays with the budget.
@@ -50,7 +50,7 @@ export async function prepareResponsesSidecarAuth(
50
50
  let openAiSidecar: ResolvedOpenAiForwardSidecar | undefined;
51
51
  const visionDescribeTerminal = options.visionDescribeTerminal === true;
52
52
  const routedCompaction = parsed._compactionRequest === true
53
- && !isCanonicalOpenAiForwardProvider(route.provider);
53
+ && (!isCanonicalOpenAiForwardProvider(route.provider) || parsed._portableCompaction === true);
54
54
  const needsOpenAiVision = !visionDescribeTerminal
55
55
  && shouldResolveOpenAiVisionSidecar(config, route.provider, route.modelId, parsed, route.providerName);
56
56
  const needsOpenAiSearch = !routedCompaction && !transportState.adapter.runTurn
@@ -64,6 +64,7 @@ import {
64
64
  recordKeyAttemptUsage,
65
65
  } from "../request-log";
66
66
  import type { AttemptRecoveryKind } from "../../usage/log";
67
+ import { bindAttemptDeliveryRecorder } from "../../usage/attempt-delivery";
67
68
  import { resolvePassiveRouteSubjectId } from "../passive-route-linker";
68
69
 
69
70
  /** Owns live credential selection and adapter bindings for one request. */
@@ -298,15 +299,25 @@ export async function prepareResponsesTransport(
298
299
  recordKeyAttemptUsage(logCtx, event.usage);
299
300
  }
300
301
  };
302
+ // Counted at the one seam every adapter parse passes, and counted for EVERY event rather
303
+ // than only usage-bearing ones: the number this pairs with is the frame count the client
304
+ // transport relayed, and a difference between the two is the loss signal (#3983). Reading
305
+ // the current attempt through logCtx rather than capturing one keeps the count with the
306
+ // attempt that is live when the event arrives, across a mid-request attempt rotation.
307
+ const delivery = bindAttemptDeliveryRecorder(translatorBudget, () => logCtx.activeAttempt);
308
+ const observeEvent = (event: AdapterEvent, response: object): void => {
309
+ delivery.noteAdapterEvent();
310
+ observeUsage(event, response);
311
+ };
301
312
  const parseStream = resolved.parseStream.bind(resolved);
302
313
  resolved.parseStream = async function* (...args) {
303
- for await (const event of parseStream(...args)) { observeUsage(event, args[0]); yield event; }
314
+ for await (const event of parseStream(...args)) { observeEvent(event, args[0]); yield event; }
304
315
  };
305
316
  if (resolved.parseResponse) {
306
317
  const parseResponse = resolved.parseResponse.bind(resolved);
307
318
  resolved.parseResponse = async (...args) => {
308
319
  const events = await parseResponse(...args);
309
- events.forEach(event => observeUsage(event, args[0]));
320
+ events.forEach(event => observeEvent(event, args[0]));
310
321
  return events;
311
322
  };
312
323
  }
@@ -321,7 +332,7 @@ export async function prepareResponsesTransport(
321
332
  const runTurn = resolved.runTurn.bind(resolved);
322
333
  rawRunTurns.set(resolved, (requestParsed, incoming, emit) => {
323
334
  const response = {};
324
- return runTurn(requestParsed, incoming, event => { observeUsage(event, response); emit(event); });
335
+ return runTurn(requestParsed, incoming, event => { observeEvent(event, response); emit(event); });
325
336
  });
326
337
  resolved.runTurn = (requestParsed, incoming, emit) => runSelectedTurn(resolved, requestParsed, incoming, emit);
327
338
  }
@@ -338,7 +349,7 @@ export async function prepareResponsesTransport(
338
349
  route.provider = current;
339
350
  }
340
351
  adapter = activeAdapter = runTurnAdapter = resolveSelectionAdapter(
341
- resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire),
352
+ resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire, route.staticPolicy),
342
353
  );
343
354
  invalidateSameTargetRequest();
344
355
  return adapter;
@@ -409,7 +420,16 @@ export async function prepareResponsesTransport(
409
420
  // Either way the send crosses the physical boundary, so the connection policy is
410
421
  // applied around whichever implementation was just selected (#4992).
411
422
  commitKeyAttemptSend();
412
- const response = await sendWithConnectionPolicy(fetchImpl, destination, { ...dispatchInit, redirect: "manual" });
423
+ // The binding travels with the send, so a rebuilt request resolves its provider route
424
+ // against the destination it is actually going to rather than the one this dispatch
425
+ // started with. Account reselection can move the upstream host, which would otherwise
426
+ // apply a host-scoped decision to a different host.
427
+ const response = await sendWithConnectionPolicy(
428
+ fetchImpl,
429
+ destination,
430
+ { ...dispatchInit, redirect: "manual" },
431
+ { providerName: route.providerName, provider: route.provider },
432
+ );
413
433
  if (!response.ok) await recordKeyAttemptFailure(logCtx, response, dispatchInit.signal ?? options.abortSignal);
414
434
  // Observe each physical response before retries replace it. The binding belongs to
415
435
  // this dispatch, so a manual switch cannot file A's headers against B. Header
@@ -619,7 +639,7 @@ export async function prepareResponsesTransport(
619
639
  ? resolveCopilotApiBaseUrl(sentOAuthSnapshot?.apiBaseUrl)
620
640
  : undefined,
621
641
  );
622
- let adapterProvider = resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire);
642
+ let adapterProvider = resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire, route.staticPolicy);
623
643
  const stripClaudeMainAuth = options.stripClaudeMainAuthForNoncanonicalForward === true
624
644
  && !isCanonicalOpenAiForwardProvider(adapterProvider)
625
645
  && ((adapterProvider.adapter === "openai-responses" && adapterProvider.authMode === "forward")
@@ -0,0 +1,108 @@
1
+ /**
2
+ * The operator opt-in that overrides the stage table's refusal, and the one request-wide
3
+ * allowance both ambiguous stages claim from.
4
+ *
5
+ * `resendPermission` answers `refused-ambiguous` for a native Responses send that died with
6
+ * the caller having observed nothing -- before the response head, or after it while the SSE
7
+ * body carried only control events. This module is the one place that answer is overridden,
8
+ * and it is narrow on three axes at once: the provider has to opt in, the request has to be
9
+ * one whose second send cannot do more than run the same inference again, and the whole
10
+ * logical request gets a fixed number of replacements no matter how many legs ask.
11
+ *
12
+ * The judgment is made on the inbound body the client sent, which is already parsed. It is
13
+ * conservative for the outbound request: the proxy expands `previous_response_id` and lowers
14
+ * hosted tools into client execution, so every hazard that reaches the wire was visible here,
15
+ * and a hazard visible here may already have been removed. A cheap fail-closed answer beats
16
+ * re-parsing a multi-megabyte outbound body on every send.
17
+ */
18
+ import type { OcxProviderConfig } from "../../types";
19
+ import { resetReplayPolicyFor } from "../../providers/key-failover";
20
+ import type { AmbiguousResendAllowance } from "../../lib/request-resend-gate";
21
+
22
+ /** Input items a client owns end to end: replaying them re-runs nothing but the model. */
23
+ const CLIENT_INPUT_ITEM_TYPES: ReadonlySet<string> = new Set([
24
+ "message", "reasoning", "compaction",
25
+ "function_call", "function_call_output",
26
+ "custom_tool_call", "custom_tool_call_output",
27
+ "tool_search_call",
28
+ ]);
29
+ const MESSAGE_ROLES: ReadonlySet<string> = new Set(["user", "assistant", "system", "developer"]);
30
+ /** Bounded traversal: a catalog is operator data, not a reason to walk forever. */
31
+ const MAX_TOOL_ENTRIES = 4096;
32
+ const MAX_TOOL_DEPTH = 4;
33
+
34
+ function record(value: unknown): value is Record<string, unknown> {
35
+ return value !== null && typeof value === "object" && !Array.isArray(value);
36
+ }
37
+
38
+ /**
39
+ * True when every tool in the catalog is executed by the client. Hosted tools (`web_search`,
40
+ * `mcp`, `code_interpreter`, ...) run on the origin during the turn, so an unknown or hosted
41
+ * type fails the whole catalog rather than being skipped: a tool this proxy does not
42
+ * recognise is a tool it cannot vouch for.
43
+ */
44
+ function clientExecutedTools(tools: unknown, budget: { remaining: number }, depth = 0): boolean {
45
+ if (!Array.isArray(tools) || depth > MAX_TOOL_DEPTH) return false;
46
+ return tools.every(tool => {
47
+ budget.remaining -= 1;
48
+ if (budget.remaining < 0 || !record(tool)) return false;
49
+ if (tool.type === "function" || tool.type === "custom") return true;
50
+ if (tool.type === "tool_search") return tool.execution === "client";
51
+ return tool.type === "namespace" && typeof tool.name === "string"
52
+ && clientExecutedTools(tool.tools, budget, depth + 1);
53
+ });
54
+ }
55
+
56
+ /**
57
+ * A Responses body whose second send can only repeat the inference: nothing stored, no
58
+ * server-side continuation state, complete input, and only client-executed tools. Deferred
59
+ * tool declarations inside `input` are checked by the same rule as the root catalog, so a
60
+ * hosted tool cannot ride in through `additional_tools` or a `tool_search_output`.
61
+ */
62
+ export function selfContainedResponsesBody(body: unknown): boolean {
63
+ if (!record(body)) return false;
64
+ if (body.store !== false || body.background === true) return false;
65
+ if (body.previous_response_id != null || body.conversation != null || Object.hasOwn(body, "stream_id")) return false;
66
+ const input = body.input;
67
+ if (typeof input !== "string" && !Array.isArray(input)) return false;
68
+ const budget = { remaining: MAX_TOOL_ENTRIES };
69
+ if (body.tools !== undefined && !clientExecutedTools(body.tools, budget)) return false;
70
+ if (typeof input === "string") return true;
71
+ return input.every(item => {
72
+ if (!record(item)) return false;
73
+ if (item.type === "additional_tools" || item.type === "tool_search_output") {
74
+ return clientExecutedTools(item.tools, budget);
75
+ }
76
+ if (item.type === undefined) return typeof item.role === "string" && MESSAGE_ROLES.has(item.role);
77
+ return typeof item.type === "string" && CLIENT_INPUT_ITEM_TYPES.has(item.type);
78
+ });
79
+ }
80
+
81
+ /**
82
+ * The allowance for ONE logical request, or nothing when the provider did not opt in.
83
+ *
84
+ * Built once per request and handed to every leg. `claim` spends the request's counter, which
85
+ * lives on the execution budget and is therefore shared with a combo child's derived scope --
86
+ * that sharing is the reason the pre-header helper takes a callback instead of a number.
87
+ *
88
+ * The body judgment is carried rather than applied here, because the gate has to be able to
89
+ * say WHY it refused: "the operator granted nothing" and "this request cannot be replayed" are
90
+ * different operator problems, and folding them together is what made the old refusal a single
91
+ * undifferentiated no.
92
+ *
93
+ * `selfContained` is a predicate rather than a body, and the getter below is why: a provider
94
+ * that never opted in must not pay to walk the input array, and the caller memoizes one answer
95
+ * across every leg of the request.
96
+ */
97
+ export function ambiguousResendAllowanceFor(
98
+ provider: Pick<OcxProviderConfig, "retryOnReset">,
99
+ requestIsSelfContained: () => boolean,
100
+ claim: (limit: number) => boolean,
101
+ ): AmbiguousResendAllowance | undefined {
102
+ const policy = resetReplayPolicyFor(provider);
103
+ if (policy === null) return undefined;
104
+ return {
105
+ get selfContained(): boolean { return requestIsSelfContained(); },
106
+ claim: () => claim(policy.replacements),
107
+ };
108
+ }
@@ -79,7 +79,11 @@ export async function executeResponsesRunTurn(
79
79
  >,
80
80
  sendBudgetState: Pick<
81
81
  ResponsesSendBudget,
82
- "adapterDispatchBudget" | "reserveCredentialHop" | "pendingHopPermit"
82
+ | "adapterDispatchBudget"
83
+ | "noteAdapterPhysicalSend"
84
+ | "noteAdapterRecoveryWithheld"
85
+ | "reserveCredentialHop"
86
+ | "pendingHopPermit"
83
87
  >,
84
88
  completionPolicy: Pick<ResponsesCompletionPolicy, "emptyCompletionGuardEnabled">,
85
89
  ): Promise<Response> {
@@ -100,7 +104,12 @@ export async function executeResponsesRunTurn(
100
104
  rememberKiroDeliveredFinalAnswer,
101
105
  responseStateOptions,
102
106
  } = requestState;
103
- const { adapterDispatchBudget, reserveCredentialHop } = sendBudgetState;
107
+ const {
108
+ adapterDispatchBudget,
109
+ noteAdapterPhysicalSend,
110
+ noteAdapterRecoveryWithheld,
111
+ reserveCredentialHop,
112
+ } = sendBudgetState;
104
113
  const { emptyCompletionGuardEnabled } = completionPolicy;
105
114
  const {
106
115
  cancelResponseCompletion,
@@ -146,7 +155,11 @@ export async function executeResponsesRunTurn(
146
155
  await waitForProviderRequestSlot(route.providerName, route.provider, route.modelId, runTurnAbort.signal);
147
156
  }
148
157
  await refreshRunTurnSelection();
149
- transportState.noteRoutedAttemptSend(logCtx.usageLogInputTokens, recovery);
158
+ // An adapter that reports its own sends accounts for the first one at the boundary that
159
+ // dispatches it. Logging here would claim a send that the adapter's own budget can still
160
+ // refuse, which is exactly what happens once earlier recovery has spent the allowance.
161
+ const reportsOwnSends = transportState.runTurnAdapter.reportsPhysicalSends === true;
162
+ if (!reportsOwnSends) transportState.noteRoutedAttemptSend(logCtx.usageLogInputTokens, recovery);
150
163
  const runTurnProviderFetch = providerFetch(
151
164
  route.provider,
152
165
  options.codexWsRuntimeIdentity,
@@ -169,6 +182,14 @@ export async function executeResponsesRunTurn(
169
182
  // The only way the request budget reaches a transport the adapter owns. Without it
170
183
  // a Cursor turn's inner ladder was three physical sends the cap read as one.
171
184
  ...(adapterDispatchBudget ? { sendBudget: adapterDispatchBudget } : {}),
185
+ onPhysicalSend: send => noteAdapterPhysicalSend(
186
+ logCtx.usageLogInputTokens,
187
+ // The attempt's own recovery kind still labels its first send when the adapter
188
+ // does not supply one of its own.
189
+ { ...send, ...(send.recovery ?? recovery ? { recovery: send.recovery ?? recovery } : {}) },
190
+ { includeFirst: reportsOwnSends },
191
+ ),
192
+ onRecoveryWithheld: noteAdapterRecoveryWithheld,
172
193
  },
173
194
  targetQueue.push,
174
195
  );
@@ -273,6 +294,7 @@ export async function executeResponsesRunTurn(
273
294
  route.modelId,
274
295
  route.provider,
275
296
  inboundWire,
297
+ route.staticPolicy,
276
298
  );
277
299
  const rotatedAdapter = resolveSelectionAdapter(rotatedProvider, config.cacheRetention);
278
300
  if (!rotatedAdapter.runTurn) {
@@ -17,6 +17,7 @@ import {
17
17
  clampImageMaxRounds,
18
18
  } from "../../images";
19
19
  import type { ProviderAdapter } from "../../adapters/base";
20
+ import type { OcxParsedRequest } from "../../types";
20
21
  import { rotateProviderTransportOn429, rateLimitRetryPolicyFor } from "../../providers/key-failover";
21
22
  import {
22
23
  GENERIC_OAUTH_MAX_FAILOVERS_PER_REQUEST,
@@ -155,6 +156,7 @@ export async function executeResponsesSidecars(
155
156
  const rotateSidecarProviderOn429 = async (
156
157
  retryAfter: string | null,
157
158
  responseHeaders?: Headers,
159
+ retryParsed?: OcxParsedRequest,
158
160
  ): Promise<ProviderAdapter | null> => {
159
161
  const rotated = rotateProviderTransportOn429(config, route.providerName, route.provider, {
160
162
  retryAfter,
@@ -196,7 +198,7 @@ export async function executeResponsesSidecars(
196
198
  try {
197
199
  const snapshot = await failoverAccountSnapshot(route.providerName, nextAccountId);
198
200
  transportState.genericFailovers += 1;
199
- if (!await applyFailoverSnapshot(snapshot)) {
201
+ if (!await applyFailoverSnapshot(snapshot, retryParsed)) {
200
202
  hop.permit?.release();
201
203
  return null;
202
204
  }
@@ -254,9 +256,22 @@ export async function executeResponsesSidecars(
254
256
  return null;
255
257
  }
256
258
  const rotatedAdapter = resolveSelectionAdapter(
257
- resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire),
259
+ resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire, route.staticPolicy),
258
260
  config.cacheRetention,
259
261
  );
262
+ // The sidecar loops build each attempt from an iteration-local shallow copy of parsed, so a
263
+ // rebind that lands only on the outer request never reaches the wire: the retry would pair
264
+ // the new bearer with the previous account's Kiro routing metadata and continuation identity.
265
+ // Rebind the exact request the rotated adapter will be built from (same pattern as the
266
+ // continuation loop's nextParsed); when the hook ran without one, the outer bind below stands.
267
+ if (retryParsed && retryParsed !== parsed) {
268
+ bindRouteReasoningReplayScope({
269
+ parsed: retryParsed,
270
+ providerName: route.providerName,
271
+ provider: route.provider,
272
+ adapterName: rotatedAdapter.name,
273
+ });
274
+ }
260
275
  bindRouteReasoningReplayScope({
261
276
  parsed,
262
277
  providerName: route.providerName,