@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
@@ -7,9 +7,11 @@ import { readBoundedResponseBytes } from "../../lib/bounded-body";
7
7
  import { isRequestExecutionBudget, type RequestExecutionBudget, type SingleUseDispatchPermit } from "../../lib/request-execution-budget";
8
8
  import { isNonReplayableResponse, isNonReplayableUpstreamCode, markResponseNonReplayable, TRANSIENT_RETRY_MAX_ATTEMPTS } from "../../lib/upstream-retry";
9
9
  import { isCyberPolicyCode, isTerminalRefusalCode } from "../../lib/errors";
10
- import { isCanonicalOpenAiForwardProvider, supportsNativeResponsesCompactEndpoint } from "../../providers/openai-tiers";
10
+ import { isCanonicalOpenAiForwardProvider, supportsNativeResponsesCompactEndpoint, supportsNativeResponsesCompactionTrigger } from "../../providers/openai-tiers";
11
11
  import { bridgeToResponsesSSE, formatErrorResponse } from "../../bridge";
12
12
  import { buildCompactV1Output, decodeCompactionSummary, encodeCompactionSummary, extractCompactUserMessages } from "../../responses/compaction";
13
+ import { decodeRetainedCompaction, encodeRetainedCompaction } from "../../responses/retained-compaction";
14
+ import { collectReasoningTexts } from "../../responses/reasoning-retention";
13
15
  import { finishRequestAttempt, usageFromResponsesPayload, type RequestLogContext } from "../request-log";
14
16
  import { linkRequestSessionLane } from "../request-log-conversation";
15
17
  import { isNativePassthroughSseResponse, markNativePassthroughSseResponse, isEagerRelaySseResponse, markEagerRelaySseResponse } from "../relay";
@@ -55,15 +57,17 @@ function portableBody(body: Record<string, unknown>): boolean {
55
57
  if (!Array.isArray(body.input) || body.store === true || conversationCarriesUploadedFiles(body)) return false;
56
58
  // Native ciphertext cannot be summarized by another provider. Never silently replace it with a note.
57
59
  if (body.input.some(item => record(item) && ["compaction", "compaction_summary", "context_compaction"].includes(String(item.type))
58
- && typeof item.encrypted_content === "string" && !item.encrypted_content.startsWith("ocx1:"))) return false;
60
+ && typeof item.encrypted_content === "string" && !item.encrypted_content.startsWith("ocx1:") && !decodeRetainedCompaction(item.encrypted_content))) return false;
59
61
  const input = body.input.filter(item => !record(item) || item.type !== "compaction_trigger");
60
62
  return selfContainedResponsesBody({ ...body, store: false, input });
61
63
  }
62
64
 
63
- function routed(route: RouteResult): boolean {
65
+ function routed(route: RouteResult, kind: "compaction-v1" | "compaction-v2"): boolean {
64
66
  return !route.combo && route.routeKind !== "policy" && route.routeReason !== "default-provider"
65
67
  && !isCanonicalOpenAiForwardProvider(route.provider)
66
- && !supportsNativeResponsesCompactEndpoint(route.providerName, route.provider);
68
+ && !(kind === "compaction-v1"
69
+ ? supportsNativeResponsesCompactEndpoint(route.providerName, route.provider)
70
+ : supportsNativeResponsesCompactionTrigger(route.provider));
67
71
  }
68
72
 
69
73
  /** One reader, bounded bytes, and an exact replacement body; never clone a live stream. */
@@ -89,6 +93,7 @@ export async function runWithCompactionRecovery(
89
93
  req: Request, config: OcxConfig, logCtx: RequestLogContext, options: Options, dispatch: Dispatch,
90
94
  ): Promise<Response> {
91
95
  const recovery = readCompactionRecoveryConfig(config.compactionRecovery);
96
+ const kind = options.compactionRecoveryKind ?? "compaction-v2";
92
97
  if (!recovery || options.compactionRecoveryAttempted || options.comboAttempt || (options.inboundWire && options.inboundWire !== "responses")) {
93
98
  return dispatch(req, config, logCtx, options);
94
99
  }
@@ -132,7 +137,7 @@ export async function runWithCompactionRecovery(
132
137
  },
133
138
  onCompactionRecoveryRoute(route) {
134
139
  options.onCompactionRecoveryRoute?.(route);
135
- if (snapshot && routed(route)) sourceRoute = { ...route };
140
+ if (snapshot && routed(route, kind)) sourceRoute = { ...route };
136
141
  },
137
142
  onCompactionRecoveryAdapterEvent(event) {
138
143
  options.onCompactionRecoveryAdapterEvent?.(event);
@@ -151,7 +156,7 @@ export async function runWithCompactionRecovery(
151
156
  if (!snapshot || !sourceRoute || signal.aborted || req.signal.aborted || isNonReplayableResponse(response)) return keep(response);
152
157
  let target: RouteResult;
153
158
  try { target = routeConcreteModel(config, recovery.model); } catch { return keep(response); }
154
- if (!routed(target) || identity(sourceRoute) === identity(target)) return keep(response);
159
+ if (!routed(target, kind) || identity(sourceRoute) === identity(target)) return keep(response);
155
160
  const originalModel = firstOptions.compactionRoutingOverride?.sourceModel ?? String(snapshot.model);
156
161
  // Use the established protocol commit boundary. For runTurn streams the direct event
157
162
  // observer additionally preserves side-effect heartbeats that the bridge does not publish.
@@ -289,7 +294,8 @@ export async function runWithCompactionRecovery(
289
294
  const compactions = items.filter(value => record(value) && value.type === "compaction");
290
295
  const permitted = items.every(value => record(value) && (value.type === "compaction" || value.type === "reasoning"));
291
296
  const item = permitted && compactions.length === 1 ? compactions[0] as Record<string, unknown> : undefined;
292
- const summary = item && typeof item.encrypted_content === "string" ? decodeCompactionSummary(item.encrypted_content) : null;
297
+ const structured = item && typeof item.encrypted_content === "string" ? decodeRetainedCompaction(item.encrypted_content) : null;
298
+ const summary = structured?.summary ?? (item && typeof item.encrypted_content === "string" ? decodeCompactionSummary(item.encrypted_content) : null);
293
299
  if (json?.status !== "completed" || !summary?.trim()) {
294
300
  void completed.response.body?.cancel().catch(() => undefined);
295
301
  restoreSourceLog?.(completed.response.status);
@@ -298,11 +304,13 @@ export async function runWithCompactionRecovery(
298
304
  // v1 unpacks this text through buildCompactV1Output, which already re-adds retained user
299
305
  // messages as items; embedding them here too would duplicate the same text in the output.
300
306
  const preserved = options.compactionRecoveryKind === "compaction-v1" ? summary : (() => {
301
- const retained = buildCompactV1Output(extractCompactUserMessages(snapshot.input), summary).slice(0, -1);
307
+ const usersOnly = Array.isArray(snapshot.input) ? snapshot.input.filter(item => collectReasoningTexts([item]).length === 0) : snapshot.input;
308
+ const retained = buildCompactV1Output(extractCompactUserMessages(usersOnly), summary).slice(0, -1);
302
309
  const userText = extractCompactUserMessages(retained).map((text, index) => `User message ${index + 1}:\n${text}`).join("\n\n");
303
310
  return `${summary}\n\nRetained original user messages (verbatim; preserve their goals and constraints):\n${userText}`;
304
311
  })();
305
- item!.encrypted_content = encodeCompactionSummary(preserved);
312
+ const encryptedContent = structured ? encodeRetainedCompaction(preserved, structured.reasoning) : encodeCompactionSummary(preserved);
313
+ item!.encrypted_content = encryptedContent;
306
314
  json.model = originalModel;
307
315
  void completed.response.body?.cancel().catch(() => undefined);
308
316
  void response.body?.cancel().catch(() => undefined);
@@ -310,7 +318,7 @@ export async function runWithCompactionRecovery(
310
318
  const usage = usageFromResponsesPayload(json.usage);
311
319
  async function* events(): AsyncGenerator<AdapterEvent> {
312
320
  yield { type: "text_delta", text: preserved };
313
- yield { type: "done", ...(usage ? { usage } : {}) };
321
+ yield { type: "done", compactionEncryptedContent: encryptedContent, ...(usage ? { usage } : {}) };
314
322
  }
315
323
  return new Response(bridgeToResponsesSSE(events(), originalModel, undefined, undefined, undefined, undefined, 2_000,
316
324
  { compaction: true, translatorBudget: options.translatorBudget, onCompletedResponse: () => options.onResponseComplete?.(originalModel) }), { headers: { "content-type": "text/event-stream" } });
@@ -0,0 +1,61 @@
1
+ import { rmSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { getConfigDir } from "../../config";
4
+ import type { OcxConfig, OcxParsedRequest } from "../../types";
5
+ import type { TranslatorBudget } from "../../lib/translator-budget";
6
+ import { parseRequest } from "../../responses/parser";
7
+ import { applyReasoningRetention, appendRetentionNotice } from "../../responses/reasoning-retention";
8
+ import { encodeRetainedCompaction } from "../../responses/retained-compaction";
9
+ import { finalizeAccountLease } from "./core-lifetime";
10
+
11
+ export class InvalidRetainedCompactionSummary extends Error {}
12
+
13
+ /** One routed v2 attempt owns its local retention until its response ends or is cancelled. */
14
+ export function prepareCompactionRetention(
15
+ parsed: OcxParsedRequest,
16
+ config: OcxConfig,
17
+ contextWindow: number | undefined,
18
+ budget: TranslatorBudget,
19
+ signal: AbortSignal,
20
+ ) {
21
+ if (config.reasoningRetention?.enabled !== true) return undefined;
22
+ const raw = parsed._rawBody as Record<string, unknown>;
23
+ const retention = applyReasoningRetention(raw.input, {
24
+ archiveDir: join(getConfigDir(), "reasoning-archive"), contextWindow,
25
+ ...config.reasoningRetention,
26
+ });
27
+ if (!retention.retainedReasoning && !retention.archived) return undefined;
28
+ let committed = false;
29
+ let disposed = false;
30
+ let bytes = 0;
31
+ const dispose = () => {
32
+ if (disposed) return;
33
+ disposed = true;
34
+ signal.removeEventListener("abort", dispose);
35
+ delete parsed._compactionRetention;
36
+ budget.releaseRetained(bytes, { kind: "retained_collectors" });
37
+ if (!committed && retention.archived) rmSync(retention.archived.path, { force: true });
38
+ };
39
+ try {
40
+ const reasoning = retention.retainedReasoning?.map(item => item.content[0]!.text) ?? [];
41
+ const retainedBytes = Buffer.byteLength(JSON.stringify(reasoning));
42
+ budget.chargeRetained(retainedBytes, { kind: "retained_collectors" });
43
+ bytes = retainedBytes;
44
+ const body = { ...raw, model: parsed.modelId, input: retention.input };
45
+ const messages = parseRequest(body, { replayCacheScope: parsed._reasoningReplayScope }).context.messages;
46
+ parsed._rawBody = body;
47
+ parsed.context.messages = messages;
48
+ const callbacks: NonNullable<OcxParsedRequest["_compactionRetention"]> = {
49
+ encode(summary) {
50
+ if (disposed || signal.aborted) throw new Error("Compaction was cancelled");
51
+ if (!summary.trim()) throw new InvalidRetainedCompactionSummary("Compaction returned an empty summary");
52
+ return encodeRetainedCompaction(appendRetentionNotice(summary, retention.archived), reasoning);
53
+ },
54
+ commit() { if (!disposed && !signal.aborted) committed = true; },
55
+ };
56
+ Object.defineProperty(parsed, "_compactionRetention", { value: callbacks, configurable: true });
57
+ if (signal.aborted) { dispose(); throw signal.reason; }
58
+ signal.addEventListener("abort", dispose, { once: true });
59
+ return { dispose, deliver: (response: Response) => finalizeAccountLease(response, dispose, dispose) };
60
+ } catch (error) { dispose(); throw error; }
61
+ }
@@ -17,21 +17,25 @@ export function lateAttributionLimitResponse(): Response {
17
17
  export const PROVIDER_INPUT_TOO_LARGE_MESSAGE =
18
18
  "The provider rejected this turn because its input exceeds the provider size or context limit. Reduce the current input or compact the conversation before retrying.";
19
19
 
20
- /** Preserve non-streaming HTTP failure semantics without exposing an upstream body. */
21
- export function jsonContextOverflowResponse(): Response {
20
+ /**
21
+ * Preserve non-streaming HTTP failure semantics without exposing an upstream body. `message` is
22
+ * replaced only for the proxy's own wording, never with upstream text: when every combo target
23
+ * was refused by local input admission, no provider saw the turn and saying one did is false.
24
+ */
25
+ export function jsonContextOverflowResponse(message = PROVIDER_INPUT_TOO_LARGE_MESSAGE): Response {
22
26
  return Response.json({
23
27
  error: {
24
- message: PROVIDER_INPUT_TOO_LARGE_MESSAGE,
28
+ message,
25
29
  type: "invalid_request_error",
26
30
  code: "context_length_exceeded",
27
31
  },
28
32
  }, { status: 413, headers: { "Cache-Control": "no-store" } });
29
33
  }
30
34
 
31
- async function* contextOverflowEvents(): AsyncGenerator<AdapterEvent> {
35
+ async function* contextOverflowEvents(message: string): AsyncGenerator<AdapterEvent> {
32
36
  yield {
33
37
  type: "error",
34
- message: PROVIDER_INPUT_TOO_LARGE_MESSAGE,
38
+ message,
35
39
  status: 413,
36
40
  errorType: "invalid_request_error",
37
41
  code: "context_length_exceeded",
@@ -51,9 +55,10 @@ async function* contextOverflowEvents(): AsyncGenerator<AdapterEvent> {
51
55
  export function streamingContextOverflowResponse(
52
56
  modelId: string,
53
57
  translatorBudget: TranslatorBudget,
58
+ message = PROVIDER_INPUT_TOO_LARGE_MESSAGE,
54
59
  ): Response {
55
60
  return new Response(bridgeToResponsesSSE(
56
- contextOverflowEvents(),
61
+ contextOverflowEvents(message),
57
62
  modelId,
58
63
  undefined,
59
64
  undefined,
@@ -43,6 +43,7 @@ import { codexAuthContextLogLabel } from "../../codex/account-label";
43
43
  import { forceRefreshMainAccountToken } from "../../codex/main-account";
44
44
  import { rebindPoolCreditPolicy } from "../../codex/pool-credit-policy";
45
45
  import { safeCallerSessionId } from "../caller-session-identity";
46
+ import { copyResponsesClientMetadata } from "../../adapters/openai-responses/transport";
46
47
 
47
48
  /** Normalize caller aliases only on native egress; preserve ingress and fallback identity. */
48
49
  export function withClaudeNativeSession(headers: Headers, provider: OcxProviderConfig, sessionId?: string): Headers {
@@ -335,6 +336,11 @@ export async function resolveResponsesCodexAuth(
335
336
  headers.delete("chatgpt-account-id");
336
337
  }
337
338
  }
339
+ // Codex auth materialization keeps only its credential allowlist. A compact-v1 handoff
340
+ // must also retain the permitted client fingerprint for its routed summarizer send.
341
+ if (options.compactionRecoveryKind === "compaction-v1") {
342
+ copyResponsesClientMetadata(headers, authInputHeaders, route.provider);
343
+ }
338
344
  return {
339
345
  ok: true,
340
346
  authCtx,
@@ -32,6 +32,7 @@ import {
32
32
  type CodexWsPreOutputRefusalHold,
33
33
  } from "./codex-ws-wire";
34
34
  import type { RequestLogContext } from "../request-log";
35
+ import type { AttemptRecoveryKind } from "../../usage/log";
35
36
  import type { DataPlaneAdmission } from "../auth-cors";
36
37
  import type { InboundWire } from "../../providers/registry";
37
38
  import type { BunRuntimeGateInput } from "./ws-upstream";
@@ -909,6 +910,8 @@ export async function retryCodexPoolOnAlternateAccount(
909
910
  applyAccountChangeConversationStateScrub({
910
911
  body: parsed._rawBody,
911
912
  parsed,
913
+ preserveReasoningEncryptedContent: isCanonicalOpenAiForwardProvider(retryProvider)
914
+ && parsed._nativeReasoningRetention?.accountSwitch === true,
912
915
  bindingKey: binding.bindingKey,
913
916
  servingAccountId: binding.accountId,
914
917
  priorAccountId: firstAuthCtx.accountId,
@@ -971,6 +974,10 @@ export async function retryCodexPoolOnAlternateAccount(
971
974
  // What this diff does fix is that the rungs are now CHARGED instead of free.
972
975
  const maxRetrySends = retrySameConfirmedAccount ? 7 : 1;
973
976
  let retrySendCount = 0;
977
+ // The gated-model 400 has no roster kind; it stays unattributed as before.
978
+ const moveRecovery: AttemptRecoveryKind | undefined = outcomeStatus === 429 || outcomeStatus === 402
979
+ ? "oauth-account-429"
980
+ : isTransientUpstreamStatus(outcomeStatus) ? "transient-5xx" : undefined;
974
981
  let upstreamResponse: Response;
975
982
  try {
976
983
  while (true) {
@@ -1005,7 +1012,10 @@ export async function retryCodexPoolOnAlternateAccount(
1005
1012
  // The move is a physical send like any other, so the root workflow is charged too.
1006
1013
  chargeWorkflowSends(args.options.workflowRootId, 1);
1007
1014
  }
1008
- noteProviderAttemptSend(logCtx, route.providerName, route.provider, passthroughEstimate);
1015
+ // A move seals the first account's attempt here. Give it the status that account actually
1016
+ // answered, not the 502 the split infers, and name why the second send exists.
1017
+ if (logCtx.activeAttempt && logCtx.activeAttempt.status < 100) logCtx.activeAttempt.status = firstResponse.status;
1018
+ noteProviderAttemptSend(logCtx, route.providerName, route.provider, passthroughEstimate, moveRecovery);
1009
1019
  try {
1010
1020
  upstreamResponse = await fetchWithHeaderTimeout(
1011
1021
  request.url,
@@ -3,6 +3,7 @@ import { parseRetryAfterMs } from "../../combos";
3
3
  import type { ConsumedComboFailure, HandleResponsesOptions } from "./core-options";
4
4
  import type { OcxUsage } from "../../types";
5
5
  import { readBoundedResponseBody } from "../../lib/bounded-body";
6
+ import { carryAnthropicRefusalCredential } from "../../oauth/anthropic-account-refusal";
6
7
  import { codexQuotaFailureMessage, codexQuotaOutcomeMeta } from "./core-codex-account";
7
8
  import {
8
9
  isRateLimitOrQuotaFailureMessage,
@@ -125,6 +126,7 @@ export async function consumeComboFailure(
125
126
  },
126
127
  );
127
128
  if (nonReplayable) markResponseNonReplayable(failureResponse);
129
+ carryAnthropicRefusalCredential(response, failureResponse);
128
130
  return {
129
131
  response: failureResponse,
130
132
  ...(nonReplayable ? { nonReplayable: true } : {}),
@@ -6,7 +6,7 @@ import {
6
6
  isDeclaredReasoningEffort,
7
7
  resolveEffortAtOrBelow,
8
8
  } from "../../reasoning-effort";
9
- import { resolveAdmissionModelScope, routeAllowedByScope } from "../admission-model-scope";
9
+ import { admissionMayUseMeteredComboTargets, resolveAdmissionModelScope, routeAllowedByScope } from "../admission-model-scope";
10
10
  import { recordAttemptRequestedEffort } from "../request-log";
11
11
  import {
12
12
  CODEX_TEXT_GUARDED_BUDGET_POLICY,
@@ -31,6 +31,7 @@ import {
31
31
  comboDefaultEffort,
32
32
  isComboTargetInCooldown,
33
33
  snapshotComboQuotaCooldowns,
34
+ comboExclusionReasons,
34
35
  noteComboSuccess,
35
36
  comboFailureDecision,
36
37
  advanceComboAfterFailure,
@@ -52,14 +53,17 @@ import {
52
53
  import { hasUnreadableEncryptedAgentTask } from "./encrypted-payload";
53
54
  import { routeConcreteModel, comboRouteDecisionTrace } from "../../router";
54
55
  import { memoryModelRouteReason } from "./memory-models";
55
- import { poolAccountProviderLabel } from "../../providers/label";
56
+ import { baseProviderLabel, poolAccountProviderLabel } from "../../providers/label";
56
57
  import { getAccountSet } from "../../oauth/store";
58
+ import { anthropicRefusalCredentialIsCurrent } from "../../oauth/anthropic-account-refusal";
57
59
  import { configuredAnthropicInstance } from "../../providers/anthropic-instance";
60
+ import { isAnthropicBillingFailure } from "../../providers/anthropic-billing-refusal";
58
61
  import {
59
62
  anthropicRoutingFor,
60
63
  formatAnthropicProviderForLog,
61
64
  } from "../../oauth/anthropic-routing";
62
- import { codexAccountLogLabel } from "../../codex/account-label";
65
+ import { codexAccountLogLabel, oauthAccountLogLabel } from "../../codex/account-label";
66
+ import { isGenericFailoverProvider, rotateGenericOAuthAccountOn429 } from "../../oauth/generic-account-failover";
63
67
  import { codexQuotaScopeForModel, getCodexQuotaHealthSnapshot } from "../../codex/routing";
64
68
  import { isCanonicalOpenAiForwardProvider } from "../../providers/openai-tiers";
65
69
  import type { AgentTaskRecoveryFailureReason } from "./agent-task-recovery";
@@ -135,6 +139,48 @@ function cooledPoolAccountLabel(config: OcxConfig, providerName: string, modelId
135
139
  ? label : undefined;
136
140
  }
137
141
 
142
+ /**
143
+ * The generic OAuth pool's counterpart of `cooledPoolAccountLabel`. Its 429 names one account
144
+ * through `accountLogLabel`, but the per-request account budget can end the pool's own rotation
145
+ * before the last refused account is recorded. Hand that account to the pool's rotator, which
146
+ * cools it under the pool's own Retry-After policy, and spare the target only when the pool still
147
+ * has an eligible sibling. A lone account or an all-cooled pool returns null and cools the target.
148
+ */
149
+ function cooledGenericOAuthAccountLabel(
150
+ config: OcxConfig, providerName: string, modelId: string, label: string | undefined, retryAfter: string | undefined,
151
+ ): string | undefined {
152
+ const provider = config.providers[providerName];
153
+ if (!label || !provider || !isGenericFailoverProvider(providerName, provider)) return undefined;
154
+ const base = baseProviderLabel(providerName);
155
+ const matches = getAccountSet(providerName)?.accounts.filter(account =>
156
+ oauthAccountLogLabel(account.id, base) === label) ?? [];
157
+ return matches.length === 1
158
+ && rotateGenericOAuthAccountOn429(config, providerName, matches[0]!.id, retryAfter, Date.now(), modelId)
159
+ ? label : undefined;
160
+ }
161
+
162
+ /**
163
+ * A 402 credit-balance refusal names one Anthropic OAuth account -- the evidence the pool already
164
+ * accepts on a 403 -- but the pool rotates only on 401/403/429, so the 402 reaches the combo with
165
+ * that account still selectable. Cool it under the pool's entitlement backoff and spare the target
166
+ * only while a sibling is eligible; a lone account or an exhausted pool still cools the target.
167
+ */
168
+ function creditRefusedAnthropicAccountLabel(
169
+ config: OcxConfig, providerName: string, modelId: string, label: string | undefined,
170
+ message: string, code: string | null | undefined, response: Response,
171
+ ): string | undefined {
172
+ const instance = configuredAnthropicInstance(config, providerName);
173
+ if (!label || !instance || config.providers[providerName]?.authMode !== "oauth"
174
+ || !isAnthropicBillingFailure(402, message, code)) return undefined;
175
+ const routing = anthropicRoutingFor(instance);
176
+ const matches = getAccountSet(instance)?.accounts.filter(account =>
177
+ formatAnthropicProviderForLog(instance, account.id) === label) ?? [];
178
+ return matches.length === 1
179
+ && anthropicRefusalCredentialIsCurrent(response, instance, matches[0]!.id)
180
+ && routing.recordAnthropicAccountRefusal(config, matches[0]!.id, 403, null)
181
+ && routing.getEligibleAnthropicAccounts(Date.now(), modelId).length > 0
182
+ ? label : undefined;
183
+ }
138
184
 
139
185
  /**
140
186
  * True only for the Anthropic OAuth pool's OWN "all accounts cooled" 429. Its Retry-After is the
@@ -251,6 +297,30 @@ interface JevComboChoice {
251
297
  candidate: JevCandidate;
252
298
  }
253
299
 
300
+ /** A target's JEV effort choices, or undefined when an explicit allowlist has none still supported. */
301
+ function jevChoiceEfforts(
302
+ config: OcxConfig,
303
+ target: NonNullable<ReturnType<typeof getCombo>>["targets"][number],
304
+ ): OcxComboDefaultEffort[] | undefined {
305
+ let ladder: string[] | undefined;
306
+ try {
307
+ const route = routeConcreteModel(config, targetKey(target));
308
+ ladder = supportedLadderFor({ provider: route.provider, modelId: route.modelId });
309
+ } catch {
310
+ // Preserve the existing routing-failure surface. Unknown capability becomes the explicit
311
+ // no-effort choice rather than broadening JEV's effort allowlist.
312
+ ladder = undefined;
313
+ }
314
+ const supportedEfforts = (ladder ?? []).filter(isCodexReasoningEffort) as OcxComboDefaultEffort[];
315
+ const configuredEfforts = target.reasoningEfforts;
316
+ const reasoningEfforts = configuredEfforts === undefined
317
+ ? supportedEfforts
318
+ : configuredEfforts.filter(effort => supportedEfforts.includes(effort));
319
+ // An explicit allowlist is restrictive. If catalog capabilities drift until no configured
320
+ // effort remains supported, omit the target instead of silently broadening JEV's choices.
321
+ return configuredEfforts !== undefined && reasoningEfforts.length === 0 ? undefined : reasoningEfforts;
322
+ }
323
+
254
324
  /** Enumerate the current ordinary Combo eligibility set without retaining attempted picks. */
255
325
  function eligibleJevComboChoices(
256
326
  config: OcxConfig,
@@ -269,23 +339,8 @@ function eligibleJevComboChoices(
269
339
  excluded.add(key);
270
340
  // The TypeSafe row owns a decision credential, not an inference transport.
271
341
  if (pick.target.provider === JEV_PROVIDER_ID) continue;
272
- let ladder: string[] | undefined;
273
- try {
274
- const route = routeConcreteModel(config, key);
275
- ladder = supportedLadderFor({ provider: route.provider, modelId: route.modelId });
276
- } catch {
277
- // Preserve the existing routing-failure surface. Unknown capability becomes the explicit
278
- // no-effort choice rather than broadening JEV's effort allowlist.
279
- ladder = undefined;
280
- }
281
- const supportedEfforts = (ladder ?? []).filter(isCodexReasoningEffort) as OcxComboDefaultEffort[];
282
- const configuredEfforts = pick.target.reasoningEfforts;
283
- const reasoningEfforts = configuredEfforts === undefined
284
- ? supportedEfforts
285
- : configuredEfforts.filter(effort => supportedEfforts.includes(effort));
286
- // An explicit allowlist is restrictive. If catalog capabilities drift until no configured
287
- // effort remains supported, omit the target instead of silently broadening JEV's choices.
288
- if (configuredEfforts !== undefined && reasoningEfforts.length === 0) continue;
342
+ const reasoningEfforts = jevChoiceEfforts(config, pick.target);
343
+ if (!reasoningEfforts) continue;
289
344
  choices.push({
290
345
  pick: { ...pick, attempted: [key] },
291
346
  candidate: {
@@ -317,9 +372,10 @@ export async function executeComboResponses(
317
372
  options: HandleResponsesOptions & { translatorBudget: TranslatorBudget },
318
373
  requestDispatchers: ResponsesDispatchers,
319
374
  ): Promise<Response> {
320
- const requestedModel = typeof (rawBody as { model?: unknown } | null)?.model === "string"
321
- ? (rawBody as { model: string }).model
322
- : `combo/${comboId}`;
375
+ const requestedModel = options.mainHardLockFallbackOverride?.sourceModel
376
+ ?? (typeof (rawBody as { model?: unknown } | null)?.model === "string"
377
+ ? (rawBody as { model: string }).model
378
+ : `combo/${comboId}`);
323
379
  Object.assign(logCtx, {
324
380
  requestedModel,
325
381
  model: requestedModel,
@@ -449,8 +505,10 @@ export async function executeComboResponses(
449
505
  let comboPayloadReadable = false;
450
506
  const payloadEligible = (target: (typeof combo.targets)[number]): boolean =>
451
507
  comboPayloadReadable || !unreadableEncryptedAgentTask || canDecryptUnreadableAgentTask(target);
508
+ const meteredAllowed = admissionMayUseMeteredComboTargets(config, options.admission, combo);
452
509
  const targetEligible = (target: (typeof combo.targets)[number]): boolean =>
453
- (combo.strategy !== "jev" || target.provider !== JEV_PROVIDER_ID)
510
+ (meteredAllowed || !target.metered)
511
+ && (combo.strategy !== "jev" || target.provider !== JEV_PROVIDER_ID)
454
512
  && payloadEligible(target)
455
513
  && reasoningReplayEligible(target)
456
514
  && (protocolLanes?.pickable(target) ?? true);
@@ -524,6 +582,9 @@ export async function executeComboResponses(
524
582
  now?: number;
525
583
  }) => pickComboTargetWithWait(config, comboId, {
526
584
  ...pickOptions,
585
+ // Every pick, including the recovery picks that skip `targetEligible`, keeps metered
586
+ // targets away from callers that may not spend on them.
587
+ eligible: target => (meteredAllowed || !target.metered) && (pickOptions.eligible?.(target) ?? true),
527
588
  waitForCooldownMs: combo.waitForCooldownMs,
528
589
  abortSignal: options.abortSignal,
529
590
  });
@@ -543,7 +604,8 @@ export async function executeComboResponses(
543
604
  );
544
605
  return options.abortSignal?.aborted
545
606
  ? clientCancelledResponse()
546
- : comboUnavailable(comboId);
607
+ : comboUnavailable(comboId, undefined,
608
+ comboExclusionReasons(config, comboId, target => meteredAllowed || !target.metered));
547
609
  }
548
610
  if (!(await recoverUnreadableEncryptedTask())) {
549
611
  return options.abortSignal?.aborted
@@ -559,13 +621,16 @@ export async function executeComboResponses(
559
621
  if (onlyReplayIncompatibleTargetsRemain()) return targetIncompatibleResponse();
560
622
  return options.abortSignal?.aborted
561
623
  ? clientCancelledResponse()
562
- : comboUnavailable(comboId);
624
+ : comboUnavailable(comboId, undefined, comboExclusionReasons(config, comboId, targetEligible));
563
625
  }
564
626
  let jevDecision: JevDecision | undefined;
565
627
  if (combo.strategy === "jev") {
566
628
  const choices = eligibleJevComboChoices(config, comboId, targetEligible, Date.now());
567
629
  const first = choices[0];
568
- if (!first) return comboUnavailable(comboId);
630
+ if (!first) {
631
+ return comboUnavailable(comboId, undefined, comboExclusionReasons(config, comboId, target =>
632
+ targetEligible(target) && target.provider !== JEV_PROVIDER_ID && jevChoiceEfforts(config, target) !== undefined));
633
+ }
569
634
  const resolvedFailOpenEffort = resolveEffortAtOrBelow(
570
635
  "medium",
571
636
  first.candidate.reasoningEfforts,
@@ -679,6 +744,9 @@ export async function executeComboResponses(
679
744
  // is gone, so carry the loop's own classification decision instead of re-deriving a
680
745
  // weaker one from the status alone (#4149).
681
746
  let lastFailureClassifiesOverflow = false;
747
+ // Local input admission refusals (#1524), while every failed target so far was one: such a
748
+ // target sent nothing, so an exhausted ladder must not be reported as a provider rejection.
749
+ let localAdmissionRefusals: string[] | undefined = [];
682
750
  // The first quota/rate-limit refusal of the ladder. A fallback that then refuses its own
683
751
  // credential or plan (401/403, or a 400 the ladder hopped past) says nothing about why the
684
752
  // request failed: the primary target ran out. Returning that fallback refusal told clients
@@ -853,7 +921,8 @@ export async function executeComboResponses(
853
921
  // The live config can change while the child is streaming. Never retain credentials.
854
922
  const currentCombo = getCombo(config, comboId);
855
923
  const provider = config.providers[completedTarget.provider];
856
- if (!options.compactionRoutingOverride && Object.hasOwn(config.providers, completedTarget.provider)
924
+ if (!options.compactionRoutingOverride && !options.mainHardLockFallbackOverride
925
+ && Object.hasOwn(config.providers, completedTarget.provider)
857
926
  && provider && provider.disabled !== true
858
927
  && currentCombo?.targets.some(target => targetKey(target) === targetKey(completedTarget))) {
859
928
  rememberComboForLane(sessionLaneIdFromRequest(req.headers), comboId, completedTarget, model, writerGeneration);
@@ -1045,6 +1114,12 @@ export async function executeComboResponses(
1045
1114
  Date.now() - started,
1046
1115
  failure.usage,
1047
1116
  );
1117
+ if (failure.upstreamCode === "input_admission_refused" && attempt.sendCount === 0) {
1118
+ attempt.errorCode = failure.upstreamCode;
1119
+ localAdmissionRefusals?.push(failure.upstreamMessage ?? "");
1120
+ } else {
1121
+ localAdmissionRefusals = undefined;
1122
+ }
1048
1123
  attemptRetained = true;
1049
1124
  lastFailure = failure.response;
1050
1125
  if (!quotaFailure && (lastFailure.status === 429 || lastFailure.status === 402)) {
@@ -1128,12 +1203,16 @@ export async function executeComboResponses(
1128
1203
  const failedTargetKey = targetKey(pick.target);
1129
1204
  let failedTargetCooldownRecorded = false;
1130
1205
  // The dispatch rewrote this to name the pool account that actually served the turn.
1131
- const failedAccount = cooledPoolAccountLabel(
1132
- config,
1133
- pick.target.provider,
1134
- pick.target.model,
1135
- poolAccountProviderLabel(childLog.provider, pick.target.provider),
1136
- );
1206
+ const poolAccountLabel = poolAccountProviderLabel(childLog.provider, pick.target.provider);
1207
+ const failedAccount = failure.response.status === 402
1208
+ ? creditRefusedAnthropicAccountLabel(
1209
+ config, pick.target.provider, pick.target.model, poolAccountLabel, failure.classificationText, failure.upstreamCode,
1210
+ failure.response,
1211
+ )
1212
+ : cooledPoolAccountLabel(config, pick.target.provider, pick.target.model, poolAccountLabel)
1213
+ ?? (failure.response.status === 429 && failureCooldownScope === "target"
1214
+ ? cooledGenericOAuthAccountLabel(config, pick.target.provider, pick.target.model, childLog.accountLogLabel, failure.retryAfter)
1215
+ : undefined);
1137
1216
  // The target still cools on the pool's own refusal (the anti-hammer guard), but for the local
1138
1217
  // fallback rather than the pool's restated per-account Retry-After.
1139
1218
  const poolLocalRefusal = isAnthropicPoolLocalRefusal(
@@ -1225,9 +1304,14 @@ export async function executeComboResponses(
1225
1304
  lastFailure?.status === 413
1226
1305
  && lastFailureClassifiesOverflow
1227
1306
  ) {
1307
+ if (localAdmissionRefusals?.length) logCtx.errorCode = "input_admission_refused";
1308
+ const localMessage = localAdmissionRefusals?.length
1309
+ ? `opencodex refused this turn locally; nothing was sent upstream. No target of combo ${comboId} `
1310
+ + `that was tried can fit its input: ${localAdmissionRefusals.join(" ")} Compact the conversation before retrying.`
1311
+ : undefined;
1228
1312
  return (rawBody as { stream?: unknown } | null)?.stream === true
1229
- ? streamingContextOverflowResponse(requestedModel, options.translatorBudget)
1230
- : jsonContextOverflowResponse();
1313
+ ? streamingContextOverflowResponse(requestedModel, options.translatorBudget, localMessage)
1314
+ : jsonContextOverflowResponse(localMessage);
1231
1315
  }
1232
1316
  return failure;
1233
1317
  }
@@ -8,7 +8,7 @@ import {
8
8
  DecompressedBodyTooLargeError,
9
9
  describeInboundBodyRefusal,
10
10
  } from "../request-decompress";
11
- import { comboCooldownRetryAfterSeconds } from "../../combos";
11
+ import { comboCooldownRetryAfterSeconds, NoAvailableComboTargetsError } from "../../combos";
12
12
  import type { AgentTaskRecoveryFailureReason } from "./agent-task-recovery";
13
13
 
14
14
  /**
@@ -115,8 +115,8 @@ export function comboUnavailableResponse(
115
115
  }
116
116
 
117
117
 
118
- export function comboUnavailable(comboId: string, now = Date.now()): Response {
119
- return comboUnavailableResponse(`No available targets for combo: ${comboId}`, {
118
+ export function comboUnavailable(comboId: string, now = Date.now(), reasons = ""): Response {
119
+ return comboUnavailableResponse(new NoAvailableComboTargetsError(comboId, reasons).message, {
120
120
  retryAfter: comboCooldownRetryAfterSeconds(comboId, now),
121
121
  });
122
122
  }
@@ -48,11 +48,13 @@ export function finalizeOwnedTranslatorBudget(response: Response, budget: Transl
48
48
  markEagerRelaySseResponse(finalizedResponse);
49
49
  }
50
50
  if (isPreinspectedJsonResponse(response)) markPreinspectedJsonResponse(finalizedResponse);
51
+ if (isNonReplayableResponse(response)) markResponseNonReplayable(finalizedResponse);
52
+ if (runTurnAdapterSseResponses.has(response)) runTurnAdapterSseResponses.add(finalizedResponse);
51
53
  return finalizedResponse;
52
54
  }
53
55
 
54
56
  /** Release a serving-account slot when the client body ends, errors or is cancelled. */
55
- export function finalizeAccountLease(response: Response, release: () => void): Response {
57
+ export function finalizeAccountLease(response: Response, release: () => void, onCancel?: () => void): Response {
56
58
  if (!response.body) { release(); return response; }
57
59
  const reader = response.body.getReader();
58
60
  let done = false;
@@ -65,13 +67,14 @@ export function finalizeAccountLease(response: Response, release: () => void): R
65
67
  else controller.enqueue(next.value);
66
68
  } catch (error) { finish(); controller.error(error); }
67
69
  },
68
- async cancel(reason) { try { await reader.cancel(reason); } finally { finish(); } },
70
+ async cancel(reason) { onCancel?.(); try { await reader.cancel(reason); } finally { finish(); } },
69
71
  });
70
72
  const wrapped = new Response(body, { status: response.status, statusText: response.statusText, headers: response.headers });
71
73
  if (isNonReplayableResponse(response)) markResponseNonReplayable(wrapped);
72
74
  if (isNativePassthroughSseResponse(response)) markNativePassthroughSseResponse(wrapped);
73
75
  if (isEagerRelaySseResponse(response)) markEagerRelaySseResponse(wrapped);
74
76
  if (isPreinspectedJsonResponse(response)) markPreinspectedJsonResponse(wrapped);
77
+ if (runTurnAdapterSseResponses.has(response)) runTurnAdapterSseResponses.add(wrapped);
75
78
  return wrapped;
76
79
  }
77
80