@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,109 @@
1
+ import type { OcxConfig } from "../../types";
2
+ import { resolveComboId } from "../../combos";
3
+ import { isBareOpenAiFamilyModel } from "../../router";
4
+ import { NATIVE_RESERVE_MODEL } from "../../codex/catalog/native-models";
5
+ import {
6
+ getMainAccountHardLockStatus,
7
+ resolveMainAccountHardLockFallback,
8
+ } from "../../codex/main-account-hard-lock";
9
+ import { codexQuotaScopeForModel } from "../../codex/routing/health-store";
10
+ import { getEligiblePoolAccounts } from "../../codex/routing/selection";
11
+ import { OPENAI_CODEX_PROVIDER_ID, isCanonicalOpenAiForwardProvider } from "../../providers/openai-tiers";
12
+ import { providerCodexAccountMode } from "../../providers/registry";
13
+
14
+ function isConfiguredComboSelector(config: OcxConfig, modelId: string): boolean {
15
+ const comboId = resolveComboId(config, modelId);
16
+ return Boolean(comboId && Object.hasOwn(config.combos ?? {}, comboId));
17
+ }
18
+
19
+ function targetsBlockedCanonicalOpenAi(config: OcxConfig, modelId: string): boolean {
20
+ if (isBareOpenAiFamilyModel(modelId) && !isConfiguredComboSelector(config, modelId)) return true;
21
+ const slash = modelId.indexOf("/");
22
+ if (slash <= 0) return false;
23
+ const providerName = modelId.slice(0, slash);
24
+ const rest = modelId.slice(slash + 1);
25
+ if (providerName !== OPENAI_CODEX_PROVIDER_ID || !isBareOpenAiFamilyModel(rest)) return false;
26
+ const provider = config.providers?.[OPENAI_CODEX_PROVIDER_ID];
27
+ return !provider || isCanonicalOpenAiForwardProvider({
28
+ ...provider,
29
+ ...(provider.authMode === undefined ? { authMode: "forward" as const } : {}),
30
+ });
31
+ }
32
+
33
+ /**
34
+ * What one rewritten request carries so the log can show the model the client asked for
35
+ * while the pipeline routes the fallback. The rewrite happens before combo dispatch reads
36
+ * `model`, so the rest of the pipeline sees an ordinary request for the target selector.
37
+ */
38
+ export interface MainHardLockFallbackOverride {
39
+ sourceModel: string;
40
+ targetModel: string;
41
+ }
42
+
43
+ function rawModelOf(body: unknown): { raw: Record<string, unknown>; model: string } | undefined {
44
+ if (!body || typeof body !== "object" || Array.isArray(body)) return undefined;
45
+ const raw = body as Record<string, unknown>;
46
+ return typeof raw.model === "string" && raw.model.trim().length > 0
47
+ ? { raw, model: raw.model }
48
+ : undefined;
49
+ }
50
+
51
+ /**
52
+ * Extended main-account hard-lock settings: when the lock blocks and no Codex account can
53
+ * serve the request, rewrite it to the configured fallback model instead of letting the
54
+ * dispatch refuse.
55
+ *
56
+ * Every guard bounds the rewrite to exactly the traffic the lock would otherwise fail, so
57
+ * the setting can never widen what the lock protects:
58
+ *
59
+ * - The lock must be enabled AND reading `blocked` right now — `ready`, `unknown` and the
60
+ * explicit opt-out all admit, and the release is this same observation, so the reroute
61
+ * ends on its own the moment the blocking windows drop below their thresholds.
62
+ * - `getEligiblePoolAccounts` is the selection choke point the dispatch itself consults,
63
+ * so "the pool can serve it" is one shared answer, not a second opinion: a usable pool
64
+ * account (or any eligible account) keeps the request on Codex, and a pool drained to
65
+ * empty is what makes the fallback the request's only way through. The main account is
66
+ * already excluded there while blocked.
67
+ * - Only bare Codex-native selectors qualify. Provider-qualified rows, combos, account
68
+ * namespaces and policy routes name their own destination on purpose; rewriting those
69
+ * would override an explicit choice, so they keep the lock's refusal.
70
+ * - A Codex-native fallback target is refused outright: rerouting gpt-* to gpt-* would
71
+ * re-enter the same refusal, and the honest behavior is the refusal, not a loop.
72
+ * - Luna Reserve keeps its own admission policy (`assertReserveAdmission`); its helper
73
+ * traffic is not operator chat and must not silently change models.
74
+ */
75
+ export function applyMainHardLockFallbackOverride(
76
+ body: unknown,
77
+ config: OcxConfig,
78
+ now = Date.now(),
79
+ ): MainHardLockFallbackOverride | null {
80
+ const parsed = rawModelOf(body);
81
+ if (!parsed) return null;
82
+ const fallback = resolveMainAccountHardLockFallback(config);
83
+ if (!fallback) return null;
84
+ if (targetsBlockedCanonicalOpenAi(config, fallback.model)) return null;
85
+ if (!isBareOpenAiFamilyModel(parsed.model)
86
+ || parsed.model === NATIVE_RESERVE_MODEL
87
+ || isConfiguredComboSelector(config, parsed.model)) return null;
88
+ if (getMainAccountHardLockStatus(config, now).state !== "blocked") return null;
89
+ const accountMode = providerCodexAccountMode(
90
+ OPENAI_CODEX_PROVIDER_ID,
91
+ config.providers?.[OPENAI_CODEX_PROVIDER_ID],
92
+ );
93
+ if (accountMode !== "direct"
94
+ && getEligiblePoolAccounts(config, undefined, now, codexQuotaScopeForModel(parsed.model)).length > 0) {
95
+ return null;
96
+ }
97
+
98
+ parsed.raw.model = fallback.model;
99
+ if (fallback.reasoningEffort !== undefined) {
100
+ const reasoning = parsed.raw.reasoning;
101
+ parsed.raw.reasoning = {
102
+ ...(reasoning && typeof reasoning === "object" && !Array.isArray(reasoning)
103
+ ? reasoning as Record<string, unknown>
104
+ : {}),
105
+ effort: fallback.reasoningEffort,
106
+ };
107
+ }
108
+ return { sourceModel: parsed.model, targetModel: fallback.model };
109
+ }
@@ -4,6 +4,7 @@ import type { NativeSteeringReplayObserver } from "./native-steering-replay";
4
4
  import type { ProviderExecutedCallType } from "../responses-undeclared-tool-guard";
5
5
 
6
6
  import { isInjectionRequest } from "./native-injection-protocol";
7
+ import { registerProviderResponseProvenance } from "../../providers/request-pacing";
7
8
 
8
9
  /** Shared transport ownership, not a shared steer/inject protocol state machine. */
9
10
  export interface NativeResponseControl {
@@ -25,9 +26,10 @@ export interface NativeResponseControl {
25
26
 
26
27
  export const OPENAI_API_RESPONSES_URL = "https://api.openai.com/v1/responses";
27
28
 
28
- const nativeControlResponses = new WeakSet<Response>();
29
+ const nativeControlResponses = new WeakMap<Response, true>();
30
+ registerProviderResponseProvenance(nativeControlResponses);
29
31
  /** Mark the exact response for multi-response delivery without serializing a wire field. */
30
- export function markNativeControlResponse(response: Response): Response { nativeControlResponses.add(response); return response; }
32
+ export function markNativeControlResponse(response: Response): Response { nativeControlResponses.set(response, true); return response; }
31
33
  /** Recognize a marked native response by identity, not by caller-controlled content. */
32
34
  export function isNativeControlResponse(response: Response): boolean { return nativeControlResponses.has(response); }
33
35
 
@@ -58,6 +58,7 @@ import {
58
58
  hasResponsesSnapshotRepair,
59
59
  createResponsesSnapshotBlockRewrite,
60
60
  } from "../responses-snapshot-repair";
61
+ import { rewriteResponsesIncompleteReasonJson } from "../../responses/incomplete-reason";
61
62
  import { createResponsesModelPayloadRewrite, rewriteResponsesModelJson } from "../responses-model-rewrite";
62
63
  import { createImageGenCallRestoreRewrite, restoreImageGenCallsInJson } from "../responses-image-gen-repair";
63
64
  import {
@@ -768,6 +769,7 @@ export async function deliverPassthroughResponse(
768
769
  : undefined;
769
770
  // Compose opt-in payload rewrites into one parse/stringify pass (image-gen restore first).
770
771
  const payloadRewrites = [
772
+ rewriteResponsesIncompleteReasonJson,
771
773
  createImageGenCallRestoreRewrite(imageGenCallAliases),
772
774
  // #3217: a call whose namespace repeats its own name is unroutable in codex-rs.
773
775
  createSelfNamedToolCallNamespaceScrubRewrite(selfNamedNamespaceScrubAuthorization),
@@ -1317,7 +1319,7 @@ export async function deliverPassthroughResponse(
1317
1319
  const modelRewritten = parsed._responseModelId !== undefined && parsed._responseModelId !== parsed.modelId
1318
1320
  ? rewriteResponsesModelJson(repaired, parsed._responseModelId)
1319
1321
  : repaired;
1320
- return modelRewritten;
1322
+ return rewriteResponsesIncompleteReasonJson(modelRewritten);
1321
1323
  })();
1322
1324
  if (plaintextV2RestoreFailed) {
1323
1325
  return formatErrorResponse(502, "upstream_error", PLAINTEXT_V2_AGENT_MESSAGE_RESTORE_OVERFLOW_MESSAGE);
@@ -1,4 +1,6 @@
1
1
  import { spendLedgerStorageErrorResponse } from "./spend-storage-error";
2
+ import { requestPacingOverloadResponse } from "./pacing-overload";
3
+ import { recordDistributedResponseFailure } from "../../providers/api-key-response-ownership";
2
4
  import { createSteeringSettingsNormalizer } from "./native-steering-policy";
3
5
  import { nativeResponseControlEligible } from "./native-response-control";
4
6
  import { NativeInjectionReplay } from "./native-injection-replay";
@@ -141,7 +143,7 @@ import { resolveWireProtocolOverride } from "../adapter-resolve";
141
143
  import { refreshPoolForwardAuth, refreshNativeMainForwardAuth, withClaudeNativeSession } from "./core-auth";
142
144
  import { bindRouteReasoningReplayScope } from "./core-replay";
143
145
  import type { OAuthAccessSnapshot } from "../../oauth";
144
- import { OAuthAccountPausedError, OAuthLoginRequiredError, publicOAuthAuthenticationErrorMessage } from "../../oauth";
146
+ import { OAuthAccountPausedError, OAuthLoginRequiredError, OAuthTokenRefreshBusyError, publicOAuthAuthenticationErrorMessage } from "../../oauth";
145
147
  import { AnthropicAccountCooldownError } from "../../oauth/anthropic-routing";
146
148
  import { resolveCopilotApiBaseUrl } from "../../oauth/github-copilot";
147
149
  import {
@@ -154,7 +156,7 @@ import { captureCodexAffinityDiagnostic } from "../../codex/affinity-debug";
154
156
  import { ACCOUNT_GATED_NATIVE_OPENAI_MODELS } from "../../codex/catalog/native-models";
155
157
  import {
156
158
  attemptOpaqueBlobRecovery,
157
- isEncryptedFunctionOutputRejection,
159
+ isEncryptedFunctionOutputRejection, retainedReasoningStreamCandidate, retainedReasoningStreamRejection,
158
160
  outboundResponsesBodyCarriesEncryptedFunctionOutput,
159
161
  resetStreamedOpaqueBlobLogContext,
160
162
  consoleGoUploadRejectionBody,
@@ -742,7 +744,9 @@ export async function preparePassthroughExchange(
742
744
  const stage = readCodexWsStage(response);
743
745
  if (stage && logCtx.activeAttempt) logCtx.activeAttempt.codexWsStage = stage;
744
746
  };
747
+ let upstreamResponse: Response, lastDistributedResponse: Response | undefined;
745
748
  const adoptObservedResponse = <T extends Response>(response: T): T => {
749
+ if (!isNonReplayableResponse(response)) lastDistributedResponse = response.status === 401 || response.status === 429 ? response : undefined;
746
750
  settleObservedHostResponse();
747
751
  adoptCodexWsStage(response);
748
752
  return response;
@@ -759,7 +763,6 @@ export async function preparePassthroughExchange(
759
763
  const upstream = new AbortController();
760
764
  const cleanupUpstreamAbort = linkAbortSignal(upstream, options.abortSignal ?? req.signal);
761
765
  const connectMs = config.connectTimeoutMs ?? 200_000;
762
- let upstreamResponse: Response;
763
766
  /**
764
767
  * This leg's transient-5xx ladder cap, from the provider's own `transientRetryOn5xx`.
765
768
  *
@@ -892,7 +895,6 @@ export async function preparePassthroughExchange(
892
895
  ): Response | undefined => {
893
896
  const result = checkOutboundBodySize(builtRequest.body, config.maxUpstreamBodyBytes);
894
897
  if (result.admitted) return undefined;
895
-
896
898
  // This returns before the surrounding fetch/finally owns the observation, so release
897
899
  // it here or one refused body holds translator budget for the process lifetime.
898
900
  builtRequest.releaseBodyObservation?.();
@@ -930,6 +932,7 @@ export async function preparePassthroughExchange(
930
932
  releaseCodexAuthContextProbeLease(admissionState.authCtx);
931
933
  return clientCancelledResponse();
932
934
  }
935
+ if (lastDistributedResponse) recordDistributedResponseFailure(lastDistributedResponse, config, route.providerName, options.abortSignal);
933
936
  // A budget refusal is a proxy decision, not an upstream fault. Reporting it as
934
937
  // 502 upstream_error would blame the provider for a limit this process applied, and
935
938
  // would record a fake reachability failure against the account's health.
@@ -945,12 +948,12 @@ export async function preparePassthroughExchange(
945
948
  bindRefreshedAdapter(refusal.adapter);
946
949
  return handoffToChat(refusal.adapter);
947
950
  }
948
- const copilotRefusal = copilotRefusalResponse(refusal);
949
- if (copilotRefusal) {
951
+ const dispatchRefusal = requestPacingOverloadResponse(refusal) ?? copilotRefusalResponse(refusal);
952
+ if (dispatchRefusal) {
950
953
  releaseUpstreamHostAdmission(nativeHostState.lease);
951
954
  nativeHostState.lease = null;
952
955
  releaseCodexAuthContextProbeLease(admissionState.authCtx);
953
- return copilotRefusal;
956
+ return dispatchRefusal;
954
957
  }
955
958
  // Pacing may outlive the selected account's admission. No fetch occurred, so do
956
959
  // not turn an operator pause into a 502 or charge it to host/account health.
@@ -1206,7 +1209,6 @@ export async function preparePassthroughExchange(
1206
1209
 
1207
1210
  // Keep recovery kinds in sync with the generic `recovery:` loop below.
1208
1211
  passthroughRecovery: for (;;) {
1209
-
1210
1212
  if (
1211
1213
  upstreamResponse.status === 401
1212
1214
  && (admissionState.authCtx.kind === "main-pool" || admissionState.authCtx.kind === "pool")
@@ -1358,6 +1360,8 @@ export async function preparePassthroughExchange(
1358
1360
  } catch (err) {
1359
1361
  upstream.abort();
1360
1362
  releaseCodexAuthContextProbeLease(admissionState.authCtx);
1363
+ if (options.abortSignal?.aborted || req.signal.aborted) return clientCancelledResponse();
1364
+ if (err instanceof OAuthTokenRefreshBusyError) return formatErrorResponse(503, "server_error", publicOAuthAuthenticationErrorMessage(err), { retryAfter: "1" });
1361
1365
  return formatErrorResponse(401, "authentication_error", publicOAuthAuthenticationErrorMessage(err));
1362
1366
  }
1363
1367
  if (route.provider.googleMode === "cloud-code-assist" && !refreshed.projectId) {
@@ -1526,7 +1530,6 @@ export async function preparePassthroughExchange(
1526
1530
  )) noteAttemptRecoveryWithheld(logCtx.activeAttempt, "rotation-send-budget");
1527
1531
  }
1528
1532
  }
1529
-
1530
1533
  // Same-target 429 wait-and-retry (opt-in `retryOn429`) for key-auth providers on the
1531
1534
  // passthrough wire. This branch returns before the recovery loop below, so Responses-shaped
1532
1535
  // key-auth gateways (e.g. the built-in DeepSeek preset) would otherwise surface 429
@@ -1597,7 +1600,6 @@ export async function preparePassthroughExchange(
1597
1600
  return transportFailureResponse(err);
1598
1601
  }
1599
1602
  }
1600
-
1601
1603
  const captureAffinityResponse = (
1602
1604
  response: Response,
1603
1605
  captureAuthCtx: CodexAuthContext = admissionState.authCtx,
@@ -1619,8 +1621,8 @@ export async function preparePassthroughExchange(
1619
1621
  status: response.status,
1620
1622
  });
1621
1623
  };
1624
+ recordDistributedResponseFailure(lastDistributedResponse ?? upstreamResponse, config, route.providerName, options.abortSignal);
1622
1625
  captureAffinityResponse(upstreamResponse);
1623
-
1624
1626
  if (usesCodexForwardPoolAuth(admissionState.authCtx, route.provider)) {
1625
1627
  let poolRetryOutcome: number | undefined;
1626
1628
  // A success is the freshest evidence there is about this pair, and it outranks any earlier
@@ -1749,7 +1751,7 @@ export async function preparePassthroughExchange(
1749
1751
  && (recoveryContentType.includes("text/event-stream") || (!recoveryContentType && upstreamRequestsStream))
1750
1752
  && !opaqueBlobRecoveryGuard.attempted
1751
1753
  && !configuredTransientSendBudgetExhausted()
1752
- && outboundResponsesBodyCarriesEncryptedFunctionOutput(request.body);
1754
+ && (outboundResponsesBodyCarriesEncryptedFunctionOutput(request.body) || (!sendBudgetState.ambiguousResendSpent && retainedReasoningStreamCandidate(parsed, request.body)));
1753
1755
  if (streamedFunctionOutputCandidate) {
1754
1756
  const preflightLog: RequestLogContext = { model: logCtx.model, provider: logCtx.provider };
1755
1757
  const preflight = await preflightComboStreamResponse(upstreamResponse, preflightLog,
@@ -1762,7 +1764,7 @@ export async function preparePassthroughExchange(
1762
1764
  // intentionally understands only Responses error/message fields. Preserve only this
1763
1765
  // exact identity so the projected 502 can enter the existing single-shot recovery.
1764
1766
  if (decryptRejection) preflightLog.upstreamError = ENCRYPTED_FUNCTION_OUTPUT_REJECTION;
1765
- return decryptRejection;
1767
+ return decryptRejection || (!sendBudgetState.ambiguousResendSpent && retainedReasoningStreamRejection(payload, parsed, request.body, preflightLog));
1766
1768
  }, {
1767
1769
  allowMissingContentType: !recoveryContentType && upstreamRequestsStream,
1768
1770
  replayReadErrors: true,
@@ -17,7 +17,7 @@ import {
17
17
  unreadableEncryptedAgentTaskResponse,
18
18
  } from "./core-errors";
19
19
  import { parseSyntheticRowId } from "../fast-row";
20
- import { resolveComboId, comboIdFromRawBody, NoAvailableComboTargetsError } from "../../combos";
20
+ import { resolveComboId, comboIdFromRawBody, comboExclusionReasons, NoAvailableComboTargetsError, pickComboTarget } from "../../combos";
21
21
  import { INTERCEPT_TARGET_UNAVAILABLE_CODE, interceptTargetUnavailableResponse, resolveChosenTarget, resolveShadowCallTarget } from "./shadow-target-availability";
22
22
  import {
23
23
  MEMORY_MODEL_TARGET_UNAVAILABLE_CODE,
@@ -84,6 +84,7 @@ import {
84
84
  } from "../../router";
85
85
  import { evidenceFromBody } from "../../routing/request-evidence";
86
86
  import { OPENAI_CODEX_PROVIDER_ID, isCanonicalOpenAiForwardProvider } from "../../providers/openai-tiers";
87
+ import { resolveNativeReasoningRetention } from "../../config/schema/native-reasoning-retention";
87
88
  import { isThreadSpawnRequest } from "../effort-policy";
88
89
  import {
89
90
  resolveSubagentFallbackChain,
@@ -133,6 +134,7 @@ import {
133
134
  } from "../../codex/loopback-target";
134
135
  import { checkComboTargetInputAdmission, checkInputAdmission } from "./input-admission";
135
136
  import {
137
+ admissionMayUseMeteredComboTargets,
136
138
  admissionModelDeniedResponse,
137
139
  AdmissionModelDeniedError,
138
140
  assertRouteAllowedByScope,
@@ -140,6 +142,13 @@ import {
140
142
  routeAllowedByScope,
141
143
  } from "../admission-model-scope";
142
144
  import { nativeContextLimits } from "../../codex/catalog";
145
+ import type { ClientToolCatalogRestriction } from "../../lib/empty-tool-catalog";
146
+ import {
147
+ currentTurnWireToolCatalogBody,
148
+ hasExplicitEmptyToolCatalog,
149
+ collectDeclaredWireToolNames,
150
+ collectDeclaredBareCustomWireToolNames,
151
+ } from "../responses-undeclared-tool-guard";
143
152
  import { streamingContextOverflowResponse } from "./context-overflow";
144
153
  import {
145
154
  preAuthUpstreamHostCircuitKey,
@@ -149,6 +158,7 @@ import {
149
158
  } from "./core-codex-account";
150
159
  import { acquireUpstreamHostAdmission } from "../../codex/upstream-host-health";
151
160
  import { applyCompactionRoutingOverride, compactionRoutingKeepsProviderIdentity } from "./compaction-routing";
161
+ import { applyMainHardLockFallbackOverride } from "./main-hard-lock-routing";
152
162
  import { codexAuthContextLogLabel } from "../../codex/account-label";
153
163
  import {
154
164
  conversationStateBindingFromAuth,
@@ -286,14 +296,14 @@ export async function prepareResponsesRequest(
286
296
  effort: comboEffortRow.effort,
287
297
  };
288
298
  }
299
+ const isCompactionTrigger = body !== null && typeof body === "object" && !Array.isArray(body)
300
+ && Array.isArray((body as { input?: unknown }).input)
301
+ && ((body as { input: unknown[] }).input).some((item: unknown) =>
302
+ typeof item === "object" && item !== null && (item as { type?: string }).type === "compaction_trigger");
289
303
  // Compaction may send the last client-visible bare model after a combo switch.
290
304
  // Configured selectors take precedence; otherwise recall before combo dispatch (#3891).
291
305
  if (!concreteSelection && !options.compactionRoutingOverride && body && typeof body === "object" && !Array.isArray(body)) {
292
306
  const rawModel = (body as { model?: unknown }).model;
293
- const rawInput = (body as { input?: unknown }).input;
294
- const isCompactionTrigger = Array.isArray(rawInput)
295
- && rawInput.some((item: unknown) =>
296
- typeof item === "object" && item !== null && (item as { type?: string }).type === "compaction_trigger");
297
307
  if (typeof rawModel === "string" && !rawModel.includes("/") && isCompactionTrigger
298
308
  && !comboRows.fastRow && !comboEffortRow
299
309
  && !resolveComboId(config, rawModel)) {
@@ -322,6 +332,7 @@ export async function prepareResponsesRequest(
322
332
  }
323
333
  }
324
334
  let shadowCallIntercepted = false;
335
+ let shadowCallMatches = false;
325
336
  // Luna is also selectable by users/children. Both intercept sites use the same
326
337
  // per-turn provenance; WS frames must not inherit the handshake's classification.
327
338
  const threadSpawn = isThreadSpawnRequest(req.headers);
@@ -331,6 +342,7 @@ export async function prepareResponsesRequest(
331
342
  const rawShadowModel = (body as { model?: unknown }).model;
332
343
  if (shadowIntercept?.enabled && shadowIntercept.model && typeof rawShadowModel === "string"
333
344
  && isShadowSourceModel(rawShadowModel, shadowIntercept.sourceModels)) {
345
+ shadowCallMatches = true;
334
346
  const shadowComboId = resolveComboId(config, shadowIntercept.model);
335
347
  if (shadowComboId && Object.hasOwn(config.combos ?? {}, shadowComboId)) {
336
348
  shadowCallIntercepted = true;
@@ -344,6 +356,16 @@ export async function prepareResponsesRequest(
344
356
  }
345
357
  }
346
358
  }
359
+ // The hard-lock fallback owns ordinary turns only, and only after every more specific
360
+ // routing setting had its chance above: a shadow-intercepted, memory-phased, compaction
361
+ // or decision turn keeps its own destination, and an intercepted combo selector is
362
+ // already owned by the intercept. A spawned sub-agent turn names its model on purpose,
363
+ // the same reason the shadow intercept leaves it alone. Like the sites above it, this
364
+ // must run before comboIdFromRawBody reads `model`.
365
+ if (!concreteSelection && !options.compactionRoutingOverride && !isCompactionTrigger && !options.internalDecisionCall
366
+ && !shadowCallIntercepted && !shadowCallMatches && !memoryModelApplies && !threadSpawn && inboundWire === "responses") {
367
+ options.mainHardLockFallbackOverride = applyMainHardLockFallbackOverride(body, config);
368
+ }
347
369
  const comboId = !concreteSelection ? comboIdFromRawBody(body, config) : null;
348
370
  // A decision model may be a plain model or a non-JEV combo. A JEV combo here would recurse
349
371
  // into another decision, so the call is refused and the parent decision fails open.
@@ -430,15 +452,30 @@ export async function prepareResponsesRequest(
430
452
 
431
453
  let parsed: OcxParsedRequest;
432
454
  let toolBridgeMaps: ReturnType<typeof buildToolBridgeMaps>;
455
+ let clientToolCatalogRestriction: ClientToolCatalogRestriction | undefined;
433
456
  try {
434
457
  parsed = parseRequest(body);
435
458
  // Antigravity direct lane: the body carried only the request settings, so the parse above
436
459
  // produced them with an empty conversation; the conversation itself comes straight from the
437
460
  // client body. Inside this try so a decode error answers exactly like a parse error.
438
461
  if (options.directRequest) parsed = options.directRequest.decode(parsed);
462
+ parsed._nativeReasoningRetention = resolveNativeReasoningRetention(config);
463
+ // Capture before route rewrites or hosted sidecars can add tools; replayed catalogs are history.
464
+ // The direct settings body omits tools, including current legacy Chat functions. Recover
465
+ // their wire names only when this restriction needs a non-empty decoded catalog.
466
+ const catalogBody = options.directRequest && options.clientDeclaredEmptyToolCatalog === true && parsed.context.tools?.length
467
+ ? options.directRequest.bridgeConversation() : parsed._rawBody;
468
+ const currentCatalog = currentTurnWireToolCatalogBody(catalogBody, parsed._replayPrefixLen);
469
+ if (options.clientDeclaredEmptyToolCatalog === true || hasExplicitEmptyToolCatalog(currentCatalog)) {
470
+ clientToolCatalogRestriction = {
471
+ declaredToolNames: collectDeclaredWireToolNames(currentCatalog),
472
+ bareCustomToolNames: collectDeclaredBareCustomWireToolNames(currentCatalog),
473
+ };
474
+ }
439
475
  if (options.inboundWire === "anthropic") {
440
476
  parsed._nativeReasoningReplay = options.nativeReasoningReplay;
441
477
  parsed._nativeReasoningMint = options.nativeReasoningMint;
478
+ parsed._anthropicServerTools = options.anthropicServerTools;
442
479
  }
443
480
  parsed._promptCacheKeyIsSharedCohort = options.promptCacheKeyIsSharedCohort;
444
481
  // The body may have been rebuilt since the inbound observation (previous-response
@@ -561,7 +598,10 @@ export async function prepareResponsesRequest(
561
598
  if (!logCtx.conversationId) {
562
599
  logCtx.conversationId = resolvedConversationId;
563
600
  }
564
- logCtx.requestedModel = options.compactionRoutingOverride?.sourceModel ?? parsed.modelId;
601
+ // A rewritten turn logs the selector the client asked for, so the usage log never
602
+ // claims the operator typed the fallback model themselves.
603
+ logCtx.requestedModel = options.mainHardLockFallbackOverride?.sourceModel
604
+ ?? options.compactionRoutingOverride?.sourceModel ?? parsed.modelId;
565
605
  logCtx.requestedEffort = parsed.options.reasoning;
566
606
  // What this request may spend beyond its input, for the durable spend reservation (#4707).
567
607
  // Read from the caller rather than from the adapter's serialized body, because the
@@ -582,7 +622,7 @@ export async function prepareResponsesRequest(
582
622
  // The selector the caller actually sent, captured before shadow interception
583
623
  // or a subagent fallback rewrites it, so a refusal names the client's own
584
624
  // request rather than a destination it never asked for.
585
- const inboundSelector = parsed.modelId;
625
+ const inboundSelector = options.mainHardLockFallbackOverride?.sourceModel ?? parsed.modelId;
586
626
  const selectorTargetsSecondary = (selector: string): boolean => {
587
627
  const slash = selector.indexOf("/");
588
628
  if (slash <= 0) return false;
@@ -602,6 +642,19 @@ export async function prepareResponsesRequest(
602
642
  }
603
643
  const admissionScope = resolveAdmissionModelScope(config, options.admission);
604
644
  const captureInboundRoutePolicy = (candidate: RouteResult, captureRequestPolicy = true): RouteResult => {
645
+ // Router-collapsed aliases bypass the combo loop. Re-pick before capturing destination
646
+ // policy so random/RR cannot refuse while an unmetered target is still usable.
647
+ if (candidate.combo?.target.metered && !admissionMayUseMeteredComboTargets(
648
+ config, options.admission, config.combos?.[candidate.combo.comboId],
649
+ )) {
650
+ const pick = pickComboTarget(config, candidate.combo.comboId, { eligible: target => !target.metered });
651
+ if (!pick) {
652
+ throw new NoAvailableComboTargetsError(candidate.combo.comboId,
653
+ comboExclusionReasons(config, candidate.combo.comboId, target => !target.metered));
654
+ }
655
+ const routed = routeConcreteModel(config, `${pick.target.provider}/${pick.target.model}`);
656
+ candidate = { ...routed, combo: pick, routeKind: "combo", routeReason: routed.routeReason === "blocked-model-redirect" ? "blocked-model-redirect" : "combo-pick" };
657
+ }
605
658
  // Every route this request path produces passes through here: the direct
606
659
  // name, an alias, a policy or combo selection, a compaction override, a
607
660
  // shadow-intercept target and both subagent-fallback re-routes. Checking
@@ -668,7 +721,7 @@ export async function prepareResponsesRequest(
668
721
  }
669
722
  const _sci = config.shadowCallIntercept;
670
723
  let shadowRoute: RouteResult | undefined;
671
- if (!concreteSelection && !memoryRoute && !options.memoryModelPhase && !options.compactionRoutingOverride && !options.internalDecisionCall && !preserveShadowModel && _sci?.enabled && _sci.model && isShadowSourceModel(parsed.modelId, _sci.sourceModels)) {
724
+ if (!concreteSelection && !memoryRoute && !options.memoryModelPhase && !options.compactionRoutingOverride && !options.mainHardLockFallbackOverride && !options.internalDecisionCall && !preserveShadowModel && _sci?.enabled && _sci.model && isShadowSourceModel(parsed.modelId, _sci.sourceModels)) {
672
725
  const sourcePrefix = shadowSourceModelPrefix(parsed.modelId, _sci.sourceModels)!;
673
726
  let sourceIdentity = { providerName: OPENAI_CODEX_PROVIDER_ID, modelId: sourcePrefix };
674
727
  try {
@@ -721,15 +774,22 @@ export async function prepareResponsesRequest(
721
774
  };
722
775
  }
723
776
  }
724
- if (options.compactionRoutingOverride && !compactionRoutingKeepsProviderIdentity(config, options.compactionRoutingOverride, route)) {
777
+ if ((options.compactionRoutingOverride && !compactionRoutingKeepsProviderIdentity(config, options.compactionRoutingOverride, route))
778
+ || options.mainHardLockFallbackOverride) {
725
779
  credentialDomainWasRewritten = true;
726
780
  // The destination does not share the conversation's credential domain, so it can neither
727
781
  // verify the source backend's reasoning ciphertext nor decode its native compaction blob.
728
782
  // This is the same condition an account change already reports (account-change-state.ts),
729
783
  // and the serializer turns a stored summary into readable text instead of dropping it.
730
784
  parsed._stripReasoningEncryptedContent = true;
785
+ parsed._stripNativeCompactionEncryptedContent = true;
731
786
  if (parsed._compactionRequest === true) parsed._portableCompaction = true;
732
787
  }
788
+ // A v1 fallback needs an ocx1 summary for replacement-history decoding, even when this
789
+ // custom destination separately accepts native v2 triggers. Legacy defaults stay intact.
790
+ if (options.compactionRecoveryKind === "compaction-v1" && parsed._compactionRequest === true
791
+ && route.provider.adapter === "openai-responses" && route.provider.supportsNativeCompactionTrigger === true
792
+ && !isCanonicalOpenAiForwardProvider(route.provider)) parsed._portableCompaction = true;
733
793
  logCtx.routeDecision = route.routeDecision;
734
794
  logCtx.policyEligibility = route.policyEligibility;
735
795
  } catch (err) {
@@ -740,7 +800,7 @@ export async function prepareResponsesRequest(
740
800
  if (policyRefusal) return policyRefusal;
741
801
  if (err instanceof AdmissionModelDeniedError) return admissionModelDeniedResponse(err);
742
802
  if (err instanceof NoAvailableComboTargetsError) {
743
- return comboUnavailable(err.comboId);
803
+ return comboUnavailable(err.comboId, undefined, err.reasons);
744
804
  }
745
805
  if (err instanceof NoEligiblePolicyCandidateError) {
746
806
  // Persist the evaluation trace (per-candidate exclusions + the
@@ -958,7 +1018,7 @@ export async function prepareResponsesRequest(
958
1018
  if (policyRefusal) return policyRefusal;
959
1019
  if (err instanceof AdmissionModelDeniedError) return admissionModelDeniedResponse(err);
960
1020
  if (err instanceof NoAvailableComboTargetsError) {
961
- return comboUnavailable(err.comboId);
1021
+ return comboUnavailable(err.comboId, undefined, err.reasons);
962
1022
  }
963
1023
  if (err instanceof NoEligiblePolicyCandidateError) {
964
1024
  logCtx.routeDecision = err.trace;
@@ -1030,6 +1090,7 @@ export async function prepareResponsesRequest(
1030
1090
  "_promptCacheKeyIsSharedCohort",
1031
1091
  "_cursorClientThreadId",
1032
1092
  "_reasoningReplayScope",
1093
+ "_nativeReasoningRetention",
1033
1094
  "_cursorIsolateConversation",
1034
1095
  ];
1035
1096
  for (const key of kept) {
@@ -1175,7 +1236,7 @@ export async function prepareResponsesRequest(
1175
1236
  if (policyRefusal) return policyRefusal;
1176
1237
  if (err instanceof AdmissionModelDeniedError) return admissionModelDeniedResponse(err);
1177
1238
  if (err instanceof NoAvailableComboTargetsError) {
1178
- return comboUnavailable(err.comboId);
1239
+ return comboUnavailable(err.comboId, undefined, err.reasons);
1179
1240
  }
1180
1241
  if (err instanceof NoEligiblePolicyCandidateError) {
1181
1242
  logCtx.routeDecision = err.trace;
@@ -1492,6 +1553,8 @@ export async function prepareResponsesRequest(
1492
1553
  applyAccountChangeConversationStateScrub({
1493
1554
  body: parsed._rawBody,
1494
1555
  parsed,
1556
+ preserveReasoningEncryptedContent: isCanonicalOpenAiForwardProvider(route.provider)
1557
+ && parsed._nativeReasoningRetention?.accountSwitch === true,
1495
1558
  bindingKey: binding.bindingKey,
1496
1559
  servingAccountId: binding.accountId,
1497
1560
  logCtx,
@@ -1511,6 +1574,7 @@ export async function prepareResponsesRequest(
1511
1574
  return {
1512
1575
  inboundWire,
1513
1576
  clientDeclaredEmptyToolCatalog: options.clientDeclaredEmptyToolCatalog === true,
1577
+ clientToolCatalogRestriction,
1514
1578
  // Admission facts later route rewrites must not re-derive: the selector the caller sent and
1515
1579
  // the key scope as it stood when this request was admitted.
1516
1580
  inboundSelector,
@@ -2,7 +2,7 @@ import type { ResponsesRequestContext } from "./core-options";
2
2
  import type { PreparedResponsesRequest } from "./request-prepare";
3
3
  import type { ResponsesTransport } from "./request-transport";
4
4
  import type { ResolvedOpenAiForwardSidecar } from "../../providers/openai-sidecar";
5
- import { isCanonicalOpenAiForwardProvider } from "../../providers/openai-tiers";
5
+ import { isCanonicalOpenAiForwardProvider, supportsNativeResponsesCompactionTrigger } from "../../providers/openai-tiers";
6
6
  import { omitEarlierCompactionImages } from "../../responses/compaction-images";
7
7
  import {
8
8
  shouldResolveOpenAiVisionSidecar,
@@ -51,7 +51,7 @@ export async function prepareResponsesSidecarAuth(
51
51
  let openAiSidecar: ResolvedOpenAiForwardSidecar | undefined;
52
52
  const visionDescribeTerminal = options.visionDescribeTerminal === true;
53
53
  const routedCompaction = parsed._compactionRequest === true
54
- && (!isCanonicalOpenAiForwardProvider(route.provider) || parsed._portableCompaction === true);
54
+ && (!supportsNativeResponsesCompactionTrigger(route.provider) || parsed._portableCompaction === true);
55
55
  if (routedCompaction) parsed.context.messages = omitEarlierCompactionImages(parsed.context.messages);
56
56
  const needsOpenAiVision = !visionDescribeTerminal
57
57
  && shouldResolveOpenAiVisionSidecar(config, route.provider, route.modelId, parsed, route.providerName);