@bitkyc08/opencodex 2.83.0 → 2.86.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 (405) hide show
  1. package/README.md +0 -4
  2. package/bin/ocx.mjs +61 -3
  3. package/gui/dist/assets/App-BjlttVLy.js +58 -0
  4. package/gui/dist/assets/Tray-DyR9yBee.js +1 -0
  5. package/gui/dist/assets/{index-BAbcj51J.css → index-BB6uK6tU.css} +1 -1
  6. package/gui/dist/assets/index-BMssP4k3.js +94 -0
  7. package/gui/dist/assets/quota-summary-D8M1bc_R.js +1 -0
  8. package/gui/dist/index.html +2 -2
  9. package/package.json +2 -3
  10. package/src/adapters/anthropic/beta-allowlist.ts +87 -8
  11. package/src/adapters/anthropic/client-identity.ts +1 -1
  12. package/src/adapters/anthropic/passthrough.ts +33 -4
  13. package/src/adapters/azure.ts +19 -1
  14. package/src/adapters/client-fingerprint.ts +24 -0
  15. package/src/adapters/coding-agent/protocol.ts +15 -8
  16. package/src/adapters/coding-agent/turn.ts +22 -9
  17. package/src/adapters/cursor/arg-codec.ts +17 -5
  18. package/src/adapters/cursor/catalog.ts +8 -0
  19. package/src/adapters/cursor/effort-map.ts +3 -0
  20. package/src/adapters/cursor/freeform-args.ts +75 -0
  21. package/src/adapters/cursor/protobuf-events.ts +36 -23
  22. package/src/adapters/google-antigravity-wire.ts +22 -1
  23. package/src/adapters/google.ts +2 -2
  24. package/src/adapters/ollama-native.ts +29 -1
  25. package/src/adapters/openai-chat/messages.ts +8 -4
  26. package/src/adapters/openai-chat/response-events.ts +59 -1
  27. package/src/adapters/openai-chat.ts +8 -5
  28. package/src/adapters/openai-responses/empty-assistant.ts +32 -0
  29. package/src/adapters/openai-responses/passthrough.ts +3 -0
  30. package/src/adapters/openai-responses/request-strips.ts +10 -2
  31. package/src/adapters/qoder/adapter.ts +56 -15
  32. package/src/advisor/consult.ts +220 -0
  33. package/src/advisor/context.ts +226 -0
  34. package/src/advisor/disclosure.ts +20 -0
  35. package/src/advisor/runtime.ts +305 -0
  36. package/src/advisor/settings.ts +135 -0
  37. package/src/advisor/state.ts +369 -0
  38. package/src/advisor/synthetic-tool.ts +33 -0
  39. package/src/bridge/errors.ts +24 -0
  40. package/src/bridge.ts +1 -1
  41. package/src/chat/inbound.ts +30 -6
  42. package/src/chat/outbound.ts +5 -0
  43. package/src/claude/context-windows.ts +54 -1
  44. package/src/claude/desktop-3p-library.ts +43 -3
  45. package/src/claude/desktop-3p.ts +22 -8
  46. package/src/claude/desktop-picker-profile.ts +11 -4
  47. package/src/claude/desktop-profile.ts +10 -1
  48. package/src/claude/desktop-remote-store-io.ts +5 -3
  49. package/src/claude/inbound.ts +121 -50
  50. package/src/claude/intercept/connect-proxy.ts +10 -3
  51. package/src/claude/intercept/local-ca.ts +8 -1
  52. package/src/claude/intercept/picker-ca-persistence.ts +3 -1
  53. package/src/claude/intercept/picker-listener.ts +49 -1
  54. package/src/claude/intercept/settings-no-proxy.ts +146 -0
  55. package/src/claude/intercept/settings.ts +101 -9
  56. package/src/claude/model-default-settings.ts +154 -0
  57. package/src/claude/model-info.ts +7 -7
  58. package/src/claude/outbound.ts +13 -6
  59. package/src/cli/account-api.ts +65 -0
  60. package/src/cli/account-auth.ts +26 -2
  61. package/src/cli/account.ts +41 -5
  62. package/src/cli/advisor.ts +151 -0
  63. package/src/cli/auxiliary-listener-preflight.ts +37 -0
  64. package/src/cli/capabilities-access-remote.ts +8 -0
  65. package/src/cli/capabilities-agents-routing.ts +18 -0
  66. package/src/cli/capabilities-base.ts +14 -2
  67. package/src/cli/catalog-command-result.ts +2 -1
  68. package/src/cli/chatgpt-command.ts +35 -14
  69. package/src/cli/claude.ts +60 -11
  70. package/src/cli/config-command.ts +8 -2
  71. package/src/cli/debug.ts +8 -0
  72. package/src/cli/dispatch.ts +21 -4
  73. package/src/cli/doctor.ts +9 -0
  74. package/src/cli/ensure-desktop-cold-start.ts +329 -0
  75. package/src/cli/export-command.ts +15 -2
  76. package/src/cli/help.ts +2 -1
  77. package/src/cli/hub-disable.ts +99 -0
  78. package/src/cli/hub.ts +4 -2
  79. package/src/cli/index.ts +26 -8
  80. package/src/cli/integration-plan-dto.ts +2 -0
  81. package/src/cli/link-child.ts +104 -0
  82. package/src/cli/link-enrollment.ts +96 -0
  83. package/src/cli/link.ts +39 -24
  84. package/src/cli/observe-stream.ts +6 -1
  85. package/src/cli/observe.ts +10 -2
  86. package/src/cli/provider.ts +12 -0
  87. package/src/cli/registry.ts +20 -7
  88. package/src/cli/restart-scope.ts +3 -1
  89. package/src/cli/runtime-api.ts +27 -2
  90. package/src/cli/status.ts +104 -20
  91. package/src/client/connect.ts +39 -4
  92. package/src/client/hub-client.ts +14 -7
  93. package/src/client/hub-state.ts +25 -14
  94. package/src/client/link-home.ts +248 -0
  95. package/src/client/link-management-relay.ts +61 -0
  96. package/src/client/link-relay.ts +6 -1
  97. package/src/client/link-status.ts +14 -0
  98. package/src/client/machine-api.ts +14 -7
  99. package/src/client/machine-listener.ts +29 -4
  100. package/src/clients/config-export/contracts.ts +1 -0
  101. package/src/clients/config-export/model-metadata.ts +3 -2
  102. package/src/clients/config-export/qoder.ts +93 -0
  103. package/src/clients/config-export.ts +59 -16
  104. package/src/clients/omo-role-models.ts +6 -3
  105. package/src/codex/agent-role-models.ts +26 -8
  106. package/src/codex/auth-api/account-list.ts +17 -2
  107. package/src/codex/auth-api/account-selection.ts +17 -7
  108. package/src/codex/auth-api/pool-mode-gate.ts +5 -2
  109. package/src/codex/auth-api/routes.ts +5 -1
  110. package/src/codex/auth-context.ts +79 -5
  111. package/src/codex/autostart-health.ts +7 -1
  112. package/src/codex/catalog/build-entries.ts +11 -2
  113. package/src/codex/catalog/bundled.ts +5 -1
  114. package/src/codex/catalog/configured-tiers.ts +50 -0
  115. package/src/codex/catalog/effort.ts +6 -3
  116. package/src/codex/catalog/metadata.ts +4 -2
  117. package/src/codex/catalog/model-hints.ts +21 -3
  118. package/src/codex/catalog/native-models.ts +16 -2
  119. package/src/codex/catalog/native-ultrafast.ts +106 -0
  120. package/src/codex/catalog/parsing.ts +5 -0
  121. package/src/codex/catalog/provider-models.ts +4 -0
  122. package/src/codex/catalog/retained-sync.ts +2 -0
  123. package/src/codex/catalog/routed-gather.ts +22 -4
  124. package/src/codex/catalog/vendor-context.ts +82 -0
  125. package/src/codex/config-write-lock.ts +40 -2
  126. package/src/codex/convergence.ts +2 -0
  127. package/src/codex/desktop-app/types.ts +2 -0
  128. package/src/codex/desktop-app/windows.ts +29 -16
  129. package/src/codex/desktop-app-restart.ts +4 -2
  130. package/src/codex/desktop-compatibility/certificate-service.ts +99 -0
  131. package/src/codex/desktop-compatibility/certificate-store.ts +193 -0
  132. package/src/codex/desktop-compatibility/connection-store.ts +87 -0
  133. package/src/codex/desktop-compatibility/installed-build.ts +55 -0
  134. package/src/codex/desktop-compatibility/json-body.ts +29 -0
  135. package/src/codex/desktop-compatibility/native-identity.ts +62 -0
  136. package/src/codex/desktop-compatibility/relay-listener.ts +93 -0
  137. package/src/codex/desktop-compatibility/routing-binding.ts +10 -0
  138. package/src/codex/desktop-compatibility/routing-preflight.ts +69 -0
  139. package/src/codex/desktop-compatibility/runtime-ownership.ts +28 -0
  140. package/src/codex/desktop-compatibility/runtime.ts +173 -0
  141. package/src/codex/desktop-compatibility/service.ts +30 -0
  142. package/src/codex/desktop-compatibility/startup-settings.ts +50 -0
  143. package/src/codex/desktop-compatibility/usage-activation.ts +86 -0
  144. package/src/codex/desktop-compatibility/usage-controlled-fetch.ts +59 -0
  145. package/src/codex/desktop-compatibility/usage-controller.ts +107 -0
  146. package/src/codex/desktop-compatibility/usage-policy.ts +59 -0
  147. package/src/codex/desktop-compatibility/usage-refresh.ts +33 -0
  148. package/src/codex/desktop-compatibility/usage-sse-controller.ts +86 -0
  149. package/src/codex/desktop-compatibility/windows-activation-source.ts +44 -0
  150. package/src/codex/desktop-compatibility/windows-certificate-trust.ts +81 -0
  151. package/src/codex/desktop-compatibility/windows-key-protection.ts +59 -0
  152. package/src/codex/desktop-compatibility/windows-package-command.ts +86 -0
  153. package/src/codex/desktop-compatibility/windows-package-launch.ts +28 -0
  154. package/src/codex/history-provider.ts +1 -1
  155. package/src/codex/inject/config-write-section.ts +4 -1
  156. package/src/codex/inject/remove.ts +18 -6
  157. package/src/codex/journal.ts +16 -1
  158. package/src/codex/main-account.ts +99 -15
  159. package/src/codex/native-main-refresh-mode.ts +217 -0
  160. package/src/codex/native-profile-startup.ts +4 -1
  161. package/src/codex/prompt-journal.ts +20 -2
  162. package/src/codex/prompt-layers/encoding.ts +49 -0
  163. package/src/codex/prompt-layers/import-source.ts +30 -0
  164. package/src/codex/prompt-layers/toml-edit.ts +269 -37
  165. package/src/codex/prompt-layers/toml-read.ts +52 -4
  166. package/src/codex/prompt-layers.ts +383 -20
  167. package/src/codex/prompt-lock-claim.ts +13 -3
  168. package/src/codex/prompt-lock-evidence.ts +3 -3
  169. package/src/codex/prompt-lock-owner.ts +14 -6
  170. package/src/codex/prompt-text-probe.ts +34 -5
  171. package/src/codex/quota.ts +13 -6
  172. package/src/codex/role-auto-assign.ts +2 -5
  173. package/src/codex/routing/active-account.ts +21 -0
  174. package/src/codex/routing/health-store.ts +1 -1
  175. package/src/codex/routing/pool-recovery.ts +79 -0
  176. package/src/codex/routing.ts +1 -1
  177. package/src/codex/runtime.ts +196 -8
  178. package/src/codex/shim-diagnostics.ts +6 -0
  179. package/src/codex/shim-overlay.ts +15 -6
  180. package/src/codex/shim.ts +1 -1
  181. package/src/combos/failover.ts +3 -0
  182. package/src/config/api-key-reconcile.ts +34 -0
  183. package/src/config/diagnostics.ts +18 -1
  184. package/src/config/feature-flags.ts +5 -0
  185. package/src/config/live-reconcile.ts +56 -29
  186. package/src/config/live-topology.ts +29 -0
  187. package/src/config/load-degrade.ts +8 -2
  188. package/src/config/paths.ts +13 -3
  189. package/src/config/salvage.ts +3 -1
  190. package/src/config/schema/config-schema.ts +19 -0
  191. package/src/config/schema/desktop-compatibility.ts +9 -0
  192. package/src/config/schema/leaf-validators.ts +1 -0
  193. package/src/generated/compatibility-version.json +713 -285
  194. package/src/generated/model-metadata.ts +1 -1
  195. package/src/grok/grpc-web.ts +23 -1
  196. package/src/grok/reset-coupon-ledger.ts +75 -5
  197. package/src/grok/reset-coupons.ts +4 -2
  198. package/src/integrations/catalog-refresh.ts +1 -1
  199. package/src/integrations/config-io.ts +20 -2
  200. package/src/integrations/journal.ts +15 -1
  201. package/src/integrations/mutation-plan.ts +14 -7
  202. package/src/integrations/registry.ts +52 -11
  203. package/src/integrations/state.ts +19 -7
  204. package/src/integrations/store.ts +3 -0
  205. package/src/integrations/target.ts +42 -5
  206. package/src/integrations/writer.ts +24 -7
  207. package/src/lib/advisor-activation.ts +16 -0
  208. package/src/lib/app-owned-memory-stores.ts +4 -0
  209. package/src/lib/app-owned-memory.ts +11 -0
  210. package/src/lib/background-work.ts +65 -0
  211. package/src/lib/bounded-file-read.ts +58 -0
  212. package/src/lib/bun-path-runtime.d.mts +10 -1
  213. package/src/lib/bun-path-runtime.mjs +62 -3
  214. package/src/lib/desktop-proxy-route.ts +30 -0
  215. package/src/lib/desktop-upstream-tunnel.ts +107 -0
  216. package/src/lib/errors.ts +7 -0
  217. package/src/lib/json-byte-size.ts +31 -0
  218. package/src/lib/local-internal-call-capability.ts +55 -0
  219. package/src/lib/optional-shutdown-hooks.ts +35 -0
  220. package/src/lib/outbound-credential-registry.ts +210 -0
  221. package/src/lib/package-tree-integrity.ts +2 -2
  222. package/src/lib/passive-read.ts +35 -0
  223. package/src/lib/shadow-call.ts +44 -5
  224. package/src/lib/socks5-fetch.ts +48 -71
  225. package/src/lib/socks5-handshake.ts +112 -0
  226. package/src/lib/translator-budget.ts +7 -1
  227. package/src/link/codex-settings.ts +155 -0
  228. package/src/link/management-routes.ts +89 -0
  229. package/src/link/remote-probe.ts +38 -0
  230. package/src/link/ssh-argv.ts +26 -5
  231. package/src/link/ssh-runner.ts +58 -27
  232. package/src/link/status-projection.ts +22 -4
  233. package/src/link/supervisor.ts +144 -49
  234. package/src/link/tunnel-state.ts +11 -4
  235. package/src/oauth/anthropic-continuity.ts +4 -3
  236. package/src/oauth/anthropic-identity.ts +13 -2
  237. package/src/oauth/anthropic-model-quota.ts +13 -1
  238. package/src/oauth/anthropic-routing.ts +230 -23
  239. package/src/oauth/anthropic.ts +18 -4
  240. package/src/oauth/generic-account-failover.ts +29 -5
  241. package/src/oauth/google-antigravity.ts +24 -7
  242. package/src/oauth/index.ts +28 -17
  243. package/src/oauth/kiro-account-identity.ts +106 -0
  244. package/src/oauth/kiro-device-login.ts +2 -1
  245. package/src/oauth/kiro.ts +55 -6
  246. package/src/oauth/plan.ts +29 -0
  247. package/src/oauth/store.ts +40 -3
  248. package/src/oauth/types.ts +2 -0
  249. package/src/plugins/loader.ts +11 -0
  250. package/src/protocols/codecs/chat-ir.ts +467 -0
  251. package/src/protocols/codecs/chat.ts +10 -1
  252. package/src/protocols/codecs/messages-ir.ts +500 -0
  253. package/src/protocols/features.ts +10 -2
  254. package/src/protocols/path.ts +11 -1
  255. package/src/protocols/plan-snapshot.ts +39 -16
  256. package/src/protocols/plan.ts +9 -1
  257. package/src/protocols/settings.ts +8 -0
  258. package/src/providers/anthropic-billing-ownership.ts +74 -0
  259. package/src/providers/anthropic-billing-recovery.ts +31 -0
  260. package/src/providers/anthropic-billing-refusal.ts +55 -0
  261. package/src/providers/anthropic-reset-admission.ts +62 -0
  262. package/src/providers/anthropic-reset-grant-ledger.ts +28 -6
  263. package/src/providers/antigravity-account-roster.ts +102 -0
  264. package/src/providers/antigravity-models.ts +85 -7
  265. package/src/providers/api-key-resolve.ts +17 -2
  266. package/src/providers/key-failover.ts +52 -5
  267. package/src/providers/model-rename-fields.ts +2 -0
  268. package/src/providers/model-rename-migration.ts +5 -3
  269. package/src/providers/model-rename-startup.ts +5 -3
  270. package/src/providers/quota/account-cache.ts +11 -0
  271. package/src/providers/quota/anthropic-account-quota.ts +4 -2
  272. package/src/providers/quota/report-cache.ts +14 -0
  273. package/src/providers/quota/vendor-probes-oauth.ts +3 -3
  274. package/src/providers/quota.ts +4 -1
  275. package/src/providers/registry/entries-core.ts +21 -20
  276. package/src/providers/registry/entries-extended.ts +19 -1
  277. package/src/providers/registry/model-seeds.ts +17 -0
  278. package/src/providers/request-pacing.ts +9 -0
  279. package/src/providers/stale-no-vision-seed-migration.ts +72 -0
  280. package/src/responses/compaction.ts +14 -0
  281. package/src/responses/parser.ts +37 -20
  282. package/src/responses/state.ts +32 -29
  283. package/src/routing/history/build-file.ts +92 -0
  284. package/src/routing/history/indexer.ts +248 -216
  285. package/src/routing/history/ingest.ts +238 -0
  286. package/src/routing/history/rebuild-job.ts +111 -0
  287. package/src/routing/history/rebuild-protocol.ts +15 -0
  288. package/src/routing/history/rebuild-worker.ts +34 -0
  289. package/src/server/admission-model-scope.ts +4 -1
  290. package/src/server/antigravity-direct-eligibility.ts +208 -0
  291. package/src/server/auth-cors.ts +46 -0
  292. package/src/server/chat-completions.ts +75 -9
  293. package/src/server/claude-messages.ts +110 -20
  294. package/src/server/configured-credentials.ts +194 -0
  295. package/src/server/index/desktop-compatibility-startup.ts +56 -0
  296. package/src/server/index/link-management.ts +58 -0
  297. package/src/server/index/optional-listeners.ts +5 -1
  298. package/src/server/index/serve-options.ts +15 -2
  299. package/src/server/index/startup-warnings.ts +9 -0
  300. package/src/server/index/websocket-handler.ts +9 -0
  301. package/src/server/index.ts +13 -10
  302. package/src/server/inference/client-encoder-delivery.ts +6 -5
  303. package/src/server/lifecycle.ts +2 -1
  304. package/src/server/login-shell-path.ts +146 -0
  305. package/src/server/management/advisor-routes.ts +191 -0
  306. package/src/server/management/agent-settings-routes.ts +50 -14
  307. package/src/server/management/anthropic-reset-grant-routes.ts +17 -1
  308. package/src/server/management/codex-agent-role-routes.ts +19 -6
  309. package/src/server/management/codex-prompt-routes.ts +136 -5
  310. package/src/server/management/codex-role-auto-assign.ts +1 -1
  311. package/src/server/management/companion-routes.ts +4 -0
  312. package/src/server/management/config-routes.ts +2 -2
  313. package/src/server/management/context.ts +4 -0
  314. package/src/server/management/desktop-compatibility-routes.ts +46 -0
  315. package/src/server/management/desktop-compatibility-runtime-routes.ts +46 -0
  316. package/src/server/management/desktop-compatibility-settings-routes.ts +27 -0
  317. package/src/server/management/grok-coupon-routes.ts +78 -27
  318. package/src/server/management/link-enrollment.ts +192 -0
  319. package/src/server/management/link-route-state.ts +185 -0
  320. package/src/server/management/link-routes.ts +82 -402
  321. package/src/server/management/model-routes.ts +2 -2
  322. package/src/server/management/model-rows.ts +3 -1
  323. package/src/server/management/protocol-settings-patch.ts +1 -0
  324. package/src/server/management/provider-overwrite-carry.ts +10 -0
  325. package/src/server/management/provider-routes.ts +1 -0
  326. package/src/server/management/route-registry.ts +16 -3
  327. package/src/server/management/shared.ts +3 -0
  328. package/src/server/management/sibling-guard.ts +1 -0
  329. package/src/server/management-api.ts +9 -1
  330. package/src/server/messages-antigravity-direct.ts +195 -0
  331. package/src/server/messages-native.ts +73 -43
  332. package/src/server/relay-eager.ts +2 -0
  333. package/src/server/request-log-filter.ts +6 -1
  334. package/src/server/request-log-terminal-status.ts +63 -6
  335. package/src/server/request-log.ts +7 -0
  336. package/src/server/responses/adapter-continuation.ts +35 -18
  337. package/src/server/responses/adapter-delivery.ts +24 -2
  338. package/src/server/responses/adapter-dispatch.ts +261 -81
  339. package/src/server/responses/advisor-plan-slot.ts +27 -0
  340. package/src/server/responses/advisor-slot.ts +336 -0
  341. package/src/server/responses/code-mode-collab-surface.ts +313 -0
  342. package/src/server/responses/codex-ws-exchange.ts +147 -6
  343. package/src/server/responses/codex-ws-wire.ts +203 -0
  344. package/src/server/responses/collaboration.ts +36 -7
  345. package/src/server/responses/compact.ts +126 -48
  346. package/src/server/responses/context-overflow.ts +13 -1
  347. package/src/server/responses/core-codex-account.ts +105 -1
  348. package/src/server/responses/core-combo-failure.ts +24 -1
  349. package/src/server/responses/core-combo.ts +28 -2
  350. package/src/server/responses/core-errors.ts +29 -1
  351. package/src/server/responses/core-lifetime.ts +2 -0
  352. package/src/server/responses/core-normalize.ts +11 -0
  353. package/src/server/responses/core-options.ts +48 -0
  354. package/src/server/responses/core-replay.ts +7 -4
  355. package/src/server/responses/core.ts +1 -1
  356. package/src/server/responses/deferred-collab-surface.ts +158 -0
  357. package/src/server/responses/deferred-send-reporter.ts +25 -0
  358. package/src/server/responses/fetch-helpers.ts +12 -1
  359. package/src/server/responses/passthrough-delivery.ts +20 -11
  360. package/src/server/responses/passthrough-dispatch.ts +29 -7
  361. package/src/server/responses/request-prepare.ts +74 -9
  362. package/src/server/responses/request-transport.ts +22 -8
  363. package/src/server/responses/run-turn-execution.ts +70 -8
  364. package/src/server/responses/sidecar-execution.ts +89 -5
  365. package/src/server/responses/terminal-guard.ts +2 -1
  366. package/src/server/responses/ws-upstream.ts +9 -1
  367. package/src/server/responses-undeclared-tool-guard.ts +30 -9
  368. package/src/server/startup-action-control.ts +38 -4
  369. package/src/server/startup-health-cache.ts +5 -0
  370. package/src/server/system-env-shell.ts +7 -2
  371. package/src/server/system-env.ts +11 -1
  372. package/src/service/cli.ts +5 -1
  373. package/src/service/desktop-startup.ts +161 -34
  374. package/src/service/desktop-supervision.d.mts +27 -1
  375. package/src/service/desktop-supervision.mjs +111 -5
  376. package/src/service/health-sentinel-launch.ts +176 -0
  377. package/src/service/health-sentinel-main.ts +466 -0
  378. package/src/service/health-sentinel-policy.ts +241 -0
  379. package/src/service/health-sentinel-probe.ts +71 -0
  380. package/src/service/health-sentinel-windows.ts +87 -0
  381. package/src/service/linux-linger.ts +20 -0
  382. package/src/service/logging.ts +72 -0
  383. package/src/tray/windows-tray.ps1 +43 -24
  384. package/src/types/config.ts +68 -6
  385. package/src/types/provider.ts +7 -0
  386. package/src/types/request.ts +15 -0
  387. package/src/types/tools.ts +2 -0
  388. package/src/update/async-check.ts +1 -1
  389. package/src/update/badge.ts +2 -2
  390. package/src/update/index.ts +88 -15
  391. package/src/update/job-supervised-restart.ts +107 -0
  392. package/src/update/job.ts +32 -32
  393. package/src/update/notify.ts +1 -1
  394. package/src/update/refresh-scheduler.ts +2 -2
  395. package/src/update/supervised-recovery.d.mts +27 -0
  396. package/src/update/supervised-recovery.mjs +157 -0
  397. package/src/usage/cache-diagnostic.ts +2 -0
  398. package/src/usage/expected-prices.ts +16 -0
  399. package/src/usage/log.ts +19 -1
  400. package/src/web-search/gemini-executor.ts +2 -2
  401. package/assets/sponsors/packycode.png +0 -0
  402. package/gui/dist/assets/App-Ba8uG696.js +0 -54
  403. package/gui/dist/assets/Tray-IImbNPRy.js +0 -1
  404. package/gui/dist/assets/index-BHRZdJuv.js +0 -94
  405. package/gui/dist/assets/quota-summary-DiJO6sYY.js +0 -1
@@ -200,6 +200,7 @@ import {
200
200
  usesCodexForwardPoolAuth,
201
201
  } from "./core";
202
202
  import { fetchWithHeaderTimeout, providerFetch, safeHostLabel, safeOriginLabel } from "./fetch-helpers";
203
+ import { shouldRetryCodexPoolAccountQuota, shouldRetryCodexPoolAccountTransient } from "./core-codex-account";
203
204
  import { mapCodexAuthContextErrorToResponse, nativeMainRefreshFailureResponse } from "./codex-auth-error";
204
205
  import { linkRequestSessionLane, sessionLaneIdFromRequest } from "../request-log-conversation";
205
206
  import { recallComboForLane } from "./combo-session-recall";
@@ -1048,7 +1049,11 @@ export async function handleResponsesCompact(
1048
1049
  targetKey: compactTargetKey,
1049
1050
  });
1050
1051
  if (!decision.allowed) return Promise.reject(new SendBudgetExhaustedError(safeHostLabel(compactUrl)));
1051
- if (!decision.permit.use()) return Promise.reject(new SendBudgetExhaustedError(safeHostLabel(compactUrl)));
1052
+ if (!decision.permit.use()) {
1053
+ // Nothing was sent, so the reservation goes back rather than holding a send against the request.
1054
+ decision.permit.release();
1055
+ return Promise.reject(new SendBudgetExhaustedError(safeHostLabel(compactUrl)));
1056
+ }
1052
1057
  return doFetch();
1053
1058
  };
1054
1059
  const sendCompactAttempt = (
@@ -1056,6 +1061,7 @@ export async function handleResponsesCompact(
1056
1061
  sendHeaders: Headers,
1057
1062
  recovery: "normal" | "single",
1058
1063
  sendAuthCtx: CodexAuthContext,
1064
+ onPhysicalDispatch?: () => void,
1059
1065
  ): Promise<Response> => {
1060
1066
  const doFetch = (upstreamRecovery?: UpstreamSendRecovery) => fetchWithHeaderTimeout(
1061
1067
  compactUrl,
@@ -1072,6 +1078,7 @@ export async function handleResponsesCompact(
1072
1078
  modelId: route.modelId,
1073
1079
  beforeDispatch: isCanonicalOpenAiForwardProvider(sendProvider)
1074
1080
  ? createCodexAuthDispatchGuard(sendAuthCtx, config, selectedModelId, admission) : undefined,
1081
+ ...(onPhysicalDispatch ? { onPhysicalDispatch } : {}),
1075
1082
  }),
1076
1083
  // Every credential-bearing forward send gets manual redirects, not only
1077
1084
  // pool sends: direct mode carries the caller's credential too (#914).
@@ -1217,6 +1224,53 @@ export async function handleResponsesCompact(
1217
1224
  }
1218
1225
  }
1219
1226
 
1227
+ // The settle for an alternate send that threw, shared by the quota and transient moves. By the
1228
+ // time it runs the request already names the alternate (outcomeCtx), so health goes to it.
1229
+ const failedAlternateCompactSend = (err: unknown): Response => {
1230
+ if (req.signal.aborted) {
1231
+ recordCompactPoolOutcome(outcomeCtx, 499);
1232
+ return formatErrorResponse(499, "client_cancelled", "Client cancelled compact request");
1233
+ }
1234
+ const localRefusal = localDispatchRefusal(err);
1235
+ if (localRefusal) return localRefusal;
1236
+ const outcome = classifyTransportFailureKind(err);
1237
+ // Host-level evidence stands regardless of pool membership (#914 review).
1238
+ if (outcome === "connect_neutral") {
1239
+ if (compactHostCircuitEnabled) {
1240
+ recordUpstreamHostFailure(actualCompactHostKey, {
1241
+ code: transportErrorCode(err),
1242
+ threshold: config.upstreamHostCircuitThreshold,
1243
+ lease: compactHostAdmissionLease,
1244
+ });
1245
+ } else {
1246
+ recordUpstreamHostFailure(actualCompactHostKey, { code: transportErrorCode(err) });
1247
+ }
1248
+ } else {
1249
+ releaseUpstreamHostAdmission(compactHostAdmissionLease);
1250
+ }
1251
+ compactHostAdmissionLease = null;
1252
+ recordCompactPoolOutcome(outcomeCtx, outcome);
1253
+ return formatErrorResponse(502, "upstream_error", "Failed to connect to compact upstream");
1254
+ };
1255
+ const scrubCompactStateForAlternate = (alternateAuthCtx: CodexAuthContext): void => {
1256
+ const binding = conversationStateBindingFromAuth(
1257
+ alternateAuthCtx,
1258
+ (authCtx.kind === "pool" || authCtx.kind === "main-pool")
1259
+ ? authCtx.affinityKey
1260
+ : codexPoolAffinityKey(req.headers),
1261
+ );
1262
+ if (!binding) return;
1263
+ for (const body of [raw, compactBody]) {
1264
+ applyAccountChangeConversationStateScrub({
1265
+ body,
1266
+ bindingKey: binding.bindingKey,
1267
+ servingAccountId: binding.accountId,
1268
+ priorAccountId: authCtx.accountId,
1269
+ logCtx,
1270
+ });
1271
+ }
1272
+ };
1273
+
1220
1274
  // Bounded same-request alternate: the regular /v1/responses path already does this
1221
1275
  // (core.ts:319-423) and recognizes exactly 429/402. Without it a pool rejection
1222
1276
  // surfaces to the client, which retries the compact task OUTSIDE the logical request
@@ -1317,57 +1371,81 @@ export async function handleResponsesCompact(
1317
1371
  await upstream.body?.cancel().catch(() => undefined);
1318
1372
  outcomeCtx = alternate.authCtx;
1319
1373
  logCtx.accountLogLabel = codexAuthContextLogLabel(alternate.authCtx, config);
1320
- {
1321
- const binding = conversationStateBindingFromAuth(
1322
- alternate.authCtx,
1323
- (authCtx.kind === "pool" || authCtx.kind === "main-pool")
1324
- ? authCtx.affinityKey
1325
- : codexPoolAffinityKey(req.headers),
1326
- );
1327
- if (binding) {
1328
- applyAccountChangeConversationStateScrub({
1329
- body: raw,
1330
- bindingKey: binding.bindingKey,
1331
- servingAccountId: binding.accountId,
1332
- priorAccountId: authCtx.accountId,
1333
- logCtx,
1334
- });
1335
- applyAccountChangeConversationStateScrub({
1336
- body: compactBody,
1337
- bindingKey: binding.bindingKey,
1338
- servingAccountId: binding.accountId,
1339
- priorAccountId: authCtx.accountId,
1340
- logCtx,
1341
- });
1342
- }
1343
- }
1374
+ scrubCompactStateForAlternate(alternate.authCtx);
1344
1375
  try {
1345
1376
  upstream = await sendCompactAttempt(alternate.provider, alternate.headers, "single", alternate.authCtx);
1346
1377
  } catch (err) {
1347
- if (req.signal.aborted) {
1348
- recordCompactPoolOutcome(outcomeCtx, 499);
1349
- return formatErrorResponse(499, "client_cancelled", "Client cancelled compact request");
1350
- }
1351
- const localRefusal = localDispatchRefusal(err);
1352
- if (localRefusal) return localRefusal;
1353
- const outcome = classifyTransportFailureKind(err);
1354
- // Host-level evidence stands regardless of pool membership (#914 review).
1355
- if (outcome === "connect_neutral") {
1356
- if (compactHostCircuitEnabled) {
1357
- recordUpstreamHostFailure(actualCompactHostKey, {
1358
- code: transportErrorCode(err),
1359
- threshold: config.upstreamHostCircuitThreshold,
1360
- lease: compactHostAdmissionLease,
1361
- });
1362
- } else {
1363
- recordUpstreamHostFailure(actualCompactHostKey, { code: transportErrorCode(err) });
1364
- }
1365
- } else {
1366
- releaseUpstreamHostAdmission(compactHostAdmissionLease);
1378
+ return failedAlternateCompactSend(err);
1379
+ }
1380
+ }
1381
+ }
1382
+ // A transient 5xx that survived the same-account ladder above (#6969). The regular path moves
1383
+ // it to another Pool account; compact used to move only on 429/402, so the client retried the
1384
+ // compact task outside this request while another account sat idle. One bounded alternate send,
1385
+ // the same guards as the quota move, and a 5xx whose body confirms quota keeps today's path.
1386
+ if (
1387
+ shouldRetryCodexPoolAccountTransient(upstream)
1388
+ && !storedPool401ReplayAttempted
1389
+ && usesCodexForwardPoolAuth(authCtx, route.provider)
1390
+ && !authCtx.fixedAccount
1391
+ && route.codexAccountMode
1392
+ && outcomeCtx === authCtx
1393
+ && !req.signal.aborted
1394
+ && !conversationCarriesUploadedFiles(raw)
1395
+ && !await shouldRetryCodexPoolAccountQuota(upstream, req.signal)
1396
+ ) {
1397
+ const firstStatus = upstream.status;
1398
+ const firstRetryAfter = upstream.headers.get("retry-after");
1399
+ const firstResetAt = [
1400
+ upstream.headers.get("x-codex-primary-reset-at"),
1401
+ upstream.headers.get("x-codex-secondary-reset-at"),
1402
+ upstream.headers.get("x-codex-tertiary-reset-at"),
1403
+ ].filter(Boolean);
1404
+ const alternate = await resolveAlternateCompactContext({
1405
+ req,
1406
+ admission,
1407
+ config,
1408
+ route,
1409
+ selectedModelId,
1410
+ excludeAccountId: authCtx.accountId,
1411
+ turnAdmissionLease,
1412
+ });
1413
+ if (alternate && req.signal.aborted) {
1414
+ releaseCodexAuthContextProbeLease(alternate.authCtx);
1415
+ recordCompactPoolOutcome(outcomeCtx, 499);
1416
+ void upstream.body?.cancel(req.signal.reason).catch(() => undefined);
1417
+ return formatErrorResponse(499, "client_cancelled", "Client cancelled compact request");
1418
+ }
1419
+ if (alternate) {
1420
+ const firstLabel = logCtx.accountLogLabel;
1421
+ // B's identity before B's admission, so every budget and spend check runs against B. A's
1422
+ // response stays intact until B is actually sent.
1423
+ logCtx.accountLogLabel = codexAuthContextLogLabel(alternate.authCtx, config);
1424
+ scrubCompactStateForAlternate(alternate.authCtx);
1425
+ const recordFirstMove = () => {
1426
+ recordCompactPoolOutcome(authCtx, firstStatus, { retryAfter: firstRetryAfter, resetAt: firstResetAt });
1427
+ void upstream.body?.cancel().catch(() => undefined);
1428
+ outcomeCtx = alternate.authCtx;
1429
+ };
1430
+ let alternateResponse: Response | undefined;
1431
+ // Whether B's send actually left this process. Every local refusal before that point -- a
1432
+ // spent budget, a dispatch guard such as disabled credits, a pacing refusal -- keeps A.
1433
+ let alternateDispatched = false;
1434
+ try {
1435
+ alternateResponse = await sendCompactAttempt(alternate.provider, alternate.headers, "single", alternate.authCtx,
1436
+ () => { alternateDispatched = true; });
1437
+ } catch (err) {
1438
+ if (alternateDispatched) {
1439
+ recordFirstMove();
1440
+ return failedAlternateCompactSend(err);
1367
1441
  }
1368
- compactHostAdmissionLease = null;
1369
- recordCompactPoolOutcome(outcomeCtx, outcome);
1370
- return formatErrorResponse(502, "upstream_error", "Failed to connect to compact upstream");
1442
+ // Refused before anything left: A's answer and identity stand, recorded once below.
1443
+ logCtx.accountLogLabel = firstLabel;
1444
+ releaseCodexAuthContextProbeLease(alternate.authCtx);
1445
+ }
1446
+ if (alternateResponse) {
1447
+ recordFirstMove();
1448
+ upstream = alternateResponse;
1371
1449
  }
1372
1450
  }
1373
1451
  }
@@ -1,7 +1,19 @@
1
- import { bridgeToResponsesSSE } from "../../bridge";
1
+ import { bridgeToResponsesSSE, formatErrorResponse } from "../../bridge";
2
+ import { OLLAMA_LATE_ATTRIBUTION_LIMIT_MESSAGE } from "../../adapters/ollama-native";
2
3
  import type { TranslatorBudget } from "../../lib/translator-budget";
3
4
  import type { AdapterEvent } from "../../types";
4
5
 
6
+ /**
7
+ * The Ollama late-attribution refusal as a client response: the existing translator-budget
8
+ * wire shape (413 request_too_large) with only the proxy-owned constant as its message.
9
+ * It is deliberately not a context overflow: compacting on the same route cannot shrink it.
10
+ */
11
+ export function lateAttributionLimitResponse(): Response {
12
+ return formatErrorResponse(413, "request_too_large", OLLAMA_LATE_ATTRIBUTION_LIMIT_MESSAGE, {
13
+ code: "translation_buffer_limit",
14
+ });
15
+ }
16
+
5
17
  export const PROVIDER_INPUT_TOO_LARGE_MESSAGE =
6
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.";
7
19
 
@@ -22,7 +22,15 @@ import { classifyPoolRecoveryDispatch } from "../../routing/probe-lease";
22
22
  import { formatErrorResponse } from "../../bridge";
23
23
  import { readBoundedResponseBody } from "../../lib/bounded-body";
24
24
  import { upstreamErrorMessageFromPayload, isRateLimitOrQuotaFailureMessage } from "../../lib/errors";
25
- import { isNonReplayableResponse, isTransientUpstreamStatus } from "../../lib/upstream-retry";
25
+ import { isNonReplayableResponse, isTransientUpstreamStatus, SendBudgetExhaustedError } from "../../lib/upstream-retry";
26
+ import {
27
+ codexWsPreOutputRefusalHold,
28
+ codexWsPreOutputRefusalOutcomeRecorded,
29
+ isCodexWsPreOutputRefusal,
30
+ noteCodexWsPreOutputRefusalOutcome,
31
+ restoreCodexWsPreOutputRefusal,
32
+ type CodexWsPreOutputRefusalHold,
33
+ } from "./codex-ws-wire";
26
34
  import type { RequestLogContext } from "../request-log";
27
35
  import type { DataPlaneAdmission } from "../auth-cors";
28
36
  import type { InboundWire } from "../../providers/registry";
@@ -121,6 +129,90 @@ export function usesCodexForwardPoolAuth(
121
129
  && provider.authMode === "forward" && provider.adapter === "openai-responses";
122
130
  }
123
131
 
132
+ /**
133
+ * Write a pre-output refusal projection's account outcome once (#6969), for an exit that no other
134
+ * owner records: a caller abort or a local send-budget refusal during the same-account backoff,
135
+ * where the transport-failure path writes no account health and the restored stream never reaches
136
+ * delivery. A projection already recorded by the alternate-account helper is left alone.
137
+ */
138
+ export function recordCodexPreOutputRefusalOutcome(
139
+ config: OcxConfig,
140
+ authCtx: CodexAuthContext,
141
+ provider: OcxProviderConfig,
142
+ response: Response,
143
+ modelId: string,
144
+ ): void {
145
+ if (!isCodexWsPreOutputRefusal(response) || codexWsPreOutputRefusalOutcomeRecorded(response)) return;
146
+ if (!usesCodexForwardPoolAuth(authCtx, provider)) return;
147
+ noteCodexWsPreOutputRefusalOutcome(response);
148
+ recordCodexUpstreamOutcome(config, authCtx.accountId, response.status, {
149
+ threadId: authCtx.affinityKey,
150
+ fixedAccount: authCtx.fixedAccount,
151
+ modelId,
152
+ probeLeaseId: codexProbeLeaseId(authCtx),
153
+ probeQuotaScope: codexProbeQuotaScope(authCtx),
154
+ transientProbe: codexTransientProbeGrant(authCtx),
155
+ writerGeneration: authCtx.writerGeneration,
156
+ });
157
+ }
158
+
159
+ /**
160
+ * The pre-output refusal hold for one Pool send starting now (#6969), or undefined. Pool forward
161
+ * sends only, and never a combo child, whose own stream preflight already advances on a zero-output
162
+ * terminal. Every projection a send produces must later pass through
163
+ * {@link PoolPreOutputRefusals.restore}.
164
+ */
165
+ export function poolPreOutputRefusalHold(
166
+ authCtx: CodexAuthContext,
167
+ provider: OcxProviderConfig,
168
+ connectMs: number,
169
+ comboAttempt: boolean | undefined,
170
+ ): CodexWsPreOutputRefusalHold | undefined {
171
+ return !comboAttempt && usesCodexForwardPoolAuth(authCtx, provider) ? codexWsPreOutputRefusalHold(connectMs) : undefined;
172
+ }
173
+
174
+ /**
175
+ * One dispatch's view of its pre-output refusal projections (#6969): the last one the initial
176
+ * transient ladder observed, the single exit where the dispatch itself must record it, and the
177
+ * restore that hands the client the original stream.
178
+ */
179
+ export class PoolPreOutputRefusals {
180
+ private last: Response | undefined;
181
+ /** True when the response handed to delivery is a restored projection already recorded. */
182
+ outcomeRecorded = false;
183
+
184
+ readonly observe = (response: Response): Response => {
185
+ if (isCodexWsPreOutputRefusal(response)) this.last = response;
186
+ return response;
187
+ };
188
+
189
+ /**
190
+ * One owner per exceptional exit of the initial ladder. A caller abort or a local budget refusal
191
+ * during the backoff leaves the transport-failure path writing no account health, so the refusal
192
+ * already observed is recorded here; any other exception keeps that path as its single writer.
193
+ */
194
+ recordOnLocalExit(
195
+ error: unknown,
196
+ aborted: boolean,
197
+ config: OcxConfig,
198
+ authCtx: CodexAuthContext,
199
+ provider: OcxProviderConfig,
200
+ modelId: string,
201
+ ): void {
202
+ if (!this.last || !(aborted || error instanceof SendBudgetExhaustedError)) return;
203
+ recordCodexPreOutputRefusalOutcome(config, authCtx, provider, this.last, modelId);
204
+ }
205
+
206
+ /**
207
+ * Called right after the Pool block on every recovery pass: a projection that survived the ladder
208
+ * and the account move becomes the original event stream before any later reader sees it.
209
+ */
210
+ restore(response: Response): Response {
211
+ this.outcomeRecorded = isCodexWsPreOutputRefusal(response) && codexWsPreOutputRefusalOutcomeRecorded(response);
212
+ return restoreCodexWsPreOutputRefusal(response);
213
+ }
214
+ }
215
+
124
216
 
125
217
  /** Live proof that this plain-main response used the observed main credential. */
126
218
  export function liveMainQuotaDispatch(
@@ -368,6 +460,10 @@ export async function shouldRetryCodexScopedQuotaOnAlternate(
368
460
  * could duplicate a turn the origin may still be running. A 5xx whose body confirms quota is not
369
461
  * routed here either — {@link shouldRetryCodexPoolAccountQuota} classifies that one first and
370
462
  * carries the cooldown with it.
463
+ *
464
+ * A WebSocket refusal reaches this predicate only as the opt-in pre-output projection (#6969): the
465
+ * upstream itself ended the turn with a structured 5xx while nothing but empty lifecycle frames had
466
+ * been relayed. It carries no non-replayable marker because nothing could have been observed.
371
467
  */
372
468
  export function shouldRetryCodexPoolAccountTransient(response: Response): boolean {
373
469
  return !isNonReplayableResponse(response) && isTransientUpstreamStatus(response.status);
@@ -420,6 +516,8 @@ export interface CodexPoolAccountRetryArgs {
420
516
  stream: boolean;
421
517
  /** Keep a buffered canonical client on HTTP/SSE after moving to another Pool account. */
422
518
  httpOnly?: boolean;
519
+ /** Pre-output refusal hold for the alternate's WebSocket send (#6969); the caller restores. */
520
+ preOutputRefusalHold?: CodexWsPreOutputRefusalHold;
423
521
  onResponse?: (
424
522
  response: Response,
425
523
  authCtx: CodexAuthContext,
@@ -571,6 +669,10 @@ export async function retryCodexPoolOnAlternateAccount(
571
669
  // keep handing it the next request.
572
670
  const recordUnmovedTransientOutcome = (): void => {
573
671
  if (!isTransientUpstreamStatus(outcomeStatus)) return;
672
+ // #6969: one write per projection. After recordFirstOutcome, a late window or permit denial
673
+ // must not record the same refusal again, and a recorded projection tells delivery to skip it.
674
+ if (codexWsPreOutputRefusalOutcomeRecorded(firstResponse)) return;
675
+ noteCodexWsPreOutputRefusalOutcome(firstResponse);
574
676
  recordCodexUpstreamOutcome(config, firstAuthCtx.accountId, outcomeStatus, {
575
677
  threadId: firstAuthCtx.affinityKey,
576
678
  fixedAccount: firstAuthCtx.fixedAccount,
@@ -763,6 +865,7 @@ export async function retryCodexPoolOnAlternateAccount(
763
865
  // Retry already advanced the RR ring via excludeAccountId — reuse for promotion.
764
866
  ...(retryAuthCtx.accountId ? { promoteAccountId: retryAuthCtx.accountId } : {}),
765
867
  });
868
+ noteCodexWsPreOutputRefusalOutcome(firstResponse);
766
869
  };
767
870
  // Only a combo reset-derived outcome is deferred. Retry-After, defaults, and
768
871
  // ordinary requests must block the first account before the alternate send.
@@ -918,6 +1021,7 @@ export async function retryCodexPoolOnAlternateAccount(
918
1021
  httpOnly,
919
1022
  providerName: route.providerName,
920
1023
  modelId: route.modelId,
1024
+ preOutputRefusalHold: args.preOutputRefusalHold,
921
1025
  onCodexWsQuota: codexWsQuotaObserver(retryAuthCtx, route.provider, route.modelId),
922
1026
  beforeDispatch: isCanonicalOpenAiForwardProvider(route.provider)
923
1027
  ? createCodexAuthDispatchGuard(retryAuthCtx, options.codexAuthPolicy ?? config, route.modelId, options.admission, options.visionDescribeTerminal === true) : undefined,
@@ -14,7 +14,9 @@ import {
14
14
  import { normalizeUpstreamErrorText } from "./core-errors";
15
15
  import { resolveClientRetryAfter } from "../../lib/retry-after";
16
16
  import { formatErrorResponse } from "../../bridge";
17
+ import { OLLAMA_LATE_ATTRIBUTION_LIMIT_MESSAGE } from "../../adapters/ollama-native";
17
18
  import { isNonReplayableResponse, markResponseNonReplayable } from "../../lib/upstream-retry";
19
+ import { MODEL_NOT_ALLOWED_FOR_KEY } from "../admission-model-scope";
18
20
  import { usageFromResponsesPayload } from "../request-log";
19
21
  import type { ResponsesTerminalStatus } from "../../bridge";
20
22
 
@@ -48,6 +50,9 @@ export async function consumeComboFailure(
48
50
  // display-safe body carries a recognized quota message.
49
51
  let quotaConfirmedByBody = false;
50
52
  const serverError = response.status >= 500 && response.status < 600;
53
+ // A local key-scope refusal is already the client's answer: rebuilding it as an upstream_error
54
+ // would hide the stable model_not_allowed_for_key type clients branch on.
55
+ let scopeRefusalBody: string | undefined;
51
56
  try {
52
57
  const body = await readBoundedResponseBody(response, { signal, reportUtf8Validity: serverError });
53
58
  // A 5xx body counts as quota or classification evidence only when it decoded as valid
@@ -70,6 +75,9 @@ export async function consumeComboFailure(
70
75
  upstreamMessage = normalized.message;
71
76
  upstreamType = normalized.type;
72
77
  }
78
+ if (nonReplayable && response.status === 403 && upstreamType === MODEL_NOT_ALLOWED_FOR_KEY) {
79
+ scopeRefusalBody = body.text;
80
+ }
73
81
  }
74
82
  } catch (error) {
75
83
  if (signal?.aborted) throw error;
@@ -105,7 +113,9 @@ export async function consumeComboFailure(
105
113
  now,
106
114
  includeDefault: false,
107
115
  });
108
- const failureResponse = formatErrorResponse(
116
+ const failureResponse = scopeRefusalBody !== undefined
117
+ ? new Response(scopeRefusalBody, { status: 403, headers: { "Content-Type": "application/json" } })
118
+ : formatErrorResponse(
109
119
  response.status,
110
120
  cyberFailure ? (upstreamType ?? CYBER_POLICY_ERROR_CODE) : "upstream_error",
111
121
  message,
@@ -121,6 +131,7 @@ export async function consumeComboFailure(
121
131
  classificationText,
122
132
  ...(codexModelRefusal !== undefined ? { codexModelRefusal } : {}),
123
133
  ...(normalizedUpstreamCode !== undefined ? { upstreamCode: normalizedUpstreamCode } : {}),
134
+ ...(upstreamMessage !== undefined ? { upstreamMessage } : {}),
124
135
  ...(upstreamType !== undefined ? { upstreamType } : {}),
125
136
  ...(!cyberFailure && cooldownRetryAfter !== undefined ? { retryAfter: cooldownRetryAfter } : {}),
126
137
  // The EFFECTIVE classification decides, not the raw status. An upstream that wraps a quota
@@ -221,3 +232,15 @@ export function buildComboChildHeaders(parentHeaders: HeadersInit): Headers {
221
232
  childHeaders.delete("content-encoding");
222
233
  return childHeaders;
223
234
  }
235
+
236
+ /**
237
+ * A member's local Ollama late-attribution refusal: the exact proxy-owned constant on the exact
238
+ * local wire shape, from a replayable attempt. The caller replaces only the client response with
239
+ * the literal constant envelope; classification inputs are never rewritten.
240
+ */
241
+ export function isLateAttributionRefusal(failure: ConsumedComboFailure): boolean {
242
+ return !failure.nonReplayable
243
+ && failure.response.status === 413
244
+ && failure.upstreamCode === "request_too_large"
245
+ && failure.upstreamMessage === OLLAMA_LATE_ATTRIBUTION_LIMIT_MESSAGE;
246
+ }
@@ -1,3 +1,5 @@
1
+ import { effectiveAzureOutputBudget } from "../../adapters/azure";
2
+ import { resolveWireProtocolOverride } from "../adapter-resolve";
1
3
  import type { CodexAccountModelRefusal } from "../../combos/failover";
2
4
  import {
3
5
  isCodexReasoningEffort,
@@ -80,6 +82,7 @@ import {
80
82
  buildComboChildHeaders,
81
83
  createChildPassthroughCallbackGate,
82
84
  consumeComboFailure,
85
+ isLateAttributionRefusal,
83
86
  } from "./core-combo-failure";
84
87
  import {
85
88
  linkRequestSessionLane,
@@ -100,7 +103,7 @@ import {
100
103
  markEagerRelaySseResponse,
101
104
  } from "../relay";
102
105
  import { preflightComboStreamResponse } from "./combo-stream-preflight";
103
- import { streamingContextOverflowResponse, jsonContextOverflowResponse } from "./context-overflow";
106
+ import { streamingContextOverflowResponse, jsonContextOverflowResponse, lateAttributionLimitResponse } from "./context-overflow";
104
107
  import { mandatoryResponsesReasoningReplayUnavailable } from "./core-replay";
105
108
  import { settleOperatorReplacement } from "../../lib/upstream-retry";
106
109
  import { createComboProtocolLanes, dispatchNativeComboChild } from "./core-combo-native";
@@ -711,12 +714,31 @@ export async function executeComboResponses(
711
714
  retryAfter: String(Math.max(1, Math.ceil(remainingQuotaCooldownMs / 1000))),
712
715
  }) : lastFailure;
713
716
  };
717
+ // The caller's output ceiling, captured once so a floor applied to one target never
718
+ // leaks into the next. An established parent estimate wins; otherwise (combo dispatch
719
+ // precedes request preparation) the caller's own positive cap is reserved.
720
+ const callerBudget = isPlainObject(body) ? body.max_output_tokens : undefined;
721
+ const callerOutputCeilingTokens = logCtx.spendOutputCeilingTokens
722
+ ?? (typeof callerBudget === "number" && Number.isFinite(callerBudget) && callerBudget > 0
723
+ ? Math.trunc(callerBudget) : undefined);
714
724
  while (pick) {
715
725
  if (options.abortSignal?.aborted) return clientCancelledResponse();
716
726
  const firstComboTarget = comboTargetsDispatched === 0;
717
727
  const targetRoute = routeConcreteModel(config, `${pick.target.provider}/${pick.target.model}`);
718
728
  // The inherited spend tracker observes this parent log, before the child has its own label.
719
729
  logCtx.spendPoolId = targetRoute.providerName;
730
+ // Combo prepays before its child normalizes the route; the parent tracker must
731
+ // already see this target's effective cap rather than a previous target's cap.
732
+ const spendProvider = resolveWireProtocolOverride(targetRoute.providerName, targetRoute.modelId,
733
+ targetRoute.provider, options.inboundWire ?? "responses", targetRoute.staticPolicy);
734
+ const outputBudget = effectiveAzureOutputBudget(spendProvider, callerBudget);
735
+ if (outputBudget !== callerBudget && typeof outputBudget === "number" && outputBudget > 0) {
736
+ logCtx.spendOutputCeilingTokens = Math.trunc(outputBudget);
737
+ } else if (callerOutputCeilingTokens === undefined) {
738
+ delete logCtx.spendOutputCeilingTokens;
739
+ } else {
740
+ logCtx.spendOutputCeilingTokens = callerOutputCeilingTokens;
741
+ }
720
742
  // Derive the target's allowance before booking its initial send; the booking occupies it.
721
743
  const targetSendBudget = comboSendScope
722
744
  ? comboTargetSendBudget(comboSendScope, combo.targets.length - 1 - comboTargetsDispatched)
@@ -1007,6 +1029,10 @@ export async function executeComboResponses(
1007
1029
  retainCancelledAttempt();
1008
1030
  return clientCancelledResponse();
1009
1031
  }
1032
+ // A member's local late-attribution refusal reaches the client only as the literal constant
1033
+ // envelope: swap the response, keep every classification input, and skip the overflow rewrite.
1034
+ const lateAttributionRefusal = isLateAttributionRefusal(failure);
1035
+ if (lateAttributionRefusal) failure = { ...failure, response: lateAttributionLimitResponse() };
1010
1036
  sealRequestAttemptIdentity(
1011
1037
  attempt,
1012
1038
  childLog.provider,
@@ -1047,7 +1073,7 @@ export async function executeComboResponses(
1047
1073
  });
1048
1074
  const wantsStream = (rawBody as { stream?: unknown } | null)?.stream === true;
1049
1075
  // Local byte admission has its own diagnostic; do not relabel it as an upstream refusal.
1050
- const classifyOverflow = failure.response.status === 413
1076
+ const classifyOverflow = failure.response.status === 413 && !lateAttributionRefusal
1051
1077
  && (wantsStream || (failure.upstreamCode !== "outbound_body_too_large"
1052
1078
  && failure.upstreamCode !== "translation_buffer_limit"));
1053
1079
  lastFailureClassifiesOverflow = classifyOverflow;
@@ -2,7 +2,7 @@ import { readBoundedResponseBody } from "../../lib/bounded-body";
2
2
  import { redactSecretString } from "../../lib/redact";
3
3
  import { isCyberPolicyMessage, isCyberPolicyCode } from "../../lib/errors";
4
4
  import { isTranslatorBudgetExceededError } from "../../lib/translator-budget";
5
- import { formatErrorResponse } from "../../bridge";
5
+ import { formatErrorResponse, formatUsageLimitReachedResponse } from "../../bridge";
6
6
  import {
7
7
  UnsupportedContentEncodingError,
8
8
  DecompressedBodyTooLargeError,
@@ -132,6 +132,34 @@ export function clientCancelledResponse(): Response {
132
132
  return formatErrorResponse(499, "client_cancelled", "Client cancelled request");
133
133
  }
134
134
 
135
+ /**
136
+ * The Responses answer to a local Anthropic pool refusal (`AnthropicAccountCooldownError`).
137
+ *
138
+ * When every held account is out on spent quota the refusal is a usage limit (#6991), which the
139
+ * Codex client stops on and renders with its reset time; anything else stays the retryable
140
+ * `rate_limit_error` it always was. A combo child keeps the retryable form and wording: the combo
141
+ * hops on any 429 and rebuilds the body anyway, and `isAnthropicPoolLocalRefusal` in
142
+ * `core-combo.ts` deliberately cools the target for the short local fallback, which a usage-limit
143
+ * code or usage-limit prose would stretch to the ten-minute exhaustion fallback in
144
+ * `combos/failover.ts`.
145
+ */
146
+ export function anthropicPoolCooldownResponse(
147
+ refusal: {
148
+ message: string;
149
+ retryableMessage: string;
150
+ retryAfterSeconds: number | null;
151
+ usageLimit: { readonly resetsAt: number | null } | null;
152
+ },
153
+ options: { comboAttempt?: boolean; logCtx?: { errorCode?: string } } = {},
154
+ ): Response {
155
+ if (refusal.usageLimit !== null && !options.comboAttempt) {
156
+ if (options.logCtx) options.logCtx.errorCode = "usage_limit_reached";
157
+ return formatUsageLimitReachedResponse(refusal.message, refusal.usageLimit.resetsAt, refusal.retryAfterSeconds);
158
+ }
159
+ return formatErrorResponse(429, "rate_limit_error", refusal.retryableMessage,
160
+ refusal.retryAfterSeconds === null ? undefined : { retryAfter: String(refusal.retryAfterSeconds) });
161
+ }
162
+
135
163
 
136
164
  export const UNREADABLE_ENCRYPTED_AGENT_TASK_MESSAGE =
137
165
  "Routed V2 worker task is encrypted for the native ChatGPT backend and cannot be read by the selected provider. Use plaintext V2 agent-message delivery or select a native ChatGPT model.";
@@ -1,5 +1,6 @@
1
1
  import type { TranslatorBudget } from "../../lib/translator-budget";
2
2
  import { finalizeTranslatorBudgetResponse } from "../../lib/translator-budget";
3
+ import { isNonReplayableResponse, markResponseNonReplayable } from "../../lib/upstream-retry";
3
4
  import {
4
5
  isNativePassthroughSseResponse,
5
6
  markNativePassthroughSseResponse,
@@ -67,6 +68,7 @@ export function finalizeAccountLease(response: Response, release: () => void): R
67
68
  async cancel(reason) { try { await reader.cancel(reason); } finally { finish(); } },
68
69
  });
69
70
  const wrapped = new Response(body, { status: response.status, statusText: response.statusText, headers: response.headers });
71
+ if (isNonReplayableResponse(response)) markResponseNonReplayable(wrapped);
70
72
  if (isNativePassthroughSseResponse(response)) markNativePassthroughSseResponse(wrapped);
71
73
  if (isEagerRelaySseResponse(response)) markEagerRelaySseResponse(wrapped);
72
74
  if (isPreinspectedJsonResponse(response)) markPreinspectedJsonResponse(wrapped);
@@ -1,3 +1,4 @@
1
+ import { effectiveAzureOutputBudget } from "../../adapters/azure";
1
2
  import { sanitizeLogMetadataString } from "../../lib/redact";
2
3
  import type { RouteResult } from "../../router";
3
4
  import type { InboundWire } from "../../providers/registry";
@@ -211,6 +212,16 @@ export async function applyFinalRouteRequestNormalization(args: {
211
212
  logCtx.model = virtualModel?.selectedModelId ?? route.modelId;
212
213
  logCtx.provider = route.providerName;
213
214
  logCtx.spendPoolId = route.providerName;
215
+ // Admission precedes adapter serialization. Reserve the same explicit output cap
216
+ // the final destination will send, and recompute from the caller on every fallback.
217
+ const rawBudget = parsed._rawBody && typeof parsed._rawBody === "object"
218
+ ? (parsed._rawBody as { max_output_tokens?: unknown }).max_output_tokens : undefined;
219
+ const outputBudget = effectiveAzureOutputBudget(route.provider,
220
+ route.provider.adapter === "azure" || route.provider.adapter === "azure-openai"
221
+ ? rawBudget : parsed.options.maxOutputTokens);
222
+ if (typeof outputBudget === "number" && outputBudget > 0) {
223
+ logCtx.spendOutputCeilingTokens = Math.trunc(outputBudget);
224
+ }
214
225
  logCtx.providerAdapter = route.provider.adapter;
215
226
  logCtx.routeDecision = route.routeDecision;
216
227
  logCtx.policyEligibility = route.policyEligibility;