@bitkyc08/opencodex 2.86.0 → 2.88.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 (400) hide show
  1. package/gui/dist/assets/App-CHC89EXQ.js +58 -0
  2. package/gui/dist/assets/App-wT3mXqFz.css +1 -0
  3. package/gui/dist/assets/{Tray-DyR9yBee.js → Tray-B5bC7AWL.js} +1 -1
  4. package/gui/dist/assets/index-B5bsvxor.js +94 -0
  5. package/gui/dist/assets/index-DPeB9HpV.css +1 -0
  6. package/gui/dist/assets/quota-summary-D8y2wTsB.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-rejection-memo.ts +71 -0
  14. package/src/adapters/anthropic-replay.ts +135 -0
  15. package/src/adapters/anthropic-tool-catalog.ts +11 -0
  16. package/src/adapters/anthropic.ts +151 -33
  17. package/src/adapters/base.ts +2 -0
  18. package/src/adapters/claude-cli/adapter.ts +111 -23
  19. package/src/adapters/claude-cli/profiles.ts +2 -1
  20. package/src/adapters/claude-cli/stable-replay.ts +108 -0
  21. package/src/adapters/claude-cli/usage-admission.ts +230 -0
  22. package/src/adapters/coding-agent/protocol.ts +1 -1
  23. package/src/adapters/coding-agent/turn.ts +18 -2
  24. package/src/adapters/dsh-account.ts +97 -0
  25. package/src/adapters/google-progress-history.ts +53 -0
  26. package/src/adapters/google.ts +52 -9
  27. package/src/adapters/openai-chat/messages.ts +4 -3
  28. package/src/adapters/openai-chat/passthrough.ts +8 -2
  29. package/src/adapters/openai-chat/reasoning-replay.ts +52 -0
  30. package/src/adapters/openai-responses/compaction-search-history.ts +14 -7
  31. package/src/adapters/openai-responses/passthrough.ts +57 -125
  32. package/src/adapters/openai-responses/request-strips.ts +25 -3
  33. package/src/adapters/openai-responses/transport.ts +81 -0
  34. package/src/adapters/qoder/adapter.ts +4 -2
  35. package/src/adapters/registry.ts +5 -0
  36. package/src/adapters/zed.ts +26 -1
  37. package/src/advisor/activity.ts +55 -0
  38. package/src/advisor/budget.ts +62 -0
  39. package/src/advisor/consult.ts +4 -1
  40. package/src/advisor/context.ts +16 -4
  41. package/src/advisor/evidence-tool.ts +21 -0
  42. package/src/advisor/evidence.ts +117 -0
  43. package/src/advisor/runtime.ts +79 -31
  44. package/src/advisor/settings.ts +11 -2
  45. package/src/bridge/errors.ts +19 -1
  46. package/src/bridge/internal.ts +7 -1
  47. package/src/bridge/response-json.ts +18 -3
  48. package/src/bridge/sse.ts +59 -8
  49. package/src/bridge/terminal-refusal.ts +59 -0
  50. package/src/claude/desktop-3p-library.ts +32 -7
  51. package/src/claude/desktop-3p.ts +55 -13
  52. package/src/claude/desktop-first-party.ts +1 -1
  53. package/src/claude/desktop-picker-platform.ts +4 -0
  54. package/src/claude/desktop-picker.ts +15 -8
  55. package/src/claude/desktop-system-proxy.ts +50 -4
  56. package/src/claude/inbound-content-options.ts +77 -0
  57. package/src/claude/inbound.ts +33 -0
  58. package/src/claude/intercept/picker-ca-startup.ts +2 -1
  59. package/src/claude/intercept/picker-runtime.ts +6 -5
  60. package/src/claude/intercept/picker-trust-windows.ts +338 -0
  61. package/src/claude/intercept/picker-trust.ts +19 -0
  62. package/src/claude/intercept/port-policy.ts +30 -0
  63. package/src/claude/intercept/runtime.ts +97 -94
  64. package/src/claude/outbound.ts +46 -13
  65. package/src/cli/account-api.ts +1 -0
  66. package/src/cli/account-policy-dto.ts +6 -0
  67. package/src/cli/account-policy.ts +34 -5
  68. package/src/cli/account-preservation.ts +44 -0
  69. package/src/cli/account.ts +8 -2
  70. package/src/cli/advisor.ts +17 -3
  71. package/src/cli/capabilities-accounts.ts +20 -1
  72. package/src/cli/capabilities-agents-routing.ts +5 -2
  73. package/src/cli/capabilities-base.ts +38 -10
  74. package/src/cli/capabilities-observe-system.ts +63 -3
  75. package/src/cli/capabilities-provider-models.ts +11 -0
  76. package/src/cli/codex-cli-update.ts +151 -41
  77. package/src/cli/codex-provider.ts +44 -0
  78. package/src/cli/combo-input.ts +10 -1
  79. package/src/cli/combo-result.ts +1 -1
  80. package/src/cli/combo.ts +2 -1
  81. package/src/cli/cross-home-owner.ts +33 -16
  82. package/src/cli/dispatch.ts +25 -4
  83. package/src/cli/doctor.ts +6 -1
  84. package/src/cli/ensure-desktop-cold-start.ts +60 -7
  85. package/src/cli/help.ts +3 -2
  86. package/src/cli/index.ts +1 -1
  87. package/src/cli/integration-plan-dto.ts +1 -0
  88. package/src/cli/log-history.ts +172 -0
  89. package/src/cli/loopback-family.ts +31 -0
  90. package/src/cli/models-runtime-subcommands.ts +1 -0
  91. package/src/cli/models-runtime.ts +72 -8
  92. package/src/cli/observe-stream.ts +2 -0
  93. package/src/cli/observe.ts +16 -4
  94. package/src/cli/provider.ts +27 -5
  95. package/src/cli/registry.ts +28 -8
  96. package/src/cli/runtime-api.ts +5 -3
  97. package/src/cli/system-command.ts +40 -8
  98. package/src/cli/uninstall-client-state.ts +32 -0
  99. package/src/cli/usage-compare.ts +309 -0
  100. package/src/cli/usage-model-search.ts +4 -4
  101. package/src/cli/usage-report.ts +2 -2
  102. package/src/cli/v2.ts +1 -1
  103. package/src/client/machine-listener.ts +1 -3
  104. package/src/client/runtime.ts +1 -0
  105. package/src/clients/client-path.ts +51 -0
  106. package/src/clients/config-export/contracts.ts +2 -1
  107. package/src/clients/config-export.ts +18 -44
  108. package/src/clients/gentle-shell-path.ts +87 -0
  109. package/src/codex/account-auto-switch.ts +2 -0
  110. package/src/codex/account-identity.ts +29 -0
  111. package/src/codex/account-label.ts +1 -1
  112. package/src/codex/account-lifecycle.ts +21 -5
  113. package/src/codex/account-preservation.ts +28 -0
  114. package/src/codex/account-use-remaining.ts +23 -0
  115. package/src/codex/app-server-processes.ts +97 -7
  116. package/src/codex/app-server-restart-service.ts +55 -4
  117. package/src/codex/auth-api/account-list.ts +4 -0
  118. package/src/codex/auth-api/account-pause-group.ts +6 -50
  119. package/src/codex/auth-api/account-preservation.ts +57 -0
  120. package/src/codex/auth-api/routes.ts +8 -0
  121. package/src/codex/auth-api/use-remaining-route.ts +36 -0
  122. package/src/codex/auth-api.ts +0 -1
  123. package/src/codex/auth-context.ts +2 -2
  124. package/src/codex/catalog/auto-review.ts +20 -3
  125. package/src/codex/catalog/build-entries.ts +9 -1
  126. package/src/codex/catalog/combo-member.ts +14 -5
  127. package/src/codex/catalog/configured-tiers.ts +1 -1
  128. package/src/codex/catalog/gather-capture.ts +2 -0
  129. package/src/codex/catalog/metadata.ts +11 -9
  130. package/src/codex/catalog/model-hints.ts +21 -16
  131. package/src/codex/catalog/model-visibility.ts +3 -1
  132. package/src/codex/catalog/parsing.ts +30 -86
  133. package/src/codex/catalog/provider-models.ts +36 -34
  134. package/src/codex/catalog/routed-gather.ts +14 -13
  135. package/src/codex/cli-install-provenance.ts +105 -17
  136. package/src/codex/cli-update-plan.ts +545 -0
  137. package/src/codex/config-write-lock.ts +23 -2
  138. package/src/codex/desktop-app/picker-freshness.ts +96 -0
  139. package/src/codex/desktop-app-restart.ts +21 -0
  140. package/src/codex/desktop-compatibility/installed-build.ts +24 -6
  141. package/src/codex/desktop-compatibility/routing-preflight.ts +1 -1
  142. package/src/codex/desktop-compatibility/runtime.ts +12 -3
  143. package/src/codex/desktop-compatibility/windows-key-protection.ts +115 -9
  144. package/src/codex/features.ts +85 -45
  145. package/src/codex/inject/history-failure.ts +27 -0
  146. package/src/codex/inject/restore.ts +24 -2
  147. package/src/codex/inject/routing-classify.ts +4 -3
  148. package/src/codex/inject.ts +18 -27
  149. package/src/codex/main-account-hard-lock.ts +30 -1
  150. package/src/codex/native-profile-startup.ts +1 -1
  151. package/src/codex/ocx-compaction-history.ts +12 -10
  152. package/src/codex/paths.ts +86 -1
  153. package/src/codex/provider-routing.ts +174 -0
  154. package/src/codex/refresh.ts +2 -2
  155. package/src/codex/routing/health-store.ts +1 -1
  156. package/src/codex/routing/selection.ts +4 -3
  157. package/src/codex/routing/thread-affinity.ts +1 -1
  158. package/src/codex/signed-in-identity.ts +43 -0
  159. package/src/codex/subagent-model-fallback.ts +1 -1
  160. package/src/codex/sync.ts +6 -1
  161. package/src/codex/user-identity.ts +16 -5
  162. package/src/codex/websocket-activity.ts +161 -0
  163. package/src/codex/websocket-registry.ts +25 -1
  164. package/src/codex/windows-local-appdata.ts +84 -0
  165. package/src/combos/failover.ts +6 -1
  166. package/src/combos/index.ts +1 -0
  167. package/src/combos/resolve.ts +46 -10
  168. package/src/combos/types.ts +15 -0
  169. package/src/config/diagnostics.ts +33 -6
  170. package/src/config/live-reconcile.ts +38 -2
  171. package/src/config/load-degrade.ts +17 -0
  172. package/src/config/opencode-wire-upgrade.ts +54 -0
  173. package/src/config/opencode-zen-upgrade.ts +53 -0
  174. package/src/config/owner-registry.ts +22 -17
  175. package/src/config/provider-validation.ts +45 -2
  176. package/src/config/schema/config-schema.ts +22 -0
  177. package/src/config/schema/leaf-validators.ts +38 -9
  178. package/src/config/schema/main-account-hard-lock-fallback.ts +26 -0
  179. package/src/config/schema/native-reasoning-retention.ts +26 -0
  180. package/src/config.ts +2 -0
  181. package/src/generated/compatibility-version.json +657 -297
  182. package/src/grok/sync.ts +1 -1
  183. package/src/images/loop.ts +116 -27
  184. package/src/integrations/catalog-refresh.ts +1 -1
  185. package/src/integrations/mutation-plan.ts +1 -0
  186. package/src/integrations/registry.ts +8 -0
  187. package/src/lib/codex-restart-contract.ts +15 -0
  188. package/src/lib/desktop-upstream-tunnel.ts +18 -53
  189. package/src/lib/empty-tool-catalog.ts +62 -0
  190. package/src/lib/errors.ts +19 -3
  191. package/src/lib/gcp-adc.ts +96 -9
  192. package/src/lib/gcp-credential-input.ts +12 -0
  193. package/src/lib/lab-live-route-production.ts +5 -0
  194. package/src/lib/opaque-proxy-tunnel.ts +217 -0
  195. package/src/lib/redact.ts +2 -2
  196. package/src/lib/request-failure-model.ts +2 -0
  197. package/src/oauth/account-quota-rank.ts +69 -2
  198. package/src/oauth/anthropic-account-refusal.ts +20 -2
  199. package/src/oauth/anthropic-oauth-definitions.ts +3 -1
  200. package/src/oauth/anthropic-routing.ts +1 -21
  201. package/src/oauth/anthropic.ts +38 -7
  202. package/src/oauth/callback-server.ts +27 -1
  203. package/src/oauth/chatgpt-device.ts +1 -1
  204. package/src/oauth/chatgpt-shared.ts +83 -0
  205. package/src/oauth/chatgpt.ts +8 -72
  206. package/src/oauth/dsh.ts +203 -0
  207. package/src/oauth/generic-account-failover.ts +40 -11
  208. package/src/oauth/generic-session-affinity.ts +125 -0
  209. package/src/oauth/index.ts +184 -109
  210. package/src/oauth/local-token-detect.ts +21 -1
  211. package/src/oauth/login-cli-errors.ts +44 -0
  212. package/src/oauth/login-cli.ts +69 -7
  213. package/src/oauth/login-flow-state.ts +371 -23
  214. package/src/oauth/meta-muse-errors.ts +41 -0
  215. package/src/oauth/meta-muse-keychain.ts +52 -0
  216. package/src/oauth/meta-muse.ts +51 -84
  217. package/src/oauth/model-discovery-auth.ts +3 -0
  218. package/src/oauth/pool-settings-capability.ts +4 -2
  219. package/src/oauth/refresh-flight-retirement.ts +48 -0
  220. package/src/oauth/session-affinity-key.ts +23 -0
  221. package/src/oauth/store-anthropic-instance.ts +9 -2
  222. package/src/oauth/store.ts +12 -5
  223. package/src/protocols/encoders/adapter-events.ts +6 -5
  224. package/src/protocols/encoders/messages.ts +9 -6
  225. package/src/protocols/provider-summary.ts +2 -1
  226. package/src/providers/api-key-cooldowns.ts +49 -0
  227. package/src/providers/api-key-resolve.ts +11 -6
  228. package/src/providers/api-key-response-ownership.ts +57 -0
  229. package/src/providers/api-key-selection.ts +31 -6
  230. package/src/providers/api-keys.ts +214 -3
  231. package/src/providers/claude-cli-usage.ts +96 -0
  232. package/src/providers/dsh-account-destination.ts +31 -0
  233. package/src/providers/dsh-account-models.ts +41 -0
  234. package/src/providers/gcp-credential-cleanup.ts +59 -0
  235. package/src/providers/key-failover.ts +54 -50
  236. package/src/providers/key-store.ts +7 -2
  237. package/src/providers/model-rename-fields.ts +3 -0
  238. package/src/providers/model-rename-migration.ts +2 -2
  239. package/src/providers/openai-tiers-destination.ts +11 -2
  240. package/src/providers/openai-tiers.ts +5 -12
  241. package/src/providers/opencode-go-transport.ts +3 -1
  242. package/src/providers/opencode-zen-rate-limit.ts +7 -2
  243. package/src/providers/quota/account-cache.ts +6 -5
  244. package/src/providers/quota/account-row.ts +34 -0
  245. package/src/providers/quota/antigravity.ts +28 -7
  246. package/src/providers/quota/dsh-account.ts +70 -0
  247. package/src/providers/quota/vendor-probes-oauth.ts +1 -93
  248. package/src/providers/quota-types.ts +2 -0
  249. package/src/providers/quota.ts +14 -20
  250. package/src/providers/registry/entries-core.ts +37 -8
  251. package/src/providers/registry/types.ts +1 -1
  252. package/src/providers/request-pacing.ts +77 -19
  253. package/src/providers/routed-config.ts +339 -0
  254. package/src/providers/service-tier.ts +2 -1
  255. package/src/quota/window-mapping.ts +5 -2
  256. package/src/responses/anthropic-progress.ts +34 -0
  257. package/src/responses/assistant-replay.ts +35 -0
  258. package/src/responses/compaction-history-framing.ts +65 -0
  259. package/src/responses/compaction.ts +6 -1
  260. package/src/responses/incomplete-reason.ts +31 -0
  261. package/src/responses/parser.ts +50 -5
  262. package/src/responses/reasoning-envelope.ts +4 -0
  263. package/src/responses/reasoning-replay-cache.ts +74 -6
  264. package/src/responses/reasoning-retention.ts +173 -0
  265. package/src/responses/retained-compaction.ts +46 -0
  266. package/src/responses/schema.ts +23 -0
  267. package/src/router.ts +8 -321
  268. package/src/routing/analytics.ts +7 -15
  269. package/src/routing/health.ts +37 -22
  270. package/src/routing/history/indexer.ts +6 -5
  271. package/src/routing/history/ingest.ts +6 -2
  272. package/src/routing/profile.ts +1 -1
  273. package/src/server/admission-model-scope.ts +17 -0
  274. package/src/server/auth-cors.ts +11 -0
  275. package/src/server/chat-completions.ts +7 -1
  276. package/src/server/chat-native-reasoning.ts +123 -0
  277. package/src/server/chat-native-sse.ts +110 -7
  278. package/src/server/chat-native.ts +76 -17
  279. package/src/server/claude-messages.ts +56 -414
  280. package/src/server/effort-policy.ts +1 -1
  281. package/src/server/images.ts +15 -2
  282. package/src/server/index/live-sideband.ts +3 -2
  283. package/src/server/index/model-alias-id.ts +36 -0
  284. package/src/server/index/optional-listeners.ts +2 -0
  285. package/src/server/index/serve-options.ts +20 -6
  286. package/src/server/index/startup-warnings.ts +2 -0
  287. package/src/server/index/websocket-handler.ts +18 -4
  288. package/src/server/index.ts +8 -8
  289. package/src/server/inference/advisor-diagnostics.ts +31 -0
  290. package/src/server/lifecycle.ts +13 -1
  291. package/src/server/management/advisor-routes.ts +15 -4
  292. package/src/server/management/claude-desktop-picker-routes.ts +1 -1
  293. package/src/server/management/combo-routes.ts +176 -50
  294. package/src/server/management/companion-routes.ts +1 -1
  295. package/src/server/management/config-routes.ts +75 -4
  296. package/src/server/management/cursor-integration-routes.ts +1 -1
  297. package/src/server/management/model-routes.ts +11 -6
  298. package/src/server/management/model-rows.ts +14 -0
  299. package/src/server/management/native-integration-routes.ts +2 -7
  300. package/src/server/management/native-reasoning-retention-routes.ts +41 -0
  301. package/src/server/management/oauth-account-routes.ts +87 -14
  302. package/src/server/management/provider-gcp-credential-routes.ts +29 -0
  303. package/src/server/management/provider-key-pacing-routes.ts +31 -0
  304. package/src/server/management/provider-overwrite-carry.ts +7 -0
  305. package/src/server/management/provider-routes.ts +25 -30
  306. package/src/server/management/route-registry.ts +10 -1
  307. package/src/server/management-api.ts +3 -1
  308. package/src/server/messages-native-tool-guard.ts +185 -0
  309. package/src/server/messages-native.ts +117 -24
  310. package/src/server/messages-shared.ts +431 -0
  311. package/src/server/relay.ts +1 -1
  312. package/src/server/request-log-terminal-status.ts +1 -1
  313. package/src/server/request-log.ts +6 -1
  314. package/src/server/responses/account-change-state.ts +8 -2
  315. package/src/server/responses/adapter-continuation.ts +47 -19
  316. package/src/server/responses/adapter-delivery.ts +72 -12
  317. package/src/server/responses/adapter-dispatch.ts +46 -3
  318. package/src/server/responses/advisor-retention.ts +117 -0
  319. package/src/server/responses/advisor-slot.ts +73 -41
  320. package/src/server/responses/anthropic-thinking-recovery.ts +153 -0
  321. package/src/server/responses/codex-ws-wire.ts +15 -8
  322. package/src/server/responses/combo-stream-preflight.ts +3 -2
  323. package/src/server/responses/compact.ts +303 -125
  324. package/src/server/responses/compaction-recovery.ts +20 -10
  325. package/src/server/responses/compaction-retention.ts +61 -0
  326. package/src/server/responses/context-overflow.ts +11 -6
  327. package/src/server/responses/core-auth.ts +6 -0
  328. package/src/server/responses/core-codex-account.ts +11 -1
  329. package/src/server/responses/core-combo-failure.ts +2 -0
  330. package/src/server/responses/core-combo.ts +121 -36
  331. package/src/server/responses/core-errors.ts +3 -3
  332. package/src/server/responses/core-lifetime.ts +8 -4
  333. package/src/server/responses/core-opaque-recovery.ts +35 -0
  334. package/src/server/responses/core-options.ts +7 -0
  335. package/src/server/responses/core-pipeline.ts +141 -0
  336. package/src/server/responses/core-replay.ts +23 -3
  337. package/src/server/responses/core.ts +2 -115
  338. package/src/server/responses/fetch-helpers.ts +73 -19
  339. package/src/server/responses/main-hard-lock-routing.ts +109 -0
  340. package/src/server/responses/native-response-control.ts +4 -2
  341. package/src/server/responses/passthrough-delivery.ts +3 -1
  342. package/src/server/responses/passthrough-dispatch.ts +15 -13
  343. package/src/server/responses/passthrough-execution.ts +3 -2
  344. package/src/server/responses/request-prepare.ts +77 -12
  345. package/src/server/responses/request-sidecar-auth.ts +2 -2
  346. package/src/server/responses/request-transport.ts +104 -27
  347. package/src/server/responses/response-effects.ts +2 -0
  348. package/src/server/responses/response-tool-ownership.ts +21 -0
  349. package/src/server/responses/run-turn-execution.ts +96 -30
  350. package/src/server/responses/sidecar-execution.ts +63 -9
  351. package/src/server/responses/sidecar-send-budget.ts +2 -2
  352. package/src/server/responses/terminal-guard.ts +2 -16
  353. package/src/server/responses-undeclared-tool-guard.ts +1 -1
  354. package/src/server/ws-bridge.ts +2 -0
  355. package/src/service/auth-preflight.ts +71 -0
  356. package/src/service/cli.ts +4 -2
  357. package/src/service/definition.ts +97 -0
  358. package/src/service/guards.ts +1 -61
  359. package/src/service/health-sentinel-linux.ts +112 -0
  360. package/src/service/health-sentinel-main.ts +76 -57
  361. package/src/service/health.ts +5 -89
  362. package/src/service/launchd.ts +27 -7
  363. package/src/service/logging.ts +72 -9
  364. package/src/service/repair.ts +2 -1
  365. package/src/service/state.ts +7 -8
  366. package/src/service/systemd.ts +1 -1
  367. package/src/service/windows-guarded-manager.ts +2 -1
  368. package/src/service/windows-ops.ts +1 -1
  369. package/src/service/windows-scheduler.ts +2 -7
  370. package/src/service/windows-taskxml.ts +2 -2
  371. package/src/service/windows-wscript.ts +14 -0
  372. package/src/service.ts +4 -2
  373. package/src/sidecar/auth.ts +2 -0
  374. package/src/types/config.ts +64 -3
  375. package/src/types/opencode-go.ts +61 -0
  376. package/src/types/opencode-zen.ts +35 -0
  377. package/src/types/provider.ts +32 -6
  378. package/src/types/request.ts +31 -2
  379. package/src/types/wire.ts +31 -37
  380. package/src/types.ts +2 -0
  381. package/src/update/npm-invocation.mjs +12 -2
  382. package/src/usage/cost.ts +48 -12
  383. package/src/usage/expected-prices.ts +3 -0
  384. package/src/usage/ledger-retention.ts +2 -1
  385. package/src/usage/ledger-scanner.ts +6 -5
  386. package/src/usage/log.ts +17 -8
  387. package/src/usage/summary.ts +30 -11
  388. package/src/usage/telemetry-contract.ts +1 -0
  389. package/src/usage/user-cost-overlays.ts +14 -19
  390. package/src/usage/user-cost-validation.ts +38 -0
  391. package/src/vision/eligibility.ts +13 -0
  392. package/src/web-search/anthropic-history.ts +59 -0
  393. package/src/web-search/loop.ts +236 -73
  394. package/src/web-search/progress-stream.ts +15 -2
  395. package/src/web-search/run-turn-loop.ts +9 -1
  396. package/gui/dist/assets/App-BjlttVLy.js +0 -58
  397. package/gui/dist/assets/App-CSJvmjNs.css +0 -1
  398. package/gui/dist/assets/index-BB6uK6tU.css +0 -1
  399. package/gui/dist/assets/index-BMssP4k3.js +0 -94
  400. package/gui/dist/assets/quota-summary-D8M1bc_R.js +0 -1
@@ -1,4 +1,5 @@
1
1
  import type { AdapterEvent, OcxConfig } from "../../types";
2
+ import { carryResponseToolOwnership } from "./response-tool-ownership";
2
3
  import { routeConcreteModel, type RouteResult } from "../../router";
3
4
  import { copyPlainData } from "../../lib/plain-data";
4
5
  import { jsonUtf8Bytes } from "../../lib/json-byte-size";
@@ -7,9 +8,11 @@ import { readBoundedResponseBytes } from "../../lib/bounded-body";
7
8
  import { isRequestExecutionBudget, type RequestExecutionBudget, type SingleUseDispatchPermit } from "../../lib/request-execution-budget";
8
9
  import { isNonReplayableResponse, isNonReplayableUpstreamCode, markResponseNonReplayable, TRANSIENT_RETRY_MAX_ATTEMPTS } from "../../lib/upstream-retry";
9
10
  import { isCyberPolicyCode, isTerminalRefusalCode } from "../../lib/errors";
10
- import { isCanonicalOpenAiForwardProvider, supportsNativeResponsesCompactEndpoint } from "../../providers/openai-tiers";
11
+ import { isCanonicalOpenAiForwardProvider, supportsNativeResponsesCompactEndpoint, supportsNativeResponsesCompactionTrigger } from "../../providers/openai-tiers";
11
12
  import { bridgeToResponsesSSE, formatErrorResponse } from "../../bridge";
12
13
  import { buildCompactV1Output, decodeCompactionSummary, encodeCompactionSummary, extractCompactUserMessages } from "../../responses/compaction";
14
+ import { decodeRetainedCompaction, encodeRetainedCompaction } from "../../responses/retained-compaction";
15
+ import { collectReasoningTexts } from "../../responses/reasoning-retention";
13
16
  import { finishRequestAttempt, usageFromResponsesPayload, type RequestLogContext } from "../request-log";
14
17
  import { linkRequestSessionLane } from "../request-log-conversation";
15
18
  import { isNativePassthroughSseResponse, markNativePassthroughSseResponse, isEagerRelaySseResponse, markEagerRelaySseResponse } from "../relay";
@@ -55,15 +58,17 @@ function portableBody(body: Record<string, unknown>): boolean {
55
58
  if (!Array.isArray(body.input) || body.store === true || conversationCarriesUploadedFiles(body)) return false;
56
59
  // Native ciphertext cannot be summarized by another provider. Never silently replace it with a note.
57
60
  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;
61
+ && typeof item.encrypted_content === "string" && !item.encrypted_content.startsWith("ocx1:") && !decodeRetainedCompaction(item.encrypted_content))) return false;
59
62
  const input = body.input.filter(item => !record(item) || item.type !== "compaction_trigger");
60
63
  return selfContainedResponsesBody({ ...body, store: false, input });
61
64
  }
62
65
 
63
- function routed(route: RouteResult): boolean {
66
+ function routed(route: RouteResult, kind: "compaction-v1" | "compaction-v2"): boolean {
64
67
  return !route.combo && route.routeKind !== "policy" && route.routeReason !== "default-provider"
65
68
  && !isCanonicalOpenAiForwardProvider(route.provider)
66
- && !supportsNativeResponsesCompactEndpoint(route.providerName, route.provider);
69
+ && !(kind === "compaction-v1"
70
+ ? supportsNativeResponsesCompactEndpoint(route.providerName, route.provider)
71
+ : supportsNativeResponsesCompactionTrigger(route.provider));
67
72
  }
68
73
 
69
74
  /** One reader, bounded bytes, and an exact replacement body; never clone a live stream. */
@@ -74,6 +79,7 @@ async function bufferedJson(response: Response, signal: AbortSignal): Promise<{
74
79
  headers.delete("content-length");
75
80
  headers.delete("content-encoding");
76
81
  const replacement = new Response(bytes.bytes, { status: response.status, statusText: response.statusText, headers });
82
+ carryResponseToolOwnership(response, replacement);
77
83
  if (isNonReplayableResponse(response)) markResponseNonReplayable(replacement);
78
84
  try {
79
85
  const json: unknown = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes.bytes));
@@ -89,6 +95,7 @@ export async function runWithCompactionRecovery(
89
95
  req: Request, config: OcxConfig, logCtx: RequestLogContext, options: Options, dispatch: Dispatch,
90
96
  ): Promise<Response> {
91
97
  const recovery = readCompactionRecoveryConfig(config.compactionRecovery);
98
+ const kind = options.compactionRecoveryKind ?? "compaction-v2";
92
99
  if (!recovery || options.compactionRecoveryAttempted || options.comboAttempt || (options.inboundWire && options.inboundWire !== "responses")) {
93
100
  return dispatch(req, config, logCtx, options);
94
101
  }
@@ -132,7 +139,7 @@ export async function runWithCompactionRecovery(
132
139
  },
133
140
  onCompactionRecoveryRoute(route) {
134
141
  options.onCompactionRecoveryRoute?.(route);
135
- if (snapshot && routed(route)) sourceRoute = { ...route };
142
+ if (snapshot && routed(route, kind)) sourceRoute = { ...route };
136
143
  },
137
144
  onCompactionRecoveryAdapterEvent(event) {
138
145
  options.onCompactionRecoveryAdapterEvent?.(event);
@@ -151,7 +158,7 @@ export async function runWithCompactionRecovery(
151
158
  if (!snapshot || !sourceRoute || signal.aborted || req.signal.aborted || isNonReplayableResponse(response)) return keep(response);
152
159
  let target: RouteResult;
153
160
  try { target = routeConcreteModel(config, recovery.model); } catch { return keep(response); }
154
- if (!routed(target) || identity(sourceRoute) === identity(target)) return keep(response);
161
+ if (!routed(target, kind) || identity(sourceRoute) === identity(target)) return keep(response);
155
162
  const originalModel = firstOptions.compactionRoutingOverride?.sourceModel ?? String(snapshot.model);
156
163
  // Use the established protocol commit boundary. For runTurn streams the direct event
157
164
  // observer additionally preserves side-effect heartbeats that the bridge does not publish.
@@ -289,7 +296,8 @@ export async function runWithCompactionRecovery(
289
296
  const compactions = items.filter(value => record(value) && value.type === "compaction");
290
297
  const permitted = items.every(value => record(value) && (value.type === "compaction" || value.type === "reasoning"));
291
298
  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;
299
+ const structured = item && typeof item.encrypted_content === "string" ? decodeRetainedCompaction(item.encrypted_content) : null;
300
+ const summary = structured?.summary ?? (item && typeof item.encrypted_content === "string" ? decodeCompactionSummary(item.encrypted_content) : null);
293
301
  if (json?.status !== "completed" || !summary?.trim()) {
294
302
  void completed.response.body?.cancel().catch(() => undefined);
295
303
  restoreSourceLog?.(completed.response.status);
@@ -298,11 +306,13 @@ export async function runWithCompactionRecovery(
298
306
  // v1 unpacks this text through buildCompactV1Output, which already re-adds retained user
299
307
  // messages as items; embedding them here too would duplicate the same text in the output.
300
308
  const preserved = options.compactionRecoveryKind === "compaction-v1" ? summary : (() => {
301
- const retained = buildCompactV1Output(extractCompactUserMessages(snapshot.input), summary).slice(0, -1);
309
+ const usersOnly = Array.isArray(snapshot.input) ? snapshot.input.filter(item => collectReasoningTexts([item]).length === 0) : snapshot.input;
310
+ const retained = buildCompactV1Output(extractCompactUserMessages(usersOnly), summary).slice(0, -1);
302
311
  const userText = extractCompactUserMessages(retained).map((text, index) => `User message ${index + 1}:\n${text}`).join("\n\n");
303
312
  return `${summary}\n\nRetained original user messages (verbatim; preserve their goals and constraints):\n${userText}`;
304
313
  })();
305
- item!.encrypted_content = encodeCompactionSummary(preserved);
314
+ const encryptedContent = structured ? encodeRetainedCompaction(preserved, structured.reasoning) : encodeCompactionSummary(preserved);
315
+ item!.encrypted_content = encryptedContent;
306
316
  json.model = originalModel;
307
317
  void completed.response.body?.cancel().catch(() => undefined);
308
318
  void response.body?.cancel().catch(() => undefined);
@@ -310,7 +320,7 @@ export async function runWithCompactionRecovery(
310
320
  const usage = usageFromResponsesPayload(json.usage);
311
321
  async function* events(): AsyncGenerator<AdapterEvent> {
312
322
  yield { type: "text_delta", text: preserved };
313
- yield { type: "done", ...(usage ? { usage } : {}) };
323
+ yield { type: "done", compactionEncryptedContent: encryptedContent, ...(usage ? { usage } : {}) };
314
324
  }
315
325
  return new Response(bridgeToResponsesSSE(events(), originalModel, undefined, undefined, undefined, undefined, 2_000,
316
326
  { 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 ? "oauth-account-429"
979
+ : outcomeStatus === 402 ? "oauth-account-402"
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,49 @@ 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. The rotator only peeks round-robin here: nothing is sent to that sibling.
148
+ * A lone account or an all-cooled pool returns null and cools the target.
149
+ */
150
+ function cooledGenericOAuthAccountLabel(
151
+ config: OcxConfig, providerName: string, modelId: string, label: string | undefined, retryAfter: string | undefined,
152
+ ): string | undefined {
153
+ const provider = config.providers[providerName];
154
+ if (!label || !provider || !isGenericFailoverProvider(providerName, provider)) return undefined;
155
+ const base = baseProviderLabel(providerName);
156
+ const matches = getAccountSet(providerName)?.accounts.filter(account =>
157
+ oauthAccountLogLabel(account.id, base) === label) ?? [];
158
+ return matches.length === 1
159
+ && rotateGenericOAuthAccountOn429(config, providerName, matches[0]!.id, retryAfter, Date.now(), modelId, false)
160
+ ? label : undefined;
161
+ }
162
+
163
+ /**
164
+ * A 402 credit-balance refusal names one Anthropic OAuth account -- the evidence the pool already
165
+ * accepts on a 403 -- but the pool rotates only on 401/403/429, so the 402 reaches the combo with
166
+ * that account still selectable. Cool it under the pool's entitlement backoff and spare the target
167
+ * only while a sibling is eligible; a lone account or an exhausted pool still cools the target.
168
+ */
169
+ function creditRefusedAnthropicAccountLabel(
170
+ config: OcxConfig, providerName: string, modelId: string, label: string | undefined,
171
+ message: string, code: string | null | undefined, response: Response,
172
+ ): string | undefined {
173
+ const instance = configuredAnthropicInstance(config, providerName);
174
+ if (!label || !instance || config.providers[providerName]?.authMode !== "oauth"
175
+ || !isAnthropicBillingFailure(402, message, code)) return undefined;
176
+ const routing = anthropicRoutingFor(instance);
177
+ const matches = getAccountSet(instance)?.accounts.filter(account =>
178
+ formatAnthropicProviderForLog(instance, account.id) === label) ?? [];
179
+ return matches.length === 1
180
+ && anthropicRefusalCredentialIsCurrent(response, instance, matches[0]!.id)
181
+ && routing.recordAnthropicAccountRefusal(config, matches[0]!.id, 403, null)
182
+ && routing.getEligibleAnthropicAccounts(Date.now(), modelId).length > 0
183
+ ? label : undefined;
184
+ }
138
185
 
139
186
  /**
140
187
  * True only for the Anthropic OAuth pool's OWN "all accounts cooled" 429. Its Retry-After is the
@@ -251,6 +298,30 @@ interface JevComboChoice {
251
298
  candidate: JevCandidate;
252
299
  }
253
300
 
301
+ /** A target's JEV effort choices, or undefined when an explicit allowlist has none still supported. */
302
+ function jevChoiceEfforts(
303
+ config: OcxConfig,
304
+ target: NonNullable<ReturnType<typeof getCombo>>["targets"][number],
305
+ ): OcxComboDefaultEffort[] | undefined {
306
+ let ladder: string[] | undefined;
307
+ try {
308
+ const route = routeConcreteModel(config, targetKey(target));
309
+ ladder = supportedLadderFor({ provider: route.provider, modelId: route.modelId });
310
+ } catch {
311
+ // Preserve the existing routing-failure surface. Unknown capability becomes the explicit
312
+ // no-effort choice rather than broadening JEV's effort allowlist.
313
+ ladder = undefined;
314
+ }
315
+ const supportedEfforts = (ladder ?? []).filter(isCodexReasoningEffort) as OcxComboDefaultEffort[];
316
+ const configuredEfforts = target.reasoningEfforts;
317
+ const reasoningEfforts = configuredEfforts === undefined
318
+ ? supportedEfforts
319
+ : configuredEfforts.filter(effort => supportedEfforts.includes(effort));
320
+ // An explicit allowlist is restrictive. If catalog capabilities drift until no configured
321
+ // effort remains supported, omit the target instead of silently broadening JEV's choices.
322
+ return configuredEfforts !== undefined && reasoningEfforts.length === 0 ? undefined : reasoningEfforts;
323
+ }
324
+
254
325
  /** Enumerate the current ordinary Combo eligibility set without retaining attempted picks. */
255
326
  function eligibleJevComboChoices(
256
327
  config: OcxConfig,
@@ -269,23 +340,8 @@ function eligibleJevComboChoices(
269
340
  excluded.add(key);
270
341
  // The TypeSafe row owns a decision credential, not an inference transport.
271
342
  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;
343
+ const reasoningEfforts = jevChoiceEfforts(config, pick.target);
344
+ if (!reasoningEfforts) continue;
289
345
  choices.push({
290
346
  pick: { ...pick, attempted: [key] },
291
347
  candidate: {
@@ -317,9 +373,10 @@ export async function executeComboResponses(
317
373
  options: HandleResponsesOptions & { translatorBudget: TranslatorBudget },
318
374
  requestDispatchers: ResponsesDispatchers,
319
375
  ): Promise<Response> {
320
- const requestedModel = typeof (rawBody as { model?: unknown } | null)?.model === "string"
321
- ? (rawBody as { model: string }).model
322
- : `combo/${comboId}`;
376
+ const requestedModel = options.mainHardLockFallbackOverride?.sourceModel
377
+ ?? (typeof (rawBody as { model?: unknown } | null)?.model === "string"
378
+ ? (rawBody as { model: string }).model
379
+ : `combo/${comboId}`);
323
380
  Object.assign(logCtx, {
324
381
  requestedModel,
325
382
  model: requestedModel,
@@ -449,8 +506,10 @@ export async function executeComboResponses(
449
506
  let comboPayloadReadable = false;
450
507
  const payloadEligible = (target: (typeof combo.targets)[number]): boolean =>
451
508
  comboPayloadReadable || !unreadableEncryptedAgentTask || canDecryptUnreadableAgentTask(target);
509
+ const meteredAllowed = admissionMayUseMeteredComboTargets(config, options.admission, combo);
452
510
  const targetEligible = (target: (typeof combo.targets)[number]): boolean =>
453
- (combo.strategy !== "jev" || target.provider !== JEV_PROVIDER_ID)
511
+ (meteredAllowed || !target.metered)
512
+ && (combo.strategy !== "jev" || target.provider !== JEV_PROVIDER_ID)
454
513
  && payloadEligible(target)
455
514
  && reasoningReplayEligible(target)
456
515
  && (protocolLanes?.pickable(target) ?? true);
@@ -524,6 +583,9 @@ export async function executeComboResponses(
524
583
  now?: number;
525
584
  }) => pickComboTargetWithWait(config, comboId, {
526
585
  ...pickOptions,
586
+ // Every pick, including the recovery picks that skip `targetEligible`, keeps metered
587
+ // targets away from callers that may not spend on them.
588
+ eligible: target => (meteredAllowed || !target.metered) && (pickOptions.eligible?.(target) ?? true),
527
589
  waitForCooldownMs: combo.waitForCooldownMs,
528
590
  abortSignal: options.abortSignal,
529
591
  });
@@ -543,7 +605,8 @@ export async function executeComboResponses(
543
605
  );
544
606
  return options.abortSignal?.aborted
545
607
  ? clientCancelledResponse()
546
- : comboUnavailable(comboId);
608
+ : comboUnavailable(comboId, undefined,
609
+ comboExclusionReasons(config, comboId, target => meteredAllowed || !target.metered));
547
610
  }
548
611
  if (!(await recoverUnreadableEncryptedTask())) {
549
612
  return options.abortSignal?.aborted
@@ -559,13 +622,16 @@ export async function executeComboResponses(
559
622
  if (onlyReplayIncompatibleTargetsRemain()) return targetIncompatibleResponse();
560
623
  return options.abortSignal?.aborted
561
624
  ? clientCancelledResponse()
562
- : comboUnavailable(comboId);
625
+ : comboUnavailable(comboId, undefined, comboExclusionReasons(config, comboId, targetEligible));
563
626
  }
564
627
  let jevDecision: JevDecision | undefined;
565
628
  if (combo.strategy === "jev") {
566
629
  const choices = eligibleJevComboChoices(config, comboId, targetEligible, Date.now());
567
630
  const first = choices[0];
568
- if (!first) return comboUnavailable(comboId);
631
+ if (!first) {
632
+ return comboUnavailable(comboId, undefined, comboExclusionReasons(config, comboId, target =>
633
+ targetEligible(target) && target.provider !== JEV_PROVIDER_ID && jevChoiceEfforts(config, target) !== undefined));
634
+ }
569
635
  const resolvedFailOpenEffort = resolveEffortAtOrBelow(
570
636
  "medium",
571
637
  first.candidate.reasoningEfforts,
@@ -679,6 +745,9 @@ export async function executeComboResponses(
679
745
  // is gone, so carry the loop's own classification decision instead of re-deriving a
680
746
  // weaker one from the status alone (#4149).
681
747
  let lastFailureClassifiesOverflow = false;
748
+ // Local input admission refusals (#1524), while every failed target so far was one: such a
749
+ // target sent nothing, so an exhausted ladder must not be reported as a provider rejection.
750
+ let localAdmissionRefusals: string[] | undefined = [];
682
751
  // The first quota/rate-limit refusal of the ladder. A fallback that then refuses its own
683
752
  // credential or plan (401/403, or a 400 the ladder hopped past) says nothing about why the
684
753
  // request failed: the primary target ran out. Returning that fallback refusal told clients
@@ -853,7 +922,8 @@ export async function executeComboResponses(
853
922
  // The live config can change while the child is streaming. Never retain credentials.
854
923
  const currentCombo = getCombo(config, comboId);
855
924
  const provider = config.providers[completedTarget.provider];
856
- if (!options.compactionRoutingOverride && Object.hasOwn(config.providers, completedTarget.provider)
925
+ if (!options.compactionRoutingOverride && !options.mainHardLockFallbackOverride
926
+ && Object.hasOwn(config.providers, completedTarget.provider)
857
927
  && provider && provider.disabled !== true
858
928
  && currentCombo?.targets.some(target => targetKey(target) === targetKey(completedTarget))) {
859
929
  rememberComboForLane(sessionLaneIdFromRequest(req.headers), comboId, completedTarget, model, writerGeneration);
@@ -1045,6 +1115,12 @@ export async function executeComboResponses(
1045
1115
  Date.now() - started,
1046
1116
  failure.usage,
1047
1117
  );
1118
+ if (failure.upstreamCode === "input_admission_refused" && attempt.sendCount === 0) {
1119
+ attempt.errorCode = failure.upstreamCode;
1120
+ localAdmissionRefusals?.push(failure.upstreamMessage ?? "");
1121
+ } else {
1122
+ localAdmissionRefusals = undefined;
1123
+ }
1048
1124
  attemptRetained = true;
1049
1125
  lastFailure = failure.response;
1050
1126
  if (!quotaFailure && (lastFailure.status === 429 || lastFailure.status === 402)) {
@@ -1128,12 +1204,16 @@ export async function executeComboResponses(
1128
1204
  const failedTargetKey = targetKey(pick.target);
1129
1205
  let failedTargetCooldownRecorded = false;
1130
1206
  // 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
- );
1207
+ const poolAccountLabel = poolAccountProviderLabel(childLog.provider, pick.target.provider);
1208
+ const failedAccount = failure.response.status === 402
1209
+ ? creditRefusedAnthropicAccountLabel(
1210
+ config, pick.target.provider, pick.target.model, poolAccountLabel, failure.classificationText, failure.upstreamCode,
1211
+ failure.response,
1212
+ )
1213
+ : cooledPoolAccountLabel(config, pick.target.provider, pick.target.model, poolAccountLabel)
1214
+ ?? (failure.response.status === 429 && failureCooldownScope === "target"
1215
+ ? cooledGenericOAuthAccountLabel(config, pick.target.provider, pick.target.model, childLog.accountLogLabel, failure.retryAfter)
1216
+ : undefined);
1137
1217
  // The target still cools on the pool's own refusal (the anti-hammer guard), but for the local
1138
1218
  // fallback rather than the pool's restated per-account Retry-After.
1139
1219
  const poolLocalRefusal = isAnthropicPoolLocalRefusal(
@@ -1225,9 +1305,14 @@ export async function executeComboResponses(
1225
1305
  lastFailure?.status === 413
1226
1306
  && lastFailureClassifiesOverflow
1227
1307
  ) {
1308
+ if (localAdmissionRefusals?.length) logCtx.errorCode = "input_admission_refused";
1309
+ const localMessage = localAdmissionRefusals?.length
1310
+ ? `opencodex refused this turn locally; nothing was sent upstream. No target of combo ${comboId} `
1311
+ + `that was tried can fit its input: ${localAdmissionRefusals.join(" ")} Compact the conversation before retrying.`
1312
+ : undefined;
1228
1313
  return (rawBody as { stream?: unknown } | null)?.stream === true
1229
- ? streamingContextOverflowResponse(requestedModel, options.translatorBudget)
1230
- : jsonContextOverflowResponse();
1314
+ ? streamingContextOverflowResponse(requestedModel, options.translatorBudget, localMessage)
1315
+ : jsonContextOverflowResponse(localMessage);
1231
1316
  }
1232
1317
  return failure;
1233
1318
  }
@@ -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
  }
@@ -1,4 +1,5 @@
1
1
  import type { TranslatorBudget } from "../../lib/translator-budget";
2
+ import { carryResponseToolOwnership } from "./response-tool-ownership";
2
3
  import { finalizeTranslatorBudgetResponse } from "../../lib/translator-budget";
3
4
  import { isNonReplayableResponse, markResponseNonReplayable } from "../../lib/upstream-retry";
4
5
  import {
@@ -48,11 +49,13 @@ export function finalizeOwnedTranslatorBudget(response: Response, budget: Transl
48
49
  markEagerRelaySseResponse(finalizedResponse);
49
50
  }
50
51
  if (isPreinspectedJsonResponse(response)) markPreinspectedJsonResponse(finalizedResponse);
51
- return finalizedResponse;
52
+ if (isNonReplayableResponse(response)) markResponseNonReplayable(finalizedResponse);
53
+ if (runTurnAdapterSseResponses.has(response)) runTurnAdapterSseResponses.add(finalizedResponse);
54
+ return carryResponseToolOwnership(response, finalizedResponse);
52
55
  }
53
56
 
54
57
  /** Release a serving-account slot when the client body ends, errors or is cancelled. */
55
- export function finalizeAccountLease(response: Response, release: () => void): Response {
58
+ export function finalizeAccountLease(response: Response, release: () => void, onCancel?: () => void): Response {
56
59
  if (!response.body) { release(); return response; }
57
60
  const reader = response.body.getReader();
58
61
  let done = false;
@@ -65,14 +68,15 @@ export function finalizeAccountLease(response: Response, release: () => void): R
65
68
  else controller.enqueue(next.value);
66
69
  } catch (error) { finish(); controller.error(error); }
67
70
  },
68
- async cancel(reason) { try { await reader.cancel(reason); } finally { finish(); } },
71
+ async cancel(reason) { onCancel?.(); try { await reader.cancel(reason); } finally { finish(); } },
69
72
  });
70
73
  const wrapped = new Response(body, { status: response.status, statusText: response.statusText, headers: response.headers });
71
74
  if (isNonReplayableResponse(response)) markResponseNonReplayable(wrapped);
72
75
  if (isNativePassthroughSseResponse(response)) markNativePassthroughSseResponse(wrapped);
73
76
  if (isEagerRelaySseResponse(response)) markEagerRelaySseResponse(wrapped);
74
77
  if (isPreinspectedJsonResponse(response)) markPreinspectedJsonResponse(wrapped);
75
- return wrapped;
78
+ if (runTurnAdapterSseResponses.has(response)) runTurnAdapterSseResponses.add(wrapped);
79
+ return carryResponseToolOwnership(response, wrapped);
76
80
  }
77
81
 
78
82