@bitkyc08/opencodex 2.80.0 → 2.82.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 (376) hide show
  1. package/README.md +3 -3
  2. package/bin/ocx.mjs +166 -73
  3. package/gui/dist/assets/App-Ds_qL7o8.js +54 -0
  4. package/gui/dist/assets/Tray-BqHF5Trd.js +1 -0
  5. package/gui/dist/assets/{index-Brk6F3fn.css → index-BAbcj51J.css} +1 -1
  6. package/gui/dist/assets/index-D3FBCNiP.js +94 -0
  7. package/gui/dist/assets/quota-summary-DTBC8n7-.js +1 -0
  8. package/gui/dist/index.html +2 -2
  9. package/gui/dist/provider-icons/claude-green.svg +1 -0
  10. package/package.json +3 -2
  11. package/src/adapters/anthropic-model-contract.ts +8 -2
  12. package/src/adapters/anthropic.ts +8 -3
  13. package/src/adapters/codebuddy/mcp-server.ts +2 -25
  14. package/src/adapters/codebuddy/tool-bridge.ts +1 -7
  15. package/src/adapters/coding-agent/mcp-server.ts +33 -0
  16. package/src/adapters/coding-agent/protocol.ts +122 -17
  17. package/src/adapters/coding-agent/tool-catalog.ts +75 -0
  18. package/src/adapters/coding-agent/turn.ts +101 -44
  19. package/src/adapters/cursor/catalog.ts +8 -0
  20. package/src/adapters/cursor/effort-map.ts +2 -0
  21. package/src/adapters/devin/live-models.ts +2 -0
  22. package/src/adapters/kiro-retry.ts +9 -4
  23. package/src/adapters/openai-chat/messages.ts +7 -6
  24. package/src/adapters/openai-responses/compaction-search-history.ts +109 -0
  25. package/src/adapters/openai-responses/passthrough.ts +9 -4
  26. package/src/adapters/openai-responses/tool-output-recovery.ts +20 -0
  27. package/src/adapters/physical-send.ts +8 -5
  28. package/src/adapters/qoder/adapter.ts +41 -6
  29. package/src/adapters/qoder/scaffold-guard.ts +2 -3
  30. package/src/adapters/zed.ts +16 -1
  31. package/src/claude/agents-inject.ts +14 -3
  32. package/src/claude/context-windows.ts +19 -17
  33. package/src/claude/desktop-3p.ts +88 -12
  34. package/src/claude/desktop-profile.ts +2 -1
  35. package/src/claude/gateway-cache.ts +18 -1
  36. package/src/claude/inbound-cache-stabilize.ts +4 -9
  37. package/src/claude/inbound.ts +78 -3
  38. package/src/claude/intercept/cli-catalog.ts +3 -0
  39. package/src/claude/intercept/model-bindings.ts +1 -0
  40. package/src/claude/intercept/picker-ca-persistence.ts +7 -6
  41. package/src/claude/intercept/picker-listener.ts +71 -4
  42. package/src/claude/intercept/picker-models.ts +14 -6
  43. package/src/claude/intercept/settings.ts +3 -0
  44. package/src/claude/long-context.ts +66 -0
  45. package/src/claude/model-info.ts +19 -16
  46. package/src/claude/outbound.ts +94 -9
  47. package/src/claude/subagent-model.ts +40 -11
  48. package/src/cli/account-anthropic-threshold.ts +14 -5
  49. package/src/cli/account-api.ts +3 -1
  50. package/src/cli/account-extended.ts +13 -9
  51. package/src/cli/account-policy-dto.ts +1 -1
  52. package/src/cli/account-policy.ts +13 -4
  53. package/src/cli/account.ts +3 -3
  54. package/src/cli/agent.ts +9 -5
  55. package/src/cli/aside-profile-recovery.ts +33 -0
  56. package/src/cli/capabilities-accounts.ts +2 -2
  57. package/src/cli/capabilities-agents-routing.ts +24 -0
  58. package/src/cli/capabilities-base.ts +5 -5
  59. package/src/cli/capabilities-integrations.ts +18 -2
  60. package/src/cli/cli-path-diagnostics.ts +132 -0
  61. package/src/cli/codex-shim-autorestore.ts +3 -0
  62. package/src/cli/desktop-runtime-guidance.ts +65 -0
  63. package/src/cli/dispatch.ts +15 -1
  64. package/src/cli/doctor.ts +16 -3
  65. package/src/cli/help.ts +4 -1
  66. package/src/cli/index.ts +25 -14
  67. package/src/cli/integration-aside-sync.ts +2 -1
  68. package/src/cli/integration-plan-dto.ts +1 -0
  69. package/src/cli/integration-preview.ts +8 -1
  70. package/src/cli/integrations.ts +48 -6
  71. package/src/cli/message-args.ts +34 -0
  72. package/src/cli/message-command.ts +43 -0
  73. package/src/cli/message-runtime.ts +6 -0
  74. package/src/cli/observe.ts +3 -1
  75. package/src/cli/provider.ts +29 -6
  76. package/src/cli/registry.ts +24 -2
  77. package/src/cli/resolve.ts +25 -2
  78. package/src/cli/root.ts +4 -2
  79. package/src/cli/status.ts +64 -12
  80. package/src/cli/update-restart-child.ts +4 -2
  81. package/src/cli/update-restart-home.ts +28 -10
  82. package/src/cli/update-restart-service-record.ts +116 -0
  83. package/src/cli/update-restart-supervision.ts +96 -0
  84. package/src/cli/update-restart.ts +59 -13
  85. package/src/cli/v2.ts +27 -20
  86. package/src/cli/version-skew-notice.ts +77 -0
  87. package/src/clients/config-export/commandcode.ts +131 -0
  88. package/src/clients/config-export/contracts.ts +12 -0
  89. package/src/clients/config-export.ts +64 -1
  90. package/src/clients/lazycodex.ts +1 -1
  91. package/src/clients/omo-role-models.ts +32 -7
  92. package/src/codex/auth-api/account-list.ts +11 -3
  93. package/src/codex/auth-api/account-selection.ts +107 -0
  94. package/src/codex/auth-api/main-account-probe.ts +25 -8
  95. package/src/codex/auth-api/pool-mode-gate.ts +33 -9
  96. package/src/codex/auth-api/pool-quota-probe.ts +15 -1
  97. package/src/codex/auth-api/routes.ts +11 -49
  98. package/src/codex/auth-context.ts +20 -1
  99. package/src/codex/autostart-health.ts +12 -3
  100. package/src/codex/catalog/auto-review.ts +13 -6
  101. package/src/codex/catalog/build-entries.ts +33 -7
  102. package/src/codex/catalog/bundled.ts +155 -26
  103. package/src/codex/catalog/combo-member.ts +3 -2
  104. package/src/codex/catalog/control-plane.ts +45 -0
  105. package/src/codex/catalog/effort.ts +37 -6
  106. package/src/codex/catalog/gather-capture.ts +10 -3
  107. package/src/codex/catalog/model-hints.ts +15 -8
  108. package/src/codex/catalog/model-visibility.ts +3 -1
  109. package/src/codex/catalog/operator-tiers.ts +38 -0
  110. package/src/codex/catalog/parsing.ts +2 -1
  111. package/src/codex/catalog/provider-models.ts +58 -44
  112. package/src/codex/catalog/retained-sync.ts +7 -5
  113. package/src/codex/catalog/routed-gather.ts +15 -1
  114. package/src/codex/catalog-auto-refresh-sources.ts +3 -4
  115. package/src/codex/catalog-auto-refresh.ts +19 -0
  116. package/src/codex/catalog.ts +1 -1
  117. package/src/codex/codex-home-owner.ts +3 -0
  118. package/src/codex/config-write-lock.ts +333 -0
  119. package/src/codex/control-plane-models.ts +6 -0
  120. package/src/codex/convergence.ts +2 -1
  121. package/src/codex/features.ts +257 -200
  122. package/src/codex/inject/config-write-section.ts +34 -0
  123. package/src/codex/inject/multi-agent-v2.ts +14 -6
  124. package/src/codex/inject/remove.ts +36 -11
  125. package/src/codex/inject/restore.ts +85 -20
  126. package/src/codex/inject-coordination.ts +15 -6
  127. package/src/codex/inject.ts +62 -20
  128. package/src/codex/journal.ts +62 -17
  129. package/src/codex/main-account-cache.ts +43 -0
  130. package/src/codex/paths.ts +2 -0
  131. package/src/codex/prepared-home.ts +18 -0
  132. package/src/codex/prompt-journal.ts +25 -12
  133. package/src/codex/prompt-layers.ts +49 -26
  134. package/src/codex/prompt-lock-claim.ts +134 -0
  135. package/src/codex/prompt-lock-evidence.ts +26 -0
  136. package/src/codex/prompt-lock-io.ts +16 -0
  137. package/src/codex/prompt-lock-owner.ts +130 -0
  138. package/src/codex/prompt-lock.ts +84 -53
  139. package/src/codex/routing/failure-window.ts +146 -0
  140. package/src/codex/routing/selection.ts +4 -1
  141. package/src/codex/routing.ts +13 -17
  142. package/src/codex/runtime.ts +106 -18
  143. package/src/codex/shim.ts +45 -19
  144. package/src/combos/jev-decision-contract.ts +25 -2
  145. package/src/combos/jev-service-exchange.ts +281 -0
  146. package/src/combos/jev.ts +34 -219
  147. package/src/combos/types.ts +1 -1
  148. package/src/config/atomic-write.ts +1 -1
  149. package/src/config/codex-ws-reuse-setting.ts +10 -0
  150. package/src/config/diagnostics.ts +27 -16
  151. package/src/config/live-reconcile.ts +17 -0
  152. package/src/config/load-degrade.ts +42 -0
  153. package/src/config/rebase-provenance.ts +11 -0
  154. package/src/config/schema/anthropic-account-pool.ts +143 -0
  155. package/src/config/schema/config-schema.ts +11 -1
  156. package/src/config/schema/leaf-validators.ts +4 -0
  157. package/src/config.ts +2 -0
  158. package/src/generated/compatibility-version.json +566 -298
  159. package/src/generated/model-metadata.ts +7 -6
  160. package/src/grok/inject.ts +50 -4
  161. package/src/grok/sync.ts +2 -1
  162. package/src/images/loop.ts +30 -11
  163. package/src/integrations/aside-profiles.ts +10 -3
  164. package/src/integrations/catalog-refresh.ts +19 -3
  165. package/src/integrations/mutation-plan.ts +7 -0
  166. package/src/integrations/registry.ts +8 -0
  167. package/src/integrations/state.ts +9 -2
  168. package/src/integrations/writer.ts +17 -5
  169. package/src/lib/bun-path-runtime.d.mts +32 -0
  170. package/src/lib/bun-path-runtime.mjs +95 -0
  171. package/src/lib/bun-runtime-preflight.ts +140 -0
  172. package/src/lib/desktop-cli-handoff.d.mts +20 -0
  173. package/src/lib/desktop-cli-handoff.mjs +92 -0
  174. package/src/lib/desktop-cli-record.d.mts +29 -0
  175. package/src/lib/desktop-cli-record.mjs +102 -0
  176. package/src/lib/lab-live-route-production.ts +9 -1
  177. package/src/lib/request-execution-budget.ts +244 -58
  178. package/src/lib/spend-pool-alias-validation.ts +79 -0
  179. package/src/lib/spend-pool-continuity.ts +34 -0
  180. package/src/lib/spend-reservation-ledger.ts +412 -50
  181. package/src/lib/state-store-registrations.ts +20 -3
  182. package/src/lib/synced-state-location.ts +46 -6
  183. package/src/lib/test-home-guard.ts +87 -1
  184. package/src/lib/translator-budget.ts +17 -4
  185. package/src/lib/upstream-retry.ts +17 -5
  186. package/src/lib/windows-atomic-replace.ts +38 -16
  187. package/src/lib/windows-elevation.ts +6 -0
  188. package/src/lib/windows-secret-acl.ts +92 -25
  189. package/src/lib/winsw.ts +2 -3
  190. package/src/lib/workflow-budget.ts +53 -11
  191. package/src/messaging/budget.ts +45 -0
  192. package/src/messaging/discovery.ts +57 -0
  193. package/src/messaging/envelope.ts +49 -0
  194. package/src/messaging/input.ts +37 -0
  195. package/src/messaging/rpc.ts +196 -0
  196. package/src/messaging/send.ts +58 -0
  197. package/src/messaging/socket.ts +72 -0
  198. package/src/messaging/types.ts +29 -0
  199. package/src/oauth/anthropic-account-refusal.ts +90 -18
  200. package/src/oauth/anthropic-account-threshold.ts +8 -2
  201. package/src/oauth/anthropic-continuity.ts +15 -1
  202. package/src/oauth/anthropic-model-quota.ts +120 -48
  203. package/src/oauth/anthropic-model-routes.ts +11 -3
  204. package/src/oauth/anthropic-oauth-definitions.ts +98 -0
  205. package/src/oauth/anthropic-pool-config.ts +30 -0
  206. package/src/oauth/anthropic-rate-limit-policy.ts +64 -9
  207. package/src/oauth/anthropic-routing.ts +1202 -866
  208. package/src/oauth/anthropic-send-ownership.ts +41 -0
  209. package/src/oauth/anthropic.ts +4 -1
  210. package/src/oauth/chatgpt.ts +113 -41
  211. package/src/oauth/generic-account-failover.ts +3 -1
  212. package/src/oauth/health.ts +8 -6
  213. package/src/oauth/index.ts +96 -175
  214. package/src/oauth/kiro.ts +22 -9
  215. package/src/oauth/login-cli.ts +2 -0
  216. package/src/oauth/model-discovery-auth.ts +43 -0
  217. package/src/oauth/pool-kernel.ts +11 -5
  218. package/src/oauth/pool-settings-capability.ts +5 -3
  219. package/src/oauth/store-anthropic-instance.ts +87 -0
  220. package/src/oauth/store.ts +68 -10
  221. package/src/oauth/token-guardian.ts +2 -0
  222. package/src/oauth/types.ts +2 -0
  223. package/src/protocols/plan-snapshot.ts +5 -0
  224. package/src/protocols/settings.ts +20 -5
  225. package/src/providers/anthropic-instance-id.ts +47 -0
  226. package/src/providers/anthropic-instance.ts +48 -0
  227. package/src/providers/anthropic-reset-grant-ledger.ts +11 -1
  228. package/src/providers/azure-model-metadata.ts +115 -0
  229. package/src/providers/derive.ts +23 -0
  230. package/src/providers/kiro-models.ts +3 -0
  231. package/src/providers/label.ts +1 -1
  232. package/src/providers/model-rename-fields.ts +2 -0
  233. package/src/providers/quota/account-cache.ts +37 -11
  234. package/src/providers/quota/anthropic-account-quota.ts +93 -0
  235. package/src/providers/quota/anthropic-cooldown-recovery.ts +222 -111
  236. package/src/providers/quota/report-cache.ts +24 -1
  237. package/src/providers/quota/vendor-probes-key.ts +127 -17
  238. package/src/providers/quota/vendor-probes-oauth.ts +46 -14
  239. package/src/providers/quota.ts +23 -33
  240. package/src/providers/registry/entries-core.ts +35 -30
  241. package/src/providers/registry/model-ids.ts +1 -0
  242. package/src/providers/registry/model-seeds.ts +7 -4
  243. package/src/providers/registry/types.ts +3 -0
  244. package/src/providers/registry.ts +3 -3
  245. package/src/responses/reasoning-envelope.ts +29 -1
  246. package/src/responses/reasoning-replay-cache.ts +17 -1
  247. package/src/responses/spill-store.ts +20 -3
  248. package/src/responses/state/metrics.ts +95 -0
  249. package/src/responses/state/snapshot-codec.ts +6 -0
  250. package/src/responses/state/spill-failure.ts +12 -0
  251. package/src/responses/state/spill-queue.ts +13 -5
  252. package/src/responses/state/unforced-store-false.ts +55 -0
  253. package/src/responses/state.ts +68 -76
  254. package/src/responses/task-input.ts +17 -0
  255. package/src/router.ts +28 -4
  256. package/src/routing/compatibility/assemble.ts +1 -1
  257. package/src/routing/quota.ts +7 -3
  258. package/src/server/auth-cors.ts +5 -0
  259. package/src/server/background-lifecycle.ts +27 -0
  260. package/src/server/chat-completions.ts +7 -1
  261. package/src/server/chat-native.ts +64 -28
  262. package/src/server/claude-messages.ts +59 -8
  263. package/src/server/gui-static.ts +38 -4
  264. package/src/server/inbound-body-admission.ts +1 -0
  265. package/src/server/index/claude-intercept-lifecycle.ts +3 -1
  266. package/src/server/index/serve-options.ts +10 -2
  267. package/src/server/index/spend-ledger-lifecycle.ts +12 -5
  268. package/src/server/index/startup-warnings.ts +19 -0
  269. package/src/server/index.ts +5 -2
  270. package/src/server/inference/context.ts +1 -1
  271. package/src/server/lifecycle.ts +1 -1
  272. package/src/server/management/agent-settings-routes.ts +123 -38
  273. package/src/server/management/anthropic-account-threshold.ts +12 -9
  274. package/src/server/management/anthropic-pool-settings.ts +72 -0
  275. package/src/server/management/anthropic-reset-grant-routes.ts +98 -39
  276. package/src/server/management/codex-agent-role-routes.ts +4 -2
  277. package/src/server/management/codex-prompt-routes.ts +1 -0
  278. package/src/server/management/codex-role-auto-assign.ts +13 -2
  279. package/src/server/management/config-routes.ts +85 -14
  280. package/src/server/management/context.ts +2 -2
  281. package/src/server/management/decision-routes.ts +2 -1
  282. package/src/server/management/integration-routes.ts +25 -2
  283. package/src/server/management/oauth-account-routes.ts +79 -98
  284. package/src/server/management/provider-instance-ownership.ts +44 -0
  285. package/src/server/management/provider-routes.ts +21 -28
  286. package/src/server/management/route-registry.ts +3 -1
  287. package/src/server/management/routing-profile-routes.ts +4 -4
  288. package/src/server/management/shared.ts +3 -3
  289. package/src/server/management/system-restart.ts +44 -14
  290. package/src/server/management/system-routes.ts +24 -2
  291. package/src/server/management/vision-sidecar-options.ts +9 -6
  292. package/src/server/management/web-search-sidecar-options.ts +51 -2
  293. package/src/server/management-auth.ts +6 -0
  294. package/src/server/messages-native-eligibility.ts +3 -2
  295. package/src/server/messages-native-oauth.ts +91 -45
  296. package/src/server/messages-native-selector.ts +39 -0
  297. package/src/server/messages-native.ts +133 -38
  298. package/src/server/messages-response-headers.ts +14 -0
  299. package/src/server/request-log-conversation.ts +48 -5
  300. package/src/server/request-log.ts +36 -2
  301. package/src/server/responses/adapter-continuation.ts +31 -19
  302. package/src/server/responses/adapter-dispatch.ts +75 -24
  303. package/src/server/responses/antigravity-validation-refusal.ts +20 -6
  304. package/src/server/responses/codex-ws-exchange.ts +17 -2
  305. package/src/server/responses/codex-ws-pool.ts +100 -13
  306. package/src/server/responses/codex-ws-wire.ts +9 -0
  307. package/src/server/responses/combo-requested-effort.ts +3 -1
  308. package/src/server/responses/compact.ts +25 -8
  309. package/src/server/responses/compaction-recovery.ts +1 -1
  310. package/src/server/responses/core-codex-account.ts +22 -1
  311. package/src/server/responses/core-combo-native.ts +24 -4
  312. package/src/server/responses/core-combo.ts +38 -25
  313. package/src/server/responses/core-lifetime.ts +3 -36
  314. package/src/server/responses/core-normalize.ts +1 -0
  315. package/src/server/responses/core-options.ts +8 -1
  316. package/src/server/responses/core-replay.ts +78 -0
  317. package/src/server/responses/core.ts +3 -3
  318. package/src/server/responses/fetch-helpers.ts +26 -3
  319. package/src/server/responses/passthrough-delivery.ts +30 -4
  320. package/src/server/responses/passthrough-dispatch.ts +39 -9
  321. package/src/server/responses/request-prepare.ts +41 -5
  322. package/src/server/responses/request-send-budget.ts +150 -50
  323. package/src/server/responses/request-spend.ts +325 -33
  324. package/src/server/responses/request-transport.ts +132 -52
  325. package/src/server/responses/response-effects.ts +1 -0
  326. package/src/server/responses/run-turn-execution.ts +13 -3
  327. package/src/server/responses/sidecar-execution.ts +113 -22
  328. package/src/server/responses/sidecar-send-budget.ts +58 -0
  329. package/src/server/responses/ws-upstream.ts +19 -6
  330. package/src/server/responses-undeclared-tool-guard.ts +5 -1
  331. package/src/server/startup-health-cache.ts +2 -1
  332. package/src/server/workflow-refusal.ts +33 -0
  333. package/src/service/cli.ts +20 -4
  334. package/src/service/desktop-command-guard.ts +32 -0
  335. package/src/service/desktop-startup.ts +66 -40
  336. package/src/service/desktop-supervision.d.mts +26 -0
  337. package/src/service/desktop-supervision.mjs +140 -0
  338. package/src/service/managing-cli.ts +1 -0
  339. package/src/service/orchestration.ts +46 -20
  340. package/src/service/ownership-mutation-lease.d.mts +1 -1
  341. package/src/service/ownership-mutation-lease.mjs +5 -7
  342. package/src/service/repair.ts +25 -9
  343. package/src/service/state.ts +4 -3
  344. package/src/service/systemd.ts +17 -8
  345. package/src/service/windows-ops.ts +61 -22
  346. package/src/sidecar/anthropic-binding.ts +79 -0
  347. package/src/sidecar/auth.ts +76 -2
  348. package/src/storage/policy-job.ts +5 -1
  349. package/src/storage/policy-worker.ts +3 -1
  350. package/src/storage/policy.ts +9 -0
  351. package/src/types/anthropic-account-pool.ts +33 -0
  352. package/src/types/config.ts +33 -28
  353. package/src/types/provider.ts +5 -0
  354. package/src/types/request.ts +5 -0
  355. package/src/types/tools.ts +7 -0
  356. package/src/types/wire.ts +1 -0
  357. package/src/types.ts +4 -0
  358. package/src/update/index.ts +57 -9
  359. package/src/update/restart-ownership.ts +10 -3
  360. package/src/update/runtime-ownership.d.mts +6 -0
  361. package/src/update/runtime-ownership.mjs +23 -7
  362. package/src/usage/cost.ts +3 -2
  363. package/src/usage/expected-prices.ts +66 -14
  364. package/src/vision/anthropic-describe.ts +16 -3
  365. package/src/vision/index.ts +3 -0
  366. package/src/vision/plan.ts +19 -6
  367. package/src/web-search/alpha-search.ts +7 -1
  368. package/src/web-search/anthropic-executor.ts +16 -4
  369. package/src/web-search/index.ts +7 -3
  370. package/src/web-search/loop.ts +32 -7
  371. package/src/web-search/passthrough-bridge.ts +4 -1
  372. package/src/web-search/sidecar-providers.ts +4 -3
  373. package/gui/dist/assets/App-MbDVfUeC.js +0 -54
  374. package/gui/dist/assets/Tray-CzkxK82A.js +0 -1
  375. package/gui/dist/assets/index-UJt6npXk.js +0 -94
  376. package/gui/dist/assets/quota-summary-DU3sD6Z-.js +0 -1
@@ -1,3 +1,5 @@
1
+ import { createPhysicalSendReporter } from "../lib/request-execution-budget";
2
+ import { createInferenceSendBudget } from "./inference/context";
1
3
  /**
2
4
  * Managed native Messages lane (PF-08, behind `protocols.rollout.managedMessagesNative`).
3
5
  *
@@ -17,32 +19,36 @@
17
19
  *
18
20
  * Loaded lazily by `claude-messages.ts` only when the switch is on and a route is eligible.
19
21
  */
20
- import { resolveAnthropicModelRoute } from "../oauth/anthropic-model-routes";
21
- import { bindAnthropicRefusalCredential, rotateAnthropicAccountOnResponse } from "../oauth/anthropic-account-refusal";
22
- import { claimAnthropicFamilyRevalidation } from "../oauth/anthropic-model-quota";
22
+ import { resolveAnthropicModelRouteForInstance } from "../oauth/anthropic-model-routes";
23
+ import { bindAnthropicRefusalCredentialForSend, rotateAnthropicAccountOnResponseForInstance } from "../oauth/anthropic-account-refusal";
24
+ import { captureAnthropicPhysicalSendOwnership, anthropicPhysicalSendOwnershipIsCurrent,
25
+ type AnthropicPhysicalSendOwnership } from "../oauth/anthropic-send-ownership";
26
+ import { anthropicModelQuotaFor } from "../oauth/anthropic-model-quota";
23
27
  import { captureConfigGeneration } from "../lib/state-store-sweeper";
24
- import { recordAnthropicAccountQuotaFromHeaders } from "../providers/quota";
28
+ import { recordAnthropicAccountQuotaFromHeadersForInstance } from "../providers/quota";
25
29
  import { credentialGeneration, getAccountCredentialWithStatus } from "../oauth/store";
26
30
  import { ANTHROPIC_POOL_MAX_FAILOVERS_PER_REQUEST } from "../oauth/anthropic-routing";
27
31
  import { enforceAnthropicImageLimits } from "../adapters/anthropic-image-guard";
28
32
  import { normalizeAnthropicImages } from "../adapters/anthropic-image-normalize";
29
- import { formatAnthropicErrorBody } from "../adapters/anthropic";
33
+ import { formatAnthropicErrorBody, resolveAnthropicMessagesUrl } from "../adapters/anthropic";
30
34
  import {
31
35
  anthropicMessagesNativeWireBody,
32
36
  buildAnthropicMessagesPassthroughRequest,
33
37
  type AnthropicMessagesPassthroughRequest,
34
38
  } from "../adapters/anthropic/passthrough";
35
- import { resolveInboundModel } from "../claude/inbound";
36
- import { anthropicErrorBody, anthropicErrorResponse, collectAnthropicMessage } from "../claude/outbound";
39
+ import { effortFromOutputConfig, resolveInboundModel } from "../claude/inbound";
40
+ import { anthropicErrorBody, anthropicErrorResponse, claudeOverflowSsePayload, claudePromptTooLongMessage, collectAnthropicMessage, isContextOverflowText, isThroughputLimitText } from "../claude/outbound";
37
41
  import type { AdmissionLease } from "../lib/admission";
38
42
  import { readBoundedResponseBody } from "../lib/bounded-body";
39
43
  import { classifyError } from "../lib/errors";
44
+ import { relaySseWithPayloadRewrite } from "./sse-payload-rewrite";
40
45
  import { redactSecretString } from "../lib/redact";
41
46
  import { resolveClientRetryAfter } from "../lib/retry-after";
42
47
  import { isTranslatorBudgetExceededError, type TranslatorBudget } from "../lib/translator-budget";
43
48
  import {
44
49
  applyReplayRefusalClientHeaders,
45
50
  applyUpstreamRecoveryInit,
51
+ SendBudgetExhaustedError,
46
52
  fetchWithResetRetry,
47
53
  fetchWithTransientRetry,
48
54
  isNonReplayableResponse,
@@ -67,12 +73,13 @@ import {
67
73
  } from "../providers/key-failover";
68
74
  import { stampApiKeyAccountLabel } from "../providers/label";
69
75
  import { OAuthAccountPausedError, OAuthLoginRequiredError, publicOAuthAuthenticationErrorMessage } from "../oauth";
70
- import { AnthropicAccountCooldownError, formatAnthropicProviderForLog, hasAnthropicFailoverQuorum } from "../oauth/anthropic-routing";
76
+ import { AnthropicAccountCooldownError, formatAnthropicProviderForLog, anthropicRoutingFor } from "../oauth/anthropic-routing";
77
+ import { configuredAnthropicInstance } from "../providers/anthropic-instance";
71
78
  import { resolveProtocolSettings } from "../protocols/settings";
72
79
  import { addProtocolEntryReason, markProtocolBlocked } from "../protocols/trace";
73
80
  import type { OcxProviderTransport } from "../providers/xai-transport";
74
81
  import { preservesPhysicalComboProvider, resolveComboId } from "../combos";
75
- import { captureRouteStaticPolicy, routeModel, type RouteResult } from "../router";
82
+ import { captureRouteStaticPolicy, routedProviderConfig, routeModel, type RouteResult } from "../router";
76
83
  import { POLICY_NAMESPACE, resolvePolicyProfileId } from "../routing/profile";
77
84
  import type { AnthropicClientIdentity } from "../adapters/anthropic/client-identity";
78
85
  import type { OcxConfig, OcxProviderConfig, OcxUsage } from "../types";
@@ -88,11 +95,12 @@ import { beginInferenceAttempt } from "./inference/attempt";
88
95
  import { createFinalRequestLog, type FinalRequestLogMeta } from "./inference/final-log";
89
96
  import { registerTurn, unregisterTurn } from "./lifecycle";
90
97
  import { nativeMessagesDeclineReason, type NativeMessagesSelector } from "./messages-native-eligibility";
98
+ import { messagesSecondaryInstanceUnavailable } from "./messages-native-selector";
91
99
  import {
92
100
  nativeOAuthBindingIsCurrent,
93
101
  NativeOAuthSelectionChangedError,
94
102
  NativeOAuthModelRouteError,
95
- resolveNativeOAuthBinding,
103
+ resolveNativeOAuthBindingForInstance,
96
104
  restoreOAuthToolNamesInMessage,
97
105
  restoreOAuthToolNamesInSse,
98
106
  type NativeOAuthBinding,
@@ -100,6 +108,7 @@ import {
100
108
  import {
101
109
  noteProviderAttemptSend,
102
110
  recordAttemptCredentialSource,
111
+ recordAttemptRequestedEffort,
103
112
  recordFirstOutput,
104
113
  recordKeyAttemptFailure,
105
114
  recordKeyWireAttemptUsage,
@@ -112,6 +121,7 @@ import { workflowRefusalResponse } from "./workflow-refusal";
112
121
  import { sseFieldValue } from "../lib/sse-decoder";
113
122
  import { admissionModelDeniedResponse, type AdmissionModelScope } from "./admission-model-scope";
114
123
  import { nativeMessagesToolScopeDenial } from "./messages-native-scope";
124
+ import { retainUpstreamMessagesRequestId } from "./messages-response-headers";
115
125
 
116
126
  export {
117
127
  isNativeMessagesRouteEligible,
@@ -300,14 +310,25 @@ async function prepareNativeBody(body: Rec, signal: AbortSignal): Promise<void>
300
310
  */
301
311
  export async function handleNativeMessages(options: HandleNativeMessagesOptions): Promise<Response> {
302
312
  const { req, config, logCtx, logIds, route, body, requestedModel, translatorBudget } = options;
313
+ const nativeInstance = configuredAnthropicInstance(config, route.providerName);
303
314
  const requestedStream = body.stream === true;
304
315
  logCtx.inboundProtocol = "messages";
305
316
  logCtx.model = route.modelId;
306
317
  logCtx.provider = route.providerName;
318
+ logCtx.spendPoolId = route.providerName;
307
319
  logCtx.providerAdapter = route.provider.adapter;
308
320
  logCtx.requestedModel = requestedModel;
309
321
  if (route.routeReason === "model-alias" || route.modelId !== requestedModel) logCtx.requestedAlias = requestedModel;
310
322
  logCtx.requestedServiceTier = typeof body.service_tier === "string" ? body.service_tier : undefined;
323
+ if (logCtx.requestedEffort === undefined) {
324
+ const effort = effortFromOutputConfig(body.output_config);
325
+ const thinking = isRec(body.thinking) ? body.thinking : undefined;
326
+ const budget = thinking?.budget_tokens;
327
+ if (effort !== undefined) logCtx.requestedEffort = effort;
328
+ else if (thinking?.type === "disabled") logCtx.requestedEffort = "none";
329
+ else if (thinking?.type === "enabled" && typeof budget === "number"
330
+ && Number.isSafeInteger(budget) && budget > 0) logCtx.requestedEffort = `budget:${budget}`;
331
+ }
311
332
  // Reserve spend the way native Chat does: an input estimate that never enters usage, and the
312
333
  // caller's own output ceiling.
313
334
  if (logCtx.usageLogInputTokens === undefined) {
@@ -323,6 +344,7 @@ export async function handleNativeMessages(options: HandleNativeMessagesOptions)
323
344
  adapter: "anthropic",
324
345
  });
325
346
  attemptHandle.seal(logCtx.accountLogLabel);
347
+ recordAttemptRequestedEffort(logCtx);
326
348
  const { attempt } = attemptHandle;
327
349
  const finalLog = createFinalRequestLog(logIds, logCtx);
328
350
  const finishLog: FinishLog = (status, message, meta = { closeReason: "non_stream" }) => {
@@ -353,11 +375,18 @@ export async function handleNativeMessages(options: HandleNativeMessagesOptions)
353
375
  return null;
354
376
  };
355
377
 
378
+ if (route.provider.authMode === "oauth" && !nativeInstance) {
379
+ return fail(401, "Configured Anthropic OAuth instance is unavailable", "authentication_error");
380
+ }
381
+
356
382
  const toolScopeDenial = nativeMessagesToolScopeDenial(options.modelScope, route.providerName, requestedModel, body);
357
383
  if (toolScopeDenial) {
358
384
  finishLog(403, toolScopeDenial.message);
359
385
  return admissionModelDeniedResponse(toolScopeDenial);
360
386
  }
387
+ let routeTarget: string;
388
+ try { routeTarget = resolveAnthropicMessagesUrl(route.provider); }
389
+ catch { return fail(400, "Invalid Anthropic Messages destination", "invalid_request_error"); }
361
390
 
362
391
  try {
363
392
  await prepareNativeBody(body, req.signal);
@@ -391,7 +420,7 @@ export async function handleNativeMessages(options: HandleNativeMessagesOptions)
391
420
  const oauthProvider = (binding: NativeOAuthBinding): OcxProviderConfig => ({ ...route.provider, apiKey: binding.snapshot.accessToken });
392
421
  if (route.provider.authMode === "oauth") {
393
422
  try {
394
- oauthBinding = await resolveNativeOAuthBinding(config, { sessionKey: options.sessionKey, model: route.modelId });
423
+ oauthBinding = await resolveNativeOAuthBindingForInstance(nativeInstance!, config, { routeTarget, sessionKey: options.sessionKey, model: route.modelId });
395
424
  } catch (error) {
396
425
  cleanupAbort();
397
426
  upstream.abort();
@@ -411,6 +440,7 @@ export async function handleNativeMessages(options: HandleNativeMessagesOptions)
411
440
  stampApiKeyAccountLabel(logCtx, route.providerName, activeProvider);
412
441
  if (oauthBinding) logCtx.provider = formatAnthropicProviderForLog(route.providerName, oauthBinding.snapshot.accountId, config);
413
442
  const spendTracker = attachRequestSpendTracker(req, logCtx);
443
+ const physicalBudget = createInferenceSendBudget(req, logCtx);
414
444
  let activeRequest: AnthropicMessagesPassthroughRequest;
415
445
  let retainedRequestBytes = 0;
416
446
  const releaseRetainedRequest = () => {
@@ -473,8 +503,17 @@ export async function handleNativeMessages(options: HandleNativeMessagesOptions)
473
503
  : Number.POSITIVE_INFINITY;
474
504
  const transientSendAvailable = (): boolean => remainingTransientSends() > 0;
475
505
  const selector: NativeMessagesSelector = options.selector ?? {};
506
+ const nativeOAuthRouteIsCurrent = (binding: NativeOAuthBinding): boolean => {
507
+ const provider = config.providers[binding.instance];
508
+ if (configuredAnthropicInstance(config, binding.instance) !== binding.instance || !provider) return false;
509
+ // Same normalization as the bound target (nativeOAuthRouteTarget): compare routed destinations.
510
+ let routed: OcxProviderConfig;
511
+ try { routed = routedProviderConfig(binding.instance, provider); } catch { return false; }
512
+ return resolveAnthropicMessagesUrl(routed) === routeTarget
513
+ && nativeMessagesDeclineReason({ ...route, provider: routed }, body, config, selector) === undefined;
514
+ };
476
515
 
477
- const send = async (recovery?: "rate-limit-429" | "oauth-account-403" | "key-429" | "key-401"): Promise<Response> => {
516
+ const send = async (recovery?: "rate-limit-429" | "oauth-401" | "oauth-account-403" | "key-429" | "key-401"): Promise<Response> => {
478
517
  const remaining = remainingTransientSends();
479
518
  if (requestTransientPolicy && remaining <= 0) {
480
519
  throw new Error("native Messages transient send budget exhausted before recovery dispatch");
@@ -493,12 +532,13 @@ export async function handleNativeMessages(options: HandleNativeMessagesOptions)
493
532
  modelId: route.modelId,
494
533
  dispatchOverride: async (_input, init, execute) => {
495
534
  if (oauthBinding) {
535
+ if (!nativeOAuthRouteIsCurrent(oauthBinding)) throw new NativeOAuthSelectionChangedError();
496
536
  // The OAuth twin of the key check below: re-resolve through the same selection
497
537
  // owner when the committed account or its credential moved since the build.
498
538
  for (let attempt = 0; !nativeOAuthBindingIsCurrent(oauthBinding); attempt++) {
499
539
  if (attempt >= 3) throw new NativeOAuthSelectionChangedError();
500
540
  try {
501
- oauthBinding = await resolveNativeOAuthBinding(config, { sessionKey: options.sessionKey, model: route.modelId });
541
+ oauthBinding = await resolveNativeOAuthBindingForInstance(nativeInstance!, config, { routeTarget, sessionKey: options.sessionKey, model: route.modelId });
502
542
  } catch (error) {
503
543
  if (error instanceof OAuthAccountPausedError || error instanceof OAuthLoginRequiredError
504
544
  || error instanceof AnthropicAccountCooldownError || error instanceof NativeOAuthModelRouteError) throw error;
@@ -519,31 +559,58 @@ export async function handleNativeMessages(options: HandleNativeMessagesOptions)
519
559
  const headers = new Headers(wire.headers);
520
560
  const encoding = new Headers(init.headers).get("accept-encoding");
521
561
  if (!headers.has("accept-encoding") && encoding) headers.set("accept-encoding", encoding);
522
- const snapshot = oauthBinding?.snapshot;
523
- const providerAccountUuid = oauthBinding?.providerAccountUuid;
562
+ const sendingBinding = oauthBinding;
563
+ const snapshot = sendingBinding?.snapshot;
564
+ const providerAccountUuid = sendingBinding?.providerAccountUuid;
524
565
  const writerGeneration = snapshot ? captureConfigGeneration() : 0;
525
- const ownsBearer = snapshot && headers.get("authorization") === `Bearer ${snapshot.accessToken}` && !headers.has("x-api-key");
526
- const releaseFamily = snapshot ? claimAnthropicFamilyRevalidation(snapshot.accountId, route.modelId) : () => {};
566
+ const ownsBearer = snapshot && snapshot.provider === nativeInstance
567
+ && headers.get("authorization") === `Bearer ${snapshot.accessToken}` && !headers.has("x-api-key");
568
+ if (oauthBinding && (!ownsBearer || wire.url !== oauthBinding.routeTarget
569
+ || !nativeOAuthRouteIsCurrent(oauthBinding) || !nativeOAuthBindingIsCurrent(oauthBinding))) {
570
+ throw new NativeOAuthSelectionChangedError();
571
+ }
572
+ const releaseFamily = snapshot && oauthBinding ? anthropicModelQuotaFor(oauthBinding.instance).claimAnthropicFamilyRevalidation(snapshot.accountId, route.modelId) : () => {};
527
573
  if (!releaseFamily) throw new AnthropicAccountCooldownError(1);
528
574
  let dispatched: Response;
575
+ const spendReport = createPhysicalSendReporter(physicalBudget, () => ({
576
+ poolId: logCtx.spendPoolId ?? route.providerName, identityId: logCtx.accountLogLabel,
577
+ }));
578
+ let sendOwner: AnthropicPhysicalSendOwnership | null = null;
529
579
  try {
530
580
  if (init.signal?.aborted) throw init.signal.reason;
531
- if (!spendTracker.charge()) throw new NativeMessagesSpendRefusal();
581
+ // Capture before either accounting path so this request retains its start policy.
582
+ if (spendReport.beforeSend && !spendReport.beforeSend()) {
583
+ if (spendTracker.refusals) throw new NativeMessagesSpendRefusal();
584
+ throw new SendBudgetExhaustedError();
585
+ }
586
+ if (!physicalBudget.spendEnforced && !spendTracker.charge()) throw new NativeMessagesSpendRefusal();
532
587
  noteProviderAttemptSend(logCtx, route.providerName, activeProvider, logCtx.usageLogInputTokens, transportRecovery ?? recovery);
588
+ // Last synchronous ownership reservation before the physical fetch. Retain this
589
+ // exact incarnation across its await; a returned response can never reserve one.
590
+ sendOwner = snapshot ? captureAnthropicPhysicalSendOwnership(snapshot) : null;
591
+ if (snapshot && !sendOwner) throw new NativeOAuthSelectionChangedError();
533
592
  dispatched = await sendWithConnectionPolicy(
534
593
  (activeProvider as OcxProviderTransport).fetch ?? execute,
535
594
  wire.url,
536
595
  applyUpstreamRecoveryInit({ ...init, method: "POST", headers, body: wire.body }, transportRecovery),
537
596
  { providerName: route.providerName, provider: activeProvider },
538
597
  );
539
- } finally { releaseFamily(); }
540
- if (ownsBearer && snapshot) {
598
+ } finally {
599
+ try { if (physicalBudget.spendEnforced) spendReport(1); }
600
+ finally {
601
+ try { spendReport.close?.(); }
602
+ finally { releaseFamily(); }
603
+ }
604
+ }
605
+ if (ownsBearer && snapshot && sendOwner && sendingBinding) {
541
606
  try {
542
- const current = getAccountCredentialWithStatus("anthropic", snapshot.accountId);
543
- if (current && !current.needsReauth && credentialGeneration(current.credential) === snapshot.generation
607
+ const current = getAccountCredentialWithStatus(snapshot.provider, snapshot.accountId);
608
+ if (anthropicPhysicalSendOwnershipIsCurrent(sendOwner) && nativeOAuthRouteIsCurrent(sendingBinding)
609
+ && current && !current.needsReauth && current.credential.access === snapshot.accessToken
610
+ && credentialGeneration(current.credential) === snapshot.generation
544
611
  && current.credential.accountId === providerAccountUuid) {
545
- bindAnthropicRefusalCredential(dispatched, snapshot, providerAccountUuid);
546
- recordAnthropicAccountQuotaFromHeaders(snapshot.accountId, dispatched.headers, writerGeneration, dispatched.status, route.modelId);
612
+ bindAnthropicRefusalCredentialForSend(dispatched, sendOwner, providerAccountUuid);
613
+ recordAnthropicAccountQuotaFromHeadersForInstance(nativeInstance!, snapshot.accountId, dispatched.headers, writerGeneration, dispatched.status, route.modelId);
547
614
  }
548
615
  } catch { /* Passive observation must not fail the response. */ }
549
616
  }
@@ -618,16 +685,17 @@ export async function handleNativeMessages(options: HandleNativeMessagesOptions)
618
685
  const oauthRetryKey = {};
619
686
  const triedAccountIds = new Set<string>();
620
687
  let oauthFailovers = 0;
621
- while (oauthBinding && (response.status === 429 || response.status === 403)) {
688
+ while (oauthBinding && (response.status === 429 || response.status === 403 || response.status === 401)) {
622
689
  const sendingBinding = oauthBinding;
623
690
  const expectedRecoverySelection = sendingBinding.selection;
624
691
  triedAccountIds.add(sendingBinding.snapshot.accountId);
625
- const nextAccountId = await rotateAnthropicAccountOnResponse(response, {
692
+ const nextAccountId = await rotateAnthropicAccountOnResponseForInstance(sendingBinding.instance, response, {
626
693
  config, accountId: sendingBinding.snapshot.accountId, model: route.modelId,
627
694
  sessionKey: sendingBinding.sessionKey, decision: sendingBinding.routeDecision,
628
695
  excludedAccountIds: triedAccountIds,
629
696
  currentDecision: () => {
630
- const resolved = resolveAnthropicModelRoute(config, route.modelId);
697
+ if (!nativeOAuthRouteIsCurrent(sendingBinding)) throw new NativeOAuthSelectionChangedError();
698
+ const resolved = resolveAnthropicModelRouteForInstance(sendingBinding.instance, config, route.modelId);
631
699
  if (resolved.error) throw new NativeOAuthModelRouteError(`Invalid Anthropic model routes: ${resolved.error}`);
632
700
  return resolved.decision;
633
701
  },
@@ -635,9 +703,9 @@ export async function handleNativeMessages(options: HandleNativeMessagesOptions)
635
703
  canRetry: transientSendAvailable() && oauthFailovers < ANTHROPIC_POOL_MAX_FAILOVERS_PER_REQUEST,
636
704
  });
637
705
  if (!nextAccountId) break;
638
- const recovery = response.status === 403 ? "oauth-account-403" : "rate-limit-429";
706
+ const recovery = response.status === 401 ? "oauth-401" : response.status === 403 ? "oauth-account-403" : "rate-limit-429";
639
707
  discard(response);
640
- oauthBinding = await resolveNativeOAuthBinding(config, {
708
+ oauthBinding = await resolveNativeOAuthBindingForInstance(nativeInstance!, config, { routeTarget,
641
709
  sessionKey: options.sessionKey, model: route.modelId, candidateAccountId: nextAccountId,
642
710
  expectedRecoverySelection, expectedRecoveryRouteDecision: sendingBinding.routeDecision,
643
711
  });
@@ -652,6 +720,7 @@ export async function handleNativeMessages(options: HandleNativeMessagesOptions)
652
720
  upstream.abort();
653
721
  if (req.signal.aborted) return fail(499, "Client cancelled request", "api_error");
654
722
  const sendError = error instanceof UpstreamRetryEvidenceError ? error.cause : error;
723
+ if (sendError instanceof SendBudgetExhaustedError) return fail(429, sendError.message, "send_budget_exhausted", "send_budget_exhausted");
655
724
  if (sendError instanceof NativeMessagesSpendRefusal) {
656
725
  const refusal = workflowRefusalResponse("workflow-spend-exhausted", logCtx);
657
726
  finishLog(429);
@@ -683,7 +752,7 @@ export async function handleNativeMessages(options: HandleNativeMessagesOptions)
683
752
  upstream.abort();
684
753
  return fail(499, "Client cancelled request", "api_error");
685
754
  }
686
- return nativeMessagesErrorResponse(response, bodyText, finishLog);
755
+ return retainUpstreamMessagesRequestId(nativeMessagesErrorResponse(response, bodyText, finishLog), response.headers);
687
756
  }
688
757
 
689
758
  const contentType = response.headers.get("content-type")?.toLowerCase() ?? "";
@@ -708,10 +777,15 @@ export async function handleNativeMessages(options: HandleNativeMessagesOptions)
708
777
  releaseStreamTurn();
709
778
  }
710
779
  }, bodyGuard);
711
- return new Response(relayed, {
780
+ // A configured Messages provider words an oversized-input refusal its own way; Claude Code
781
+ // compacts only on Anthropic's wording (devlog/_plan/261009_claude_1m_default/010). The
782
+ // rewrite buffers whole frames, so it sits after the tap: stall detection keeps timing raw
783
+ // upstream bytes. Real Anthropic pools refuse pre-stream in that wording and keep one relay.
784
+ const clientStream = nativeInstance ? relayed : relaySseWithPayloadRewrite(relayed, claudeOverflowSsePayload, translatorBudget);
785
+ return retainUpstreamMessagesRequestId(new Response(clientStream, {
712
786
  status: 200,
713
787
  headers: { "Content-Type": "text/event-stream; charset=utf-8", "Cache-Control": "no-cache", Connection: "keep-alive" },
714
- });
788
+ }), response.headers);
715
789
  }
716
790
  // A non-streaming caller whose upstream streamed anyway: fold the stream into one message.
717
791
  const tapState: { closeReason?: FinalRequestLogMeta["closeReason"]; meta?: FinalRequestLogMeta } = {};
@@ -733,10 +807,11 @@ export async function handleNativeMessages(options: HandleNativeMessagesOptions)
733
807
  // as the streaming lane's row; a plain upstream error event keeps the non_stream row.
734
808
  const tapMeta = tapState.meta;
735
809
  if (tapMeta && (tapMeta.terminalStatus || tapMeta.closeReason !== "terminal")) finishLog(502, text, tapMeta);
810
+ if (isNativeOverflowError(error, text)) return fail(400, claudePromptTooLongMessage(text), "invalid_request_error");
736
811
  return fail(502, text, "api_error");
737
812
  }
738
813
  finishLog(200);
739
- return Response.json(message);
814
+ return retainUpstreamMessagesRequestId(Response.json(message), response.headers);
740
815
  } catch (error) {
741
816
  cleanupAbort();
742
817
  upstream.abort();
@@ -794,12 +869,19 @@ export async function handleNativeMessages(options: HandleNativeMessagesOptions)
794
869
  const serialized = JSON.stringify(message);
795
870
  finishLog(200);
796
871
  if (requestedStream) {
797
- return new Response(messageAsSse(message), {
872
+ return retainUpstreamMessagesRequestId(new Response(messageAsSse(message), {
798
873
  status: 200,
799
874
  headers: { "Content-Type": "text/event-stream; charset=utf-8", "Cache-Control": "no-cache" },
800
- });
875
+ }), response.headers);
801
876
  }
802
- return new Response(serialized, { status: 200, headers: { "Content-Type": "application/json" } });
877
+ return retainUpstreamMessagesRequestId(new Response(serialized, { status: 200, headers: { "Content-Type": "application/json" } }), response.headers);
878
+ }
879
+
880
+ /** A folded stream error that refuses an oversized input (same gate as the SSE rewrite). */
881
+ function isNativeOverflowError(error: Rec, message: string): boolean {
882
+ const sized = error.type === "invalid_request_error" || error.type === "request_too_large";
883
+ return sized && !isThroughputLimitText(message)
884
+ && (error.code === "context_length_exceeded" || isContextOverflowText(message));
803
885
  }
804
886
 
805
887
  /**
@@ -809,11 +891,13 @@ export async function handleNativeMessages(options: HandleNativeMessagesOptions)
809
891
  */
810
892
  function nativeMessagesErrorResponse(response: Response, bodyText: string, finishLog: FinishLog): Response {
811
893
  let upstreamType: string | undefined;
894
+ let upstreamCode: string | undefined;
812
895
  let upstreamMessage: string | undefined;
813
896
  try {
814
897
  const parsed = JSON.parse(bodyText) as Rec;
815
898
  const details = isRec(parsed.error) ? parsed.error : parsed;
816
899
  if (typeof details.type === "string") upstreamType = details.type;
900
+ if (typeof details.code === "string") upstreamCode = details.code;
817
901
  if (typeof details.message === "string" && details.message.trim()) {
818
902
  upstreamMessage = redactSecretString(details.message.trim());
819
903
  }
@@ -842,9 +926,17 @@ function nativeMessagesErrorResponse(response: Response, bodyText: string, finis
842
926
  if (retryAfter) headers.set("Retry-After", retryAfter);
843
927
  else if (transient) headers.set("Retry-After", "2");
844
928
  if (replayRefusal) applyReplayRefusalClientHeaders(headers);
929
+ // The native envelope keeps its shape (no added code); only an overflow's wording changes, so
930
+ // Claude Code recognizes it and compacts (010).
931
+ // Only a 400/413 refusal: the shared classifier also files a 429 "too many tokens per minute"
932
+ // under context_length_exceeded, and that one must stay a rate limit.
933
+ const overflow = !replayRefusal && (response.status === 400 || response.status === 413)
934
+ && !isThroughputLimitText(safeMessage)
935
+ && (classified.code === "context_length_exceeded" || upstreamCode === "context_length_exceeded"
936
+ || isContextOverflowText(safeMessage));
845
937
  const out = new Response(JSON.stringify(anthropicErrorBody(
846
938
  status,
847
- safeMessage,
939
+ overflow ? claudePromptTooLongMessage(safeMessage) : safeMessage,
848
940
  transient ? "overloaded_error" : upstreamType,
849
941
  replayRefusal ? UPSTREAM_RESET_REPLAY_REFUSED_CODE : undefined,
850
942
  )), { status, headers });
@@ -865,6 +957,7 @@ export function nativeMessagesCountBody(
865
957
  ): Rec | undefined {
866
958
  if (typeof body.model !== "string") return undefined;
867
959
  try {
960
+ if (messagesSecondaryInstanceUnavailable(config, body.model, cc)) return undefined;
868
961
  const selectorId = resolveInboundModel(body.model, cc);
869
962
  // Combo and policy selectors are never eligible, and routing them would advance round-robin
870
963
  // state or run the policy evaluator for a request that sends nothing.
@@ -875,11 +968,13 @@ export function nativeMessagesCountBody(
875
968
  route.providerName, route.modelId, route.provider, route.staticPolicy.effectiveAlias, "anthropic",
876
969
  );
877
970
  route.provider = resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, "anthropic", route.staticPolicy);
971
+ const instance = configuredAnthropicInstance(config, route.providerName);
878
972
  const selector: NativeMessagesSelector = {
879
973
  ...rows,
880
974
  routeSelector: selectorId,
881
975
  claudeCode: cc,
882
- ...(route.provider.authMode === "oauth" ? { oauthFailoverQuorum: hasAnthropicFailoverQuorum() } : {}),
976
+ ...(route.provider.authMode === "oauth" && instance
977
+ ? { oauthFailoverQuorum: anthropicRoutingFor(instance).hasAnthropicFailoverQuorum() } : {}),
883
978
  };
884
979
  if (nativeMessagesDeclineReason(route, body, config, selector) !== undefined) return undefined;
885
980
  // The body without a credential: counting never resolves or refreshes an OAuth account.
@@ -0,0 +1,14 @@
1
+ /** Only an opaque Anthropic request id is carried; arbitrary upstream headers stay private. */
2
+ export function upstreamMessagesRequestIdHeaders(upstream: Headers): Record<string, string> {
3
+ const requestId = upstream.get("request-id");
4
+ return requestId && /^req_[A-Za-z0-9_-]{1,128}$/.test(requestId)
5
+ ? { "request-id": requestId }
6
+ : {};
7
+ }
8
+
9
+ /** The response is newly constructed by the Messages handler; preserve its body and markers. */
10
+ export function retainUpstreamMessagesRequestId(response: Response, upstream: Headers): Response {
11
+ const requestId = upstreamMessagesRequestIdHeaders(upstream)["request-id"];
12
+ if (requestId) response.headers.set("request-id", requestId);
13
+ return response;
14
+ }
@@ -37,6 +37,47 @@ export function normalizeLogConversationId(raw: string | undefined | null): stri
37
37
  return createHash("sha256").update(trimmed).digest("hex").slice(0, LOG_CONVERSATION_ID_LEN);
38
38
  }
39
39
 
40
+ const CODEX_THREAD_LINK_PREFIX = "codex://threads/";
41
+ /** Real thread links stay well under this bound; larger pastes skip unwrapping so per-row matching stays cheap. */
42
+ const LOG_CONVERSATION_LINK_MAX = 512;
43
+
44
+ /**
45
+ * Unwrap a pasted `codex://threads/<id>` deep link to the bare thread id. Codex's
46
+ * "copy session link" clipboard carries that URI form while clients send the bare id
47
+ * on the wire, so Logs search accepts either paste without changing what is persisted.
48
+ * Query/fragment metadata (e.g. `?hostId=…`) belongs to the link, not the id, and is
49
+ * dropped. The scan is a bounded linear walk — no ambiguous regex — because this runs
50
+ * per log row on every filter evaluation.
51
+ */
52
+ export function unwrapLogConversationQuery(query: string): string {
53
+ const trimmed = query.trim();
54
+ if (
55
+ trimmed.length > LOG_CONVERSATION_LINK_MAX ||
56
+ trimmed.length <= CODEX_THREAD_LINK_PREFIX.length ||
57
+ !trimmed.toLowerCase().startsWith(CODEX_THREAD_LINK_PREFIX)
58
+ ) {
59
+ return trimmed;
60
+ }
61
+ const rest = trimmed.slice(CODEX_THREAD_LINK_PREFIX.length);
62
+ const metaIndex = rest.search(/[?#]/);
63
+ const segment = (metaIndex === -1 ? rest : rest.slice(0, metaIndex)).replace(/\/+$/, "");
64
+ // A thread id is a single path segment; deeper paths or embedded whitespace keep the
65
+ // whole paste as the query, so foreign `codex://` URIs still fail to match.
66
+ return segment !== "" && !/[/\s]/.test(segment) ? segment : trimmed;
67
+ }
68
+
69
+ /**
70
+ * The candidate ids a conversation query may legitimately match. Unwrapping a pasted
71
+ * `codex://threads/<id>` link must add a match path, not replace one: a client can
72
+ * literally send a `codex://threads/…` session id, which is hashed whole for storage,
73
+ * so the untouched paste stays a candidate too.
74
+ */
75
+ export function logConversationQueryCandidates(query: string): string[] {
76
+ const trimmed = query.trim();
77
+ const unwrapped = unwrapLogConversationQuery(trimmed);
78
+ return unwrapped === trimmed ? [unwrapped] : [unwrapped, trimmed];
79
+ }
80
+
40
81
  /**
41
82
  * Filter match: accept either the persisted digest or the original preimage
42
83
  * (so pasting from Logs detail or the client-facing session id both work).
@@ -46,11 +87,13 @@ export function matchesLogConversationId(
46
87
  query: string | undefined | null,
47
88
  ): boolean {
48
89
  if (!stored) return false;
49
- const trimmed = typeof query === "string" ? query.trim() : "";
50
- if (!trimmed) return false;
51
- if (stored === trimmed) return true;
52
- const hashed = normalizeLogConversationId(trimmed);
53
- return hashed !== undefined && stored === hashed;
90
+ for (const candidate of logConversationQueryCandidates(typeof query === "string" ? query : "")) {
91
+ if (!candidate) continue;
92
+ if (stored === candidate) return true;
93
+ const hashed = normalizeLogConversationId(candidate);
94
+ if (hashed !== undefined && stored === hashed) return true;
95
+ }
96
+ return false;
54
97
  }
55
98
 
56
99
  /**
@@ -63,6 +63,7 @@ import {
63
63
  type UsageStatus,
64
64
  } from "../usage/log";
65
65
  import type { RequestExecutionBudget } from "../lib/request-execution-budget";
66
+ import type { WorkflowSpendDenialDetail } from "../lib/workflow-budget";
66
67
  import { attributeFinalRequest, attributeSealedAttempt } from "./request-log-failure-attribution";
67
68
  import { debugAttemptDeliverySummary } from "../lib/debug";
68
69
  import {
@@ -203,6 +204,10 @@ export interface RequestLogContext {
203
204
  spendOutputCeilingTokens?: number;
204
205
  /** Pre-send input estimate reserved for spend only; unlike usageLogInputTokens it never enters usage. */
205
206
  spendInputEstimateTokens?: number;
207
+ /** Canonical provider-pool spend identity; independent of mutable, account-specific log labels. */
208
+ spendPoolId?: string;
209
+ /** Internal safe refusal detail for a request whose ceiling includes unidentified old pool spend. */
210
+ spendRefusalDetail?: WorkflowSpendDenialDetail;
206
211
  /** Settles this request's durable spend entries from `addFinalRequestLog`. */
207
212
  spendTracker?: RequestSpendSettlement;
208
213
  attempts?: PersistedUsageAttempt[];
@@ -227,6 +232,8 @@ export interface RequestLogContext {
227
232
  * message) extracted from a `response.failed` SSE payload or non-streaming error body, so the
228
233
  * request log / GUI shows the actual upstream failure rather than only the HTTP-mapped code. */
229
234
  upstreamError?: string;
235
+ upstreamErrorCode?: string;
236
+ upstreamRequestId?: string;
230
237
  /** HTTP status derived from a terminal `response.failed` SSE payload (429/401/503/etc.). */
231
238
  terminalHttpStatus?: number;
232
239
  /** Recognized structured terminal code whose exact identity must survive status mapping. */
@@ -337,6 +344,8 @@ export interface RequestLogEntry {
337
344
  closeReason?: "terminal" | "client_cancel" | "non_stream" | "body_stall" | "body_overflow";
338
345
  /** Secret-redacted upstream error reason, surfaced in /api/logs and the GUI detail modal. */
339
346
  upstreamError?: string;
347
+ upstreamErrorCode?: string;
348
+ upstreamRequestId?: string;
340
349
  usageStatus: UsageStatus;
341
350
  usage?: OcxUsage;
342
351
  totalTokens?: number;
@@ -1090,6 +1099,24 @@ export function inspectResponseLogSsePayloadParsed(
1090
1099
  * a non-streaming JSON error body. We keep the FIRST non-empty reason (the original failure) and
1091
1100
  * run it through redactSecretString so secrets never reach /api/logs. Pure; safe on any text.
1092
1101
  */
1102
+ const UPSTREAM_DIAGNOSTIC_TOKEN = /^[A-Za-z0-9_.:-]{1,128}$/;
1103
+
1104
+ function noteBoundedDiagnostic(
1105
+ logCtx: RequestLogContext,
1106
+ field: "upstreamErrorCode" | "upstreamRequestId",
1107
+ value: unknown,
1108
+ ): void {
1109
+ if (logCtx[field] !== undefined) return;
1110
+ if (typeof value !== "string" || !UPSTREAM_DIAGNOSTIC_TOKEN.test(value)) return;
1111
+ logCtx[field] = value;
1112
+ const status = logCtx.terminalHttpStatus;
1113
+ if (status === undefined || status >= 500) console.warn(`[opencodex] upstream failure${status ? ` status=${status}` : ""}${logCtx.upstreamErrorCode ? ` code=${logCtx.upstreamErrorCode}` : ""}${logCtx.upstreamRequestId ? ` request_id=${logCtx.upstreamRequestId}` : ""}`);
1114
+ }
1115
+
1116
+ export function noteUpstreamRequestId(logCtx: RequestLogContext, headers: Headers): void {
1117
+ noteBoundedDiagnostic(logCtx, "upstreamRequestId", headers.get("openai-request-id") ?? headers.get("x-request-id"));
1118
+ }
1119
+
1093
1120
  function captureUpstreamError(logCtx: RequestLogContext, text: string | null): void {
1094
1121
  if (!text) return;
1095
1122
  let parsed: unknown | undefined;
@@ -1109,14 +1136,19 @@ function captureUpstreamErrorParsed(
1109
1136
  if (parsed !== undefined && parsed !== null) {
1110
1137
  const json = parsed as {
1111
1138
  type?: unknown;
1112
- error?: { message?: unknown };
1113
- last_error?: { message?: unknown };
1139
+ error?: { message?: unknown; code?: unknown };
1140
+ last_error?: { message?: unknown; code?: unknown };
1114
1141
  response?: {
1115
1142
  error?: { type?: unknown; code?: unknown; message?: unknown };
1116
1143
  incomplete_details?: { reason?: unknown; message?: unknown };
1117
1144
  };
1118
1145
  };
1119
1146
  captureTerminalHttpStatus(logCtx, json);
1147
+ noteBoundedDiagnostic(
1148
+ logCtx,
1149
+ "upstreamErrorCode",
1150
+ json.error?.code ?? json.response?.error?.code ?? json.last_error?.code,
1151
+ );
1120
1152
  const reason = json?.response?.incomplete_details?.reason;
1121
1153
  if (json.type === "response.incomplete"
1122
1154
  && logCtx.terminalIncompleteReason === undefined
@@ -1588,6 +1620,8 @@ export function addFinalRequestLog(
1588
1620
  ...(meta?.terminalStatus ? { terminalStatus: meta.terminalStatus } : {}),
1589
1621
  ...(closeReason ? { closeReason } : {}),
1590
1622
  ...(logCtx.upstreamError ? { upstreamError: logCtx.upstreamError } : {}),
1623
+ ...(logCtx.upstreamErrorCode ? { upstreamErrorCode: logCtx.upstreamErrorCode } : {}),
1624
+ ...(logCtx.upstreamRequestId ? { upstreamRequestId: logCtx.upstreamRequestId } : {}),
1591
1625
  usageStatus,
1592
1626
  ...(loggedUsage ? { usage: loggedUsage } : {}),
1593
1627
  ...(totalTokens !== undefined ? { totalTokens } : {}),