@bitkyc08/opencodex 2.86.0 → 2.88.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (400) hide show
  1. package/gui/dist/assets/App-CHC89EXQ.js +58 -0
  2. package/gui/dist/assets/App-wT3mXqFz.css +1 -0
  3. package/gui/dist/assets/{Tray-DyR9yBee.js → Tray-B5bC7AWL.js} +1 -1
  4. package/gui/dist/assets/index-B5bsvxor.js +94 -0
  5. package/gui/dist/assets/index-DPeB9HpV.css +1 -0
  6. package/gui/dist/assets/quota-summary-D8y2wTsB.js +1 -0
  7. package/gui/dist/index.html +2 -2
  8. package/package.json +1 -1
  9. package/src/adapters/anthropic/beta-allowlist.ts +9 -8
  10. package/src/adapters/anthropic/beta-body-pairs.ts +93 -0
  11. package/src/adapters/anthropic/passthrough.ts +7 -23
  12. package/src/adapters/anthropic-progress.ts +84 -0
  13. package/src/adapters/anthropic-rejection-memo.ts +71 -0
  14. package/src/adapters/anthropic-replay.ts +135 -0
  15. package/src/adapters/anthropic-tool-catalog.ts +11 -0
  16. package/src/adapters/anthropic.ts +151 -33
  17. package/src/adapters/base.ts +2 -0
  18. package/src/adapters/claude-cli/adapter.ts +111 -23
  19. package/src/adapters/claude-cli/profiles.ts +2 -1
  20. package/src/adapters/claude-cli/stable-replay.ts +108 -0
  21. package/src/adapters/claude-cli/usage-admission.ts +230 -0
  22. package/src/adapters/coding-agent/protocol.ts +1 -1
  23. package/src/adapters/coding-agent/turn.ts +18 -2
  24. package/src/adapters/dsh-account.ts +97 -0
  25. package/src/adapters/google-progress-history.ts +53 -0
  26. package/src/adapters/google.ts +52 -9
  27. package/src/adapters/openai-chat/messages.ts +4 -3
  28. package/src/adapters/openai-chat/passthrough.ts +8 -2
  29. package/src/adapters/openai-chat/reasoning-replay.ts +52 -0
  30. package/src/adapters/openai-responses/compaction-search-history.ts +14 -7
  31. package/src/adapters/openai-responses/passthrough.ts +57 -125
  32. package/src/adapters/openai-responses/request-strips.ts +25 -3
  33. package/src/adapters/openai-responses/transport.ts +81 -0
  34. package/src/adapters/qoder/adapter.ts +4 -2
  35. package/src/adapters/registry.ts +5 -0
  36. package/src/adapters/zed.ts +26 -1
  37. package/src/advisor/activity.ts +55 -0
  38. package/src/advisor/budget.ts +62 -0
  39. package/src/advisor/consult.ts +4 -1
  40. package/src/advisor/context.ts +16 -4
  41. package/src/advisor/evidence-tool.ts +21 -0
  42. package/src/advisor/evidence.ts +117 -0
  43. package/src/advisor/runtime.ts +79 -31
  44. package/src/advisor/settings.ts +11 -2
  45. package/src/bridge/errors.ts +19 -1
  46. package/src/bridge/internal.ts +7 -1
  47. package/src/bridge/response-json.ts +18 -3
  48. package/src/bridge/sse.ts +59 -8
  49. package/src/bridge/terminal-refusal.ts +59 -0
  50. package/src/claude/desktop-3p-library.ts +32 -7
  51. package/src/claude/desktop-3p.ts +55 -13
  52. package/src/claude/desktop-first-party.ts +1 -1
  53. package/src/claude/desktop-picker-platform.ts +4 -0
  54. package/src/claude/desktop-picker.ts +15 -8
  55. package/src/claude/desktop-system-proxy.ts +50 -4
  56. package/src/claude/inbound-content-options.ts +77 -0
  57. package/src/claude/inbound.ts +33 -0
  58. package/src/claude/intercept/picker-ca-startup.ts +2 -1
  59. package/src/claude/intercept/picker-runtime.ts +6 -5
  60. package/src/claude/intercept/picker-trust-windows.ts +338 -0
  61. package/src/claude/intercept/picker-trust.ts +19 -0
  62. package/src/claude/intercept/port-policy.ts +30 -0
  63. package/src/claude/intercept/runtime.ts +97 -94
  64. package/src/claude/outbound.ts +46 -13
  65. package/src/cli/account-api.ts +1 -0
  66. package/src/cli/account-policy-dto.ts +6 -0
  67. package/src/cli/account-policy.ts +34 -5
  68. package/src/cli/account-preservation.ts +44 -0
  69. package/src/cli/account.ts +8 -2
  70. package/src/cli/advisor.ts +17 -3
  71. package/src/cli/capabilities-accounts.ts +20 -1
  72. package/src/cli/capabilities-agents-routing.ts +5 -2
  73. package/src/cli/capabilities-base.ts +38 -10
  74. package/src/cli/capabilities-observe-system.ts +63 -3
  75. package/src/cli/capabilities-provider-models.ts +11 -0
  76. package/src/cli/codex-cli-update.ts +151 -41
  77. package/src/cli/codex-provider.ts +44 -0
  78. package/src/cli/combo-input.ts +10 -1
  79. package/src/cli/combo-result.ts +1 -1
  80. package/src/cli/combo.ts +2 -1
  81. package/src/cli/cross-home-owner.ts +33 -16
  82. package/src/cli/dispatch.ts +25 -4
  83. package/src/cli/doctor.ts +6 -1
  84. package/src/cli/ensure-desktop-cold-start.ts +60 -7
  85. package/src/cli/help.ts +3 -2
  86. package/src/cli/index.ts +1 -1
  87. package/src/cli/integration-plan-dto.ts +1 -0
  88. package/src/cli/log-history.ts +172 -0
  89. package/src/cli/loopback-family.ts +31 -0
  90. package/src/cli/models-runtime-subcommands.ts +1 -0
  91. package/src/cli/models-runtime.ts +72 -8
  92. package/src/cli/observe-stream.ts +2 -0
  93. package/src/cli/observe.ts +16 -4
  94. package/src/cli/provider.ts +27 -5
  95. package/src/cli/registry.ts +28 -8
  96. package/src/cli/runtime-api.ts +5 -3
  97. package/src/cli/system-command.ts +40 -8
  98. package/src/cli/uninstall-client-state.ts +32 -0
  99. package/src/cli/usage-compare.ts +309 -0
  100. package/src/cli/usage-model-search.ts +4 -4
  101. package/src/cli/usage-report.ts +2 -2
  102. package/src/cli/v2.ts +1 -1
  103. package/src/client/machine-listener.ts +1 -3
  104. package/src/client/runtime.ts +1 -0
  105. package/src/clients/client-path.ts +51 -0
  106. package/src/clients/config-export/contracts.ts +2 -1
  107. package/src/clients/config-export.ts +18 -44
  108. package/src/clients/gentle-shell-path.ts +87 -0
  109. package/src/codex/account-auto-switch.ts +2 -0
  110. package/src/codex/account-identity.ts +29 -0
  111. package/src/codex/account-label.ts +1 -1
  112. package/src/codex/account-lifecycle.ts +21 -5
  113. package/src/codex/account-preservation.ts +28 -0
  114. package/src/codex/account-use-remaining.ts +23 -0
  115. package/src/codex/app-server-processes.ts +97 -7
  116. package/src/codex/app-server-restart-service.ts +55 -4
  117. package/src/codex/auth-api/account-list.ts +4 -0
  118. package/src/codex/auth-api/account-pause-group.ts +6 -50
  119. package/src/codex/auth-api/account-preservation.ts +57 -0
  120. package/src/codex/auth-api/routes.ts +8 -0
  121. package/src/codex/auth-api/use-remaining-route.ts +36 -0
  122. package/src/codex/auth-api.ts +0 -1
  123. package/src/codex/auth-context.ts +2 -2
  124. package/src/codex/catalog/auto-review.ts +20 -3
  125. package/src/codex/catalog/build-entries.ts +9 -1
  126. package/src/codex/catalog/combo-member.ts +14 -5
  127. package/src/codex/catalog/configured-tiers.ts +1 -1
  128. package/src/codex/catalog/gather-capture.ts +2 -0
  129. package/src/codex/catalog/metadata.ts +11 -9
  130. package/src/codex/catalog/model-hints.ts +21 -16
  131. package/src/codex/catalog/model-visibility.ts +3 -1
  132. package/src/codex/catalog/parsing.ts +30 -86
  133. package/src/codex/catalog/provider-models.ts +36 -34
  134. package/src/codex/catalog/routed-gather.ts +14 -13
  135. package/src/codex/cli-install-provenance.ts +105 -17
  136. package/src/codex/cli-update-plan.ts +545 -0
  137. package/src/codex/config-write-lock.ts +23 -2
  138. package/src/codex/desktop-app/picker-freshness.ts +96 -0
  139. package/src/codex/desktop-app-restart.ts +21 -0
  140. package/src/codex/desktop-compatibility/installed-build.ts +24 -6
  141. package/src/codex/desktop-compatibility/routing-preflight.ts +1 -1
  142. package/src/codex/desktop-compatibility/runtime.ts +12 -3
  143. package/src/codex/desktop-compatibility/windows-key-protection.ts +115 -9
  144. package/src/codex/features.ts +85 -45
  145. package/src/codex/inject/history-failure.ts +27 -0
  146. package/src/codex/inject/restore.ts +24 -2
  147. package/src/codex/inject/routing-classify.ts +4 -3
  148. package/src/codex/inject.ts +18 -27
  149. package/src/codex/main-account-hard-lock.ts +30 -1
  150. package/src/codex/native-profile-startup.ts +1 -1
  151. package/src/codex/ocx-compaction-history.ts +12 -10
  152. package/src/codex/paths.ts +86 -1
  153. package/src/codex/provider-routing.ts +174 -0
  154. package/src/codex/refresh.ts +2 -2
  155. package/src/codex/routing/health-store.ts +1 -1
  156. package/src/codex/routing/selection.ts +4 -3
  157. package/src/codex/routing/thread-affinity.ts +1 -1
  158. package/src/codex/signed-in-identity.ts +43 -0
  159. package/src/codex/subagent-model-fallback.ts +1 -1
  160. package/src/codex/sync.ts +6 -1
  161. package/src/codex/user-identity.ts +16 -5
  162. package/src/codex/websocket-activity.ts +161 -0
  163. package/src/codex/websocket-registry.ts +25 -1
  164. package/src/codex/windows-local-appdata.ts +84 -0
  165. package/src/combos/failover.ts +6 -1
  166. package/src/combos/index.ts +1 -0
  167. package/src/combos/resolve.ts +46 -10
  168. package/src/combos/types.ts +15 -0
  169. package/src/config/diagnostics.ts +33 -6
  170. package/src/config/live-reconcile.ts +38 -2
  171. package/src/config/load-degrade.ts +17 -0
  172. package/src/config/opencode-wire-upgrade.ts +54 -0
  173. package/src/config/opencode-zen-upgrade.ts +53 -0
  174. package/src/config/owner-registry.ts +22 -17
  175. package/src/config/provider-validation.ts +45 -2
  176. package/src/config/schema/config-schema.ts +22 -0
  177. package/src/config/schema/leaf-validators.ts +38 -9
  178. package/src/config/schema/main-account-hard-lock-fallback.ts +26 -0
  179. package/src/config/schema/native-reasoning-retention.ts +26 -0
  180. package/src/config.ts +2 -0
  181. package/src/generated/compatibility-version.json +657 -297
  182. package/src/grok/sync.ts +1 -1
  183. package/src/images/loop.ts +116 -27
  184. package/src/integrations/catalog-refresh.ts +1 -1
  185. package/src/integrations/mutation-plan.ts +1 -0
  186. package/src/integrations/registry.ts +8 -0
  187. package/src/lib/codex-restart-contract.ts +15 -0
  188. package/src/lib/desktop-upstream-tunnel.ts +18 -53
  189. package/src/lib/empty-tool-catalog.ts +62 -0
  190. package/src/lib/errors.ts +19 -3
  191. package/src/lib/gcp-adc.ts +96 -9
  192. package/src/lib/gcp-credential-input.ts +12 -0
  193. package/src/lib/lab-live-route-production.ts +5 -0
  194. package/src/lib/opaque-proxy-tunnel.ts +217 -0
  195. package/src/lib/redact.ts +2 -2
  196. package/src/lib/request-failure-model.ts +2 -0
  197. package/src/oauth/account-quota-rank.ts +69 -2
  198. package/src/oauth/anthropic-account-refusal.ts +20 -2
  199. package/src/oauth/anthropic-oauth-definitions.ts +3 -1
  200. package/src/oauth/anthropic-routing.ts +1 -21
  201. package/src/oauth/anthropic.ts +38 -7
  202. package/src/oauth/callback-server.ts +27 -1
  203. package/src/oauth/chatgpt-device.ts +1 -1
  204. package/src/oauth/chatgpt-shared.ts +83 -0
  205. package/src/oauth/chatgpt.ts +8 -72
  206. package/src/oauth/dsh.ts +203 -0
  207. package/src/oauth/generic-account-failover.ts +40 -11
  208. package/src/oauth/generic-session-affinity.ts +125 -0
  209. package/src/oauth/index.ts +184 -109
  210. package/src/oauth/local-token-detect.ts +21 -1
  211. package/src/oauth/login-cli-errors.ts +44 -0
  212. package/src/oauth/login-cli.ts +69 -7
  213. package/src/oauth/login-flow-state.ts +371 -23
  214. package/src/oauth/meta-muse-errors.ts +41 -0
  215. package/src/oauth/meta-muse-keychain.ts +52 -0
  216. package/src/oauth/meta-muse.ts +51 -84
  217. package/src/oauth/model-discovery-auth.ts +3 -0
  218. package/src/oauth/pool-settings-capability.ts +4 -2
  219. package/src/oauth/refresh-flight-retirement.ts +48 -0
  220. package/src/oauth/session-affinity-key.ts +23 -0
  221. package/src/oauth/store-anthropic-instance.ts +9 -2
  222. package/src/oauth/store.ts +12 -5
  223. package/src/protocols/encoders/adapter-events.ts +6 -5
  224. package/src/protocols/encoders/messages.ts +9 -6
  225. package/src/protocols/provider-summary.ts +2 -1
  226. package/src/providers/api-key-cooldowns.ts +49 -0
  227. package/src/providers/api-key-resolve.ts +11 -6
  228. package/src/providers/api-key-response-ownership.ts +57 -0
  229. package/src/providers/api-key-selection.ts +31 -6
  230. package/src/providers/api-keys.ts +214 -3
  231. package/src/providers/claude-cli-usage.ts +96 -0
  232. package/src/providers/dsh-account-destination.ts +31 -0
  233. package/src/providers/dsh-account-models.ts +41 -0
  234. package/src/providers/gcp-credential-cleanup.ts +59 -0
  235. package/src/providers/key-failover.ts +54 -50
  236. package/src/providers/key-store.ts +7 -2
  237. package/src/providers/model-rename-fields.ts +3 -0
  238. package/src/providers/model-rename-migration.ts +2 -2
  239. package/src/providers/openai-tiers-destination.ts +11 -2
  240. package/src/providers/openai-tiers.ts +5 -12
  241. package/src/providers/opencode-go-transport.ts +3 -1
  242. package/src/providers/opencode-zen-rate-limit.ts +7 -2
  243. package/src/providers/quota/account-cache.ts +6 -5
  244. package/src/providers/quota/account-row.ts +34 -0
  245. package/src/providers/quota/antigravity.ts +28 -7
  246. package/src/providers/quota/dsh-account.ts +70 -0
  247. package/src/providers/quota/vendor-probes-oauth.ts +1 -93
  248. package/src/providers/quota-types.ts +2 -0
  249. package/src/providers/quota.ts +14 -20
  250. package/src/providers/registry/entries-core.ts +37 -8
  251. package/src/providers/registry/types.ts +1 -1
  252. package/src/providers/request-pacing.ts +77 -19
  253. package/src/providers/routed-config.ts +339 -0
  254. package/src/providers/service-tier.ts +2 -1
  255. package/src/quota/window-mapping.ts +5 -2
  256. package/src/responses/anthropic-progress.ts +34 -0
  257. package/src/responses/assistant-replay.ts +35 -0
  258. package/src/responses/compaction-history-framing.ts +65 -0
  259. package/src/responses/compaction.ts +6 -1
  260. package/src/responses/incomplete-reason.ts +31 -0
  261. package/src/responses/parser.ts +50 -5
  262. package/src/responses/reasoning-envelope.ts +4 -0
  263. package/src/responses/reasoning-replay-cache.ts +74 -6
  264. package/src/responses/reasoning-retention.ts +173 -0
  265. package/src/responses/retained-compaction.ts +46 -0
  266. package/src/responses/schema.ts +23 -0
  267. package/src/router.ts +8 -321
  268. package/src/routing/analytics.ts +7 -15
  269. package/src/routing/health.ts +37 -22
  270. package/src/routing/history/indexer.ts +6 -5
  271. package/src/routing/history/ingest.ts +6 -2
  272. package/src/routing/profile.ts +1 -1
  273. package/src/server/admission-model-scope.ts +17 -0
  274. package/src/server/auth-cors.ts +11 -0
  275. package/src/server/chat-completions.ts +7 -1
  276. package/src/server/chat-native-reasoning.ts +123 -0
  277. package/src/server/chat-native-sse.ts +110 -7
  278. package/src/server/chat-native.ts +76 -17
  279. package/src/server/claude-messages.ts +56 -414
  280. package/src/server/effort-policy.ts +1 -1
  281. package/src/server/images.ts +15 -2
  282. package/src/server/index/live-sideband.ts +3 -2
  283. package/src/server/index/model-alias-id.ts +36 -0
  284. package/src/server/index/optional-listeners.ts +2 -0
  285. package/src/server/index/serve-options.ts +20 -6
  286. package/src/server/index/startup-warnings.ts +2 -0
  287. package/src/server/index/websocket-handler.ts +18 -4
  288. package/src/server/index.ts +8 -8
  289. package/src/server/inference/advisor-diagnostics.ts +31 -0
  290. package/src/server/lifecycle.ts +13 -1
  291. package/src/server/management/advisor-routes.ts +15 -4
  292. package/src/server/management/claude-desktop-picker-routes.ts +1 -1
  293. package/src/server/management/combo-routes.ts +176 -50
  294. package/src/server/management/companion-routes.ts +1 -1
  295. package/src/server/management/config-routes.ts +75 -4
  296. package/src/server/management/cursor-integration-routes.ts +1 -1
  297. package/src/server/management/model-routes.ts +11 -6
  298. package/src/server/management/model-rows.ts +14 -0
  299. package/src/server/management/native-integration-routes.ts +2 -7
  300. package/src/server/management/native-reasoning-retention-routes.ts +41 -0
  301. package/src/server/management/oauth-account-routes.ts +87 -14
  302. package/src/server/management/provider-gcp-credential-routes.ts +29 -0
  303. package/src/server/management/provider-key-pacing-routes.ts +31 -0
  304. package/src/server/management/provider-overwrite-carry.ts +7 -0
  305. package/src/server/management/provider-routes.ts +25 -30
  306. package/src/server/management/route-registry.ts +10 -1
  307. package/src/server/management-api.ts +3 -1
  308. package/src/server/messages-native-tool-guard.ts +185 -0
  309. package/src/server/messages-native.ts +117 -24
  310. package/src/server/messages-shared.ts +431 -0
  311. package/src/server/relay.ts +1 -1
  312. package/src/server/request-log-terminal-status.ts +1 -1
  313. package/src/server/request-log.ts +6 -1
  314. package/src/server/responses/account-change-state.ts +8 -2
  315. package/src/server/responses/adapter-continuation.ts +47 -19
  316. package/src/server/responses/adapter-delivery.ts +72 -12
  317. package/src/server/responses/adapter-dispatch.ts +46 -3
  318. package/src/server/responses/advisor-retention.ts +117 -0
  319. package/src/server/responses/advisor-slot.ts +73 -41
  320. package/src/server/responses/anthropic-thinking-recovery.ts +153 -0
  321. package/src/server/responses/codex-ws-wire.ts +15 -8
  322. package/src/server/responses/combo-stream-preflight.ts +3 -2
  323. package/src/server/responses/compact.ts +303 -125
  324. package/src/server/responses/compaction-recovery.ts +20 -10
  325. package/src/server/responses/compaction-retention.ts +61 -0
  326. package/src/server/responses/context-overflow.ts +11 -6
  327. package/src/server/responses/core-auth.ts +6 -0
  328. package/src/server/responses/core-codex-account.ts +11 -1
  329. package/src/server/responses/core-combo-failure.ts +2 -0
  330. package/src/server/responses/core-combo.ts +121 -36
  331. package/src/server/responses/core-errors.ts +3 -3
  332. package/src/server/responses/core-lifetime.ts +8 -4
  333. package/src/server/responses/core-opaque-recovery.ts +35 -0
  334. package/src/server/responses/core-options.ts +7 -0
  335. package/src/server/responses/core-pipeline.ts +141 -0
  336. package/src/server/responses/core-replay.ts +23 -3
  337. package/src/server/responses/core.ts +2 -115
  338. package/src/server/responses/fetch-helpers.ts +73 -19
  339. package/src/server/responses/main-hard-lock-routing.ts +109 -0
  340. package/src/server/responses/native-response-control.ts +4 -2
  341. package/src/server/responses/passthrough-delivery.ts +3 -1
  342. package/src/server/responses/passthrough-dispatch.ts +15 -13
  343. package/src/server/responses/passthrough-execution.ts +3 -2
  344. package/src/server/responses/request-prepare.ts +77 -12
  345. package/src/server/responses/request-sidecar-auth.ts +2 -2
  346. package/src/server/responses/request-transport.ts +104 -27
  347. package/src/server/responses/response-effects.ts +2 -0
  348. package/src/server/responses/response-tool-ownership.ts +21 -0
  349. package/src/server/responses/run-turn-execution.ts +96 -30
  350. package/src/server/responses/sidecar-execution.ts +63 -9
  351. package/src/server/responses/sidecar-send-budget.ts +2 -2
  352. package/src/server/responses/terminal-guard.ts +2 -16
  353. package/src/server/responses-undeclared-tool-guard.ts +1 -1
  354. package/src/server/ws-bridge.ts +2 -0
  355. package/src/service/auth-preflight.ts +71 -0
  356. package/src/service/cli.ts +4 -2
  357. package/src/service/definition.ts +97 -0
  358. package/src/service/guards.ts +1 -61
  359. package/src/service/health-sentinel-linux.ts +112 -0
  360. package/src/service/health-sentinel-main.ts +76 -57
  361. package/src/service/health.ts +5 -89
  362. package/src/service/launchd.ts +27 -7
  363. package/src/service/logging.ts +72 -9
  364. package/src/service/repair.ts +2 -1
  365. package/src/service/state.ts +7 -8
  366. package/src/service/systemd.ts +1 -1
  367. package/src/service/windows-guarded-manager.ts +2 -1
  368. package/src/service/windows-ops.ts +1 -1
  369. package/src/service/windows-scheduler.ts +2 -7
  370. package/src/service/windows-taskxml.ts +2 -2
  371. package/src/service/windows-wscript.ts +14 -0
  372. package/src/service.ts +4 -2
  373. package/src/sidecar/auth.ts +2 -0
  374. package/src/types/config.ts +64 -3
  375. package/src/types/opencode-go.ts +61 -0
  376. package/src/types/opencode-zen.ts +35 -0
  377. package/src/types/provider.ts +32 -6
  378. package/src/types/request.ts +31 -2
  379. package/src/types/wire.ts +31 -37
  380. package/src/types.ts +2 -0
  381. package/src/update/npm-invocation.mjs +12 -2
  382. package/src/usage/cost.ts +48 -12
  383. package/src/usage/expected-prices.ts +3 -0
  384. package/src/usage/ledger-retention.ts +2 -1
  385. package/src/usage/ledger-scanner.ts +6 -5
  386. package/src/usage/log.ts +17 -8
  387. package/src/usage/summary.ts +30 -11
  388. package/src/usage/telemetry-contract.ts +1 -0
  389. package/src/usage/user-cost-overlays.ts +14 -19
  390. package/src/usage/user-cost-validation.ts +38 -0
  391. package/src/vision/eligibility.ts +13 -0
  392. package/src/web-search/anthropic-history.ts +59 -0
  393. package/src/web-search/loop.ts +236 -73
  394. package/src/web-search/progress-stream.ts +15 -2
  395. package/src/web-search/run-turn-loop.ts +9 -1
  396. package/gui/dist/assets/App-BjlttVLy.js +0 -58
  397. package/gui/dist/assets/App-CSJvmjNs.css +0 -1
  398. package/gui/dist/assets/index-BB6uK6tU.css +0 -1
  399. package/gui/dist/assets/index-BMssP4k3.js +0 -94
  400. package/gui/dist/assets/quota-summary-D8M1bc_R.js +0 -1
@@ -0,0 +1,185 @@
1
+ import { anthropicErrorBody } from "../claude/outbound";
2
+ import { redactSecretString } from "../lib/redact";
3
+ import { sseFieldValue } from "../lib/sse-decoder";
4
+ import { TRANSLATOR_MAX_SSE_EVENT_BYTES, type TranslatorBudget } from "../lib/translator-budget";
5
+ import { UNDECLARED_TOOL_CALL_ERROR_CODE, undeclaredToolCallMessage } from "./responses-undeclared-tool-guard";
6
+ import { createSseBlockBuffer, sseDataPayload } from "./sse-payload-rewrite";
7
+
8
+ type Rec = Record<string, unknown>;
9
+ const isRec = (value: unknown): value is Rec => value !== null && typeof value === "object" && !Array.isArray(value);
10
+ const NO_DECLARED_TOOLS: ReadonlySet<string> = new Set();
11
+
12
+ /** Inline system-message definitions extend an otherwise empty native catalog. */
13
+ export function nativeMessagesInlineToolNames(body: Rec): ReadonlySet<string> {
14
+ const definitions = new Set<string>();
15
+ const active = new Set<string>();
16
+ if (!Array.isArray(body.messages)) return active;
17
+ for (const message of body.messages) {
18
+ if (!isRec(message) || message.role !== "system" || !Array.isArray(message.content)) continue;
19
+ for (const block of message.content) {
20
+ if (!isRec(block) || !isRec(block.tool)) continue;
21
+ const tool = block.tool;
22
+ if (block.type === "tool_addition" && tool.type === "tool_definition" && isRec(tool.definition)
23
+ && typeof tool.definition.name === "string" && tool.definition.name.length > 0) {
24
+ definitions.add(tool.definition.name);
25
+ active.add(tool.definition.name);
26
+ } else if (tool.type === "tool_reference" && typeof tool.name === "string") {
27
+ if (block.type === "tool_addition" && definitions.has(tool.name)) active.add(tool.name);
28
+ // Like native admission, only a permanent removal withdraws a declaration.
29
+ if (block.type === "tool_removal" && (message.clear_at === undefined || message.clear_at === "never")) active.delete(tool.name);
30
+ }
31
+ }
32
+ }
33
+ return active;
34
+ }
35
+
36
+ /** Inspect only native message content; opaque extension fields do not carry client calls. */
37
+ export function nativeMessagesClientToolName(value: unknown, declared: ReadonlySet<string> = NO_DECLARED_TOOLS): string | undefined {
38
+ if (!isRec(value)) return undefined;
39
+ const content = Array.isArray(value.content) ? value.content : [];
40
+ const block = content.find(item => isRec(item) && item.type === "tool_use"
41
+ && (typeof item.name !== "string" || !declared.has(item.name)));
42
+ if (isRec(block)) return typeof block.name === "string" ? block.name : "<unnamed>";
43
+ return undefined;
44
+ }
45
+
46
+ /** Used only when the current native request explicitly declares no client tools. */
47
+ export function guardNativeMessagesEmptyToolCatalog(
48
+ source: ReadableStream<Uint8Array>,
49
+ budget: TranslatorBudget,
50
+ onRefusal: (message: string) => void,
51
+ declared: ReadonlySet<string> = NO_DECLARED_TOOLS,
52
+ ): ReadableStream<Uint8Array> {
53
+ const reader = source.getReader();
54
+ const decoder = new TextDecoder();
55
+ const encoder = new TextEncoder();
56
+ const scope = { kind: "live_transient" as const };
57
+ const buffer = createSseBlockBuffer(budget, bytes => {
58
+ if (bytes > TRANSLATOR_MAX_SSE_EVENT_BYTES) throw new Error("upstream SSE event exceeded the safe limit");
59
+ });
60
+ // Hosted tools share input_json_delta with client tools, but cannot grant client authority.
61
+ const permittedInputBlocks = new Set<number>();
62
+ let queuedBytes = 0;
63
+ let settled = false;
64
+ const releaseQueued = () => {
65
+ budget.releaseRetained(queuedBytes, scope);
66
+ queuedBytes = 0;
67
+ };
68
+ const clear = () => {
69
+ buffer.clear();
70
+ budget.releaseRetained(permittedInputBlocks.size * 32, scope);
71
+ permittedInputBlocks.clear();
72
+ };
73
+ const enqueue = (controller: ReadableStreamDefaultController<Uint8Array>, text: string) => {
74
+ const bytes = Buffer.byteLength(text);
75
+ const reservation = budget.reserveTransient(bytes, scope);
76
+ try {
77
+ controller.enqueue(encoder.encode(text));
78
+ reservation.commitRetained();
79
+ queuedBytes += bytes;
80
+ } catch (error) {
81
+ reservation.release();
82
+ throw error;
83
+ }
84
+ };
85
+ const cancelReader = (reason?: unknown) => {
86
+ void reader.cancel(reason).catch(() => {});
87
+ };
88
+ const process = (controller: ReadableStreamDefaultController<Uint8Array>, block: string, delimiter: string) => {
89
+ const payload = sseDataPayload(block);
90
+ let data: unknown;
91
+ let terminal = false;
92
+ try { data = payload === null ? undefined : JSON.parse(payload); } catch {
93
+ // The buffered Messages collector concatenates data lines without separators. Inspect
94
+ // that representation too, so an invalid SSE split cannot become a valid folded tool call.
95
+ try { data = payload === null ? undefined : JSON.parse(payload.replace(/\n/g, "")); } catch { /* preserve opaque frames */ }
96
+ }
97
+ if (isRec(data)) {
98
+ const eventName = block.split(/\r\n|\r|\n/).map(line => sseFieldValue(line, "event"))
99
+ .filter(value => value !== null).at(-1)?.trim();
100
+ // Stream clients use the payload type; the buffered collector uses the event field.
101
+ const isEvent = (type: string) => data.type === type || eventName === type;
102
+ terminal = isEvent("message_stop") || isEvent("error");
103
+ let name = isEvent("message_start") ? nativeMessagesClientToolName(data.message, declared) : undefined;
104
+ if (isEvent("content_block_start") && isRec(data.content_block) && data.content_block.type === "tool_use"
105
+ && (typeof data.content_block.name !== "string" || !declared.has(data.content_block.name))) {
106
+ name ??= typeof data.content_block.name === "string" ? data.content_block.name : "<unnamed>";
107
+ }
108
+ const index = typeof data.index === "number" && Number.isSafeInteger(data.index) ? data.index : undefined;
109
+ if (isEvent("content_block_delta") && isRec(data.delta) && data.delta.type === "input_json_delta"
110
+ && (index === undefined || !permittedInputBlocks.has(index))) name ??= "<unnamed>";
111
+ if (name !== undefined) {
112
+ clear();
113
+ const message = redactSecretString(undeclaredToolCallMessage(name));
114
+ onRefusal(message);
115
+ enqueue(controller, `event: error\ndata: ${JSON.stringify(anthropicErrorBody(502, message, "api_error", UNDECLARED_TOOL_CALL_ERROR_CODE))}\n\n`);
116
+ settled = true;
117
+ cancelReader(new Error(message));
118
+ controller.close();
119
+ return;
120
+ }
121
+ if (isEvent("content_block_start") && index !== undefined && isRec(data.content_block)) {
122
+ if (permittedInputBlocks.delete(index)) budget.releaseRetained(32, scope);
123
+ if (data.content_block.type === "server_tool_use" || data.content_block.type === "mcp_tool_use"
124
+ || data.content_block.type === "tool_use" && typeof data.content_block.name === "string" && declared.has(data.content_block.name)) {
125
+ budget.chargeRetained(32, scope);
126
+ permittedInputBlocks.add(index);
127
+ }
128
+ } else if (isEvent("content_block_stop") && index !== undefined && permittedInputBlocks.delete(index)) {
129
+ budget.releaseRetained(32, scope);
130
+ }
131
+ }
132
+ enqueue(controller, block + delimiter);
133
+ if (terminal) {
134
+ settled = true;
135
+ clear();
136
+ cancelReader();
137
+ controller.close();
138
+ }
139
+ };
140
+ return new ReadableStream<Uint8Array>({
141
+ async pull(controller) {
142
+ releaseQueued();
143
+ if (settled) return;
144
+ try {
145
+ for (;;) {
146
+ const block = buffer.next();
147
+ if (block) {
148
+ buffer.compact();
149
+ process(controller, block.block, block.delimiter);
150
+ return;
151
+ }
152
+ const { done, value } = await reader.read();
153
+ if (settled) return;
154
+ if (!done) {
155
+ buffer.append(decoder.decode(value, { stream: true }));
156
+ continue;
157
+ }
158
+ buffer.append(decoder.decode());
159
+ const tail = buffer.tail();
160
+ if (tail) process(controller, tail, "");
161
+ if (!settled) {
162
+ settled = true;
163
+ clear();
164
+ controller.close();
165
+ }
166
+ return;
167
+ }
168
+ } catch (error) {
169
+ if (settled) return;
170
+ settled = true;
171
+ clear();
172
+ cancelReader(error);
173
+ controller.error(error);
174
+ } finally {
175
+ if (settled) releaseQueued();
176
+ }
177
+ },
178
+ cancel(reason) {
179
+ settled = true;
180
+ clear();
181
+ releaseQueued();
182
+ return reader.cancel(reason);
183
+ },
184
+ });
185
+ }
@@ -44,6 +44,7 @@ import { classifyError } from "../lib/errors";
44
44
  import { relaySseWithPayloadRewrite } from "./sse-payload-rewrite";
45
45
  import { redactSecretString } from "../lib/redact";
46
46
  import { resolveClientRetryAfter } from "../lib/retry-after";
47
+ import { enrichOpenCodeZenUpstreamMessage } from "../providers/opencode-zen-rate-limit";
47
48
  import { isTranslatorBudgetExceededError, type TranslatorBudget } from "../lib/translator-budget";
48
49
  import {
49
50
  applyReplayRefusalClientHeaders,
@@ -61,7 +62,9 @@ import {
61
62
  type UpstreamSendRecovery,
62
63
  UPSTREAM_RESET_REPLAY_REFUSED_CODE,
63
64
  } from "../lib/upstream-retry";
65
+ import { distributedKeyPhysicalFetch, distributedKeyResponseOwner, recordDistributedResponseFailure } from "../providers/api-key-response-ownership";
64
66
  import { providerApiKeySelectionIsCurrent, resolveCurrentProviderApiKeyTransport } from "../providers/api-key-selection";
67
+ import { RequestPacingQueueOverloadError } from "../providers/request-pacing";
65
68
  import {
66
69
  hasKeyPoolFailover,
67
70
  rateLimitRetryDelayMs,
@@ -91,12 +94,14 @@ import {
91
94
  resolvePassthroughBodyGuard,
92
95
  sanitizePassthroughToolCallIds,
93
96
  tapAnthropicSseForLog,
94
- } from "./claude-messages";
97
+ } from "./messages-shared";
95
98
  import { beginInferenceAttempt } from "./inference/attempt";
96
99
  import { createFinalRequestLog, type FinalRequestLogMeta } from "./inference/final-log";
97
100
  import { registerTurn, unregisterTurn } from "./lifecycle";
98
101
  import { nativeMessagesDeclineReason, type NativeMessagesSelector } from "./messages-native-eligibility";
99
102
  import { messagesSecondaryInstanceUnavailable } from "./messages-native-selector";
103
+ import { guardNativeMessagesEmptyToolCatalog, nativeMessagesClientToolName, nativeMessagesInlineToolNames } from "./messages-native-tool-guard";
104
+ import { UNDECLARED_TOOL_CALL_ERROR_CODE, undeclaredToolCallMessage } from "./responses-undeclared-tool-guard";
100
105
  import {
101
106
  nativeOAuthBindingIsCurrent,
102
107
  NativeOAuthSelectionChangedError,
@@ -315,6 +320,8 @@ export async function handleNativeMessages(options: HandleNativeMessagesOptions)
315
320
  const { req, config, logCtx, logIds, route, body, requestedModel, translatorBudget } = options;
316
321
  const nativeInstance = configuredAnthropicInstance(config, route.providerName);
317
322
  const requestedStream = body.stream === true;
323
+ const clientDeclaredEmptyToolCatalog = Array.isArray(body.tools) && body.tools.length === 0;
324
+ const declaredInlineTools = clientDeclaredEmptyToolCatalog ? nativeMessagesInlineToolNames(body) : undefined;
318
325
  logCtx.inboundProtocol = "messages";
319
326
  logCtx.model = route.modelId;
320
327
  logCtx.provider = route.providerName;
@@ -436,8 +443,16 @@ export async function handleNativeMessages(options: HandleNativeMessagesOptions)
436
443
  }
437
444
  } else {
438
445
  // Same pre-dispatch key preference every direct send path applies (see chat-native.ts).
439
- const proactiveKeyProvider = selectProactiveApiKeyTransport(config, route.providerName, route.provider);
440
- if (proactiveKeyProvider) route.provider = proactiveKeyProvider;
446
+ try {
447
+ const proactiveKeyProvider = selectProactiveApiKeyTransport(config, route.providerName, route.provider);
448
+ if (proactiveKeyProvider) route.provider = proactiveKeyProvider;
449
+ } catch (error) {
450
+ cleanupAbort(); upstream.abort();
451
+ if (!(error instanceof RequestPacingQueueOverloadError)) throw error;
452
+ const refusal = fail(429, error.message, "rate_limit_error");
453
+ refusal.headers.set("Retry-After", String(error.retryAfterSeconds));
454
+ return refusal;
455
+ }
441
456
  }
442
457
  let activeProvider: OcxProviderConfig = oauthBinding ? oauthProvider(oauthBinding) : route.provider;
443
458
  stampApiKeyAccountLabel(logCtx, route.providerName, activeProvider);
@@ -521,6 +536,15 @@ export async function handleNativeMessages(options: HandleNativeMessagesOptions)
521
536
  && nativeMessagesDeclineReason({ ...route, provider: routed }, body, config, selector) === undefined;
522
537
  };
523
538
 
539
+ // Keep the actual failed wire identity if a later recovery is refused before sending.
540
+ // A 429 remains eligible for explicit same-key retries until that ladder is finished.
541
+ let pendingDistributedFailure: (() => void) | undefined;
542
+ let distributedResponseOwnsKey = true;
543
+ const settleDistributedFailure = () => {
544
+ const record = pendingDistributedFailure;
545
+ pendingDistributedFailure = undefined;
546
+ record?.();
547
+ };
524
548
  const send = async (recovery?: "rate-limit-429" | "oauth-401" | "oauth-account-403" | "key-429" | "key-401"): Promise<Response> => {
525
549
  const remaining = remainingTransientSends();
526
550
  if (requestTransientPolicy && remaining <= 0) {
@@ -538,7 +562,7 @@ export async function handleNativeMessages(options: HandleNativeMessagesOptions)
538
562
  providerFetch(activeProvider, undefined, {
539
563
  providerName: route.providerName,
540
564
  modelId: route.modelId,
541
- dispatchOverride: async (_input, init, execute) => {
565
+ dispatchOverride: async (_input, init, execute, reacquirePacing) => {
542
566
  if (oauthBinding) {
543
567
  if (!nativeOAuthRouteIsCurrent(oauthBinding)) throw new NativeOAuthSelectionChangedError();
544
568
  // The OAuth twin of the key check below: re-resolve through the same selection
@@ -555,12 +579,25 @@ export async function handleNativeMessages(options: HandleNativeMessagesOptions)
555
579
  rebuildFor(oauthProvider(oauthBinding));
556
580
  logCtx.provider = formatAnthropicProviderForLog(route.providerName, oauthBinding.snapshot.accountId, config);
557
581
  }
558
- } else if (!providerApiKeySelectionIsCurrent(config, route.providerName, activeProvider)) {
559
- const current = resolveCurrentProviderApiKeyTransport(config, route.providerName, activeProvider);
560
- if (!current || nativeMessagesDeclineReason({ ...route, provider: current }, body, config, selector) !== undefined) {
561
- throw new Error("Provider key selection is no longer available for native Messages");
582
+ } else {
583
+ for (let attempt = 0; !providerApiKeySelectionIsCurrent(config, route.providerName, activeProvider); attempt++) {
584
+ if (attempt >= 3) throw new Error("Provider key selection changed repeatedly before native Messages dispatch");
585
+ let current = resolveCurrentProviderApiKeyTransport(config, route.providerName, activeProvider);
586
+ if (!current && activeProvider.requestPacing?.enabled
587
+ && (activeProvider.requestPacing.keys || activeProvider.requestPacing.keySelection === "round-robin")) {
588
+ const fresh = resolveCurrentProviderApiKeyTransport(config, route.providerName, { ...activeProvider, _apiKeyAttempt: undefined });
589
+ if (fresh) current = selectProactiveApiKeyTransport(config, route.providerName, fresh) ?? fresh;
590
+ }
591
+ if (!current || nativeMessagesDeclineReason({ ...route, provider: current }, body, config, selector) !== undefined) {
592
+ throw new Error("Provider key selection is no longer available for native Messages");
593
+ }
594
+ rebuildFor(current);
595
+ if (current.requestPacing?.enabled
596
+ && (current.requestPacing.keys || current.requestPacing.keySelection === "round-robin")) {
597
+ if (!reacquirePacing) throw new Error("Refreshed native Messages dispatch requires pacing readmission");
598
+ await reacquirePacing(current, route.modelId);
599
+ }
562
600
  }
563
- rebuildFor(current);
564
601
  }
565
602
  // The retry closure may hold a pre-reselection request: send the current one whole.
566
603
  const wire = activeRequest;
@@ -579,6 +616,7 @@ export async function handleNativeMessages(options: HandleNativeMessagesOptions)
579
616
  }
580
617
  const releaseFamily = snapshot && oauthBinding ? anthropicModelQuotaFor(oauthBinding.instance).claimAnthropicFamilyRevalidation(snapshot.accountId, route.modelId) : () => {};
581
618
  if (!releaseFamily) throw new AnthropicAccountCooldownError(1);
619
+ const selection = activeProvider._apiKeyAttempt;
582
620
  let dispatched: Response;
583
621
  const spendReport = createPhysicalSendReporter(physicalBudget, () => ({
584
622
  poolId: logCtx.spendPoolId ?? route.providerName, identityId: logCtx.accountLogLabel,
@@ -599,7 +637,9 @@ export async function handleNativeMessages(options: HandleNativeMessagesOptions)
599
637
  if (snapshot && !sendOwner) throw new NativeOAuthSelectionChangedError();
600
638
  physicalSendsUsed++;
601
639
  dispatched = await sendWithConnectionPolicy(
602
- anthropicBillingPhysicalFetch((activeProvider as OcxProviderTransport).fetch ?? execute, route.providerName, activeProvider),
640
+ distributedKeyPhysicalFetch(
641
+ anthropicBillingPhysicalFetch((activeProvider as OcxProviderTransport).fetch ?? execute, route.providerName, activeProvider),
642
+ route.providerName, activeProvider, wire.url, activeProvider.apiKeyTransport === "bearer" ? "authorization" : "x-api-key"),
603
643
  wire.url,
604
644
  applyUpstreamRecoveryInit({ ...init, method: "POST", headers, body: wire.body }, transportRecovery),
605
645
  { providerName: route.providerName, provider: activeProvider },
@@ -623,6 +663,14 @@ export async function handleNativeMessages(options: HandleNativeMessagesOptions)
623
663
  }
624
664
  } catch { /* Passive observation must not fail the response. */ }
625
665
  }
666
+ const ownsDistributedKey = !selection?.distributed
667
+ || distributedKeyResponseOwner(dispatched, config, route.providerName) !== null;
668
+ distributedResponseOwnsKey = ownsDistributedKey;
669
+ const failureStatus = dispatched.status;
670
+ pendingDistributedFailure = selection?.distributed && ownsDistributedKey && (failureStatus === 401 || failureStatus === 429)
671
+ && !isNonReplayableResponse(dispatched)
672
+ ? () => { recordDistributedResponseFailure(dispatched, config, route.providerName, req.signal); }
673
+ : undefined;
626
674
  if (!dispatched.ok) await recordKeyAttemptFailure(logCtx, dispatched, init.signal ?? upstream.signal);
627
675
  return dispatched;
628
676
  },
@@ -647,25 +695,28 @@ export async function handleNativeMessages(options: HandleNativeMessagesOptions)
647
695
  let response: Response;
648
696
  try {
649
697
  response = await send();
650
- const retryPolicy = oauthBinding ? undefined : rateLimitRetryPolicyFor(activeProvider);
698
+ let retryPolicy: ReturnType<typeof rateLimitRetryPolicyFor> | undefined;
651
699
  let retries = 0;
652
700
  let billingRecoveryActive = false;
653
701
  const keySendAvailable = () => transientSendAvailable()
654
702
  && (!billingRecoveryActive || Math.max(physicalSendsUsed, physicalBudget.used) < physicalBudget.policy.maxTotalModelSends);
655
- for (;;) {
703
+ const recoveryPasses = activeProvider._apiKeyAttempt?.distributed ? activeProvider.apiKeyPool?.length ?? 1 : 1;
704
+ for (let pass = 0; ; pass++) {
656
705
  const priorResponse = response;
657
706
  // A credential-scoped 401 on a static key pool says nothing about the sibling keys.
658
- while (response.status === 401 && hasKeyPoolFailover(activeProvider) && keySendAvailable()) {
707
+ while (response.status === 401 && distributedResponseOwnsKey && hasKeyPoolFailover(activeProvider) && keySendAvailable()) {
659
708
  const rotated = rotateProviderTransportOn401(config, route.providerName, activeProvider, {
660
709
  now: Date.now(),
661
710
  attemptedKey: activeProvider.apiKey,
711
+ attemptedSelection: activeProvider._apiKeyAttempt,
662
712
  });
713
+ pendingDistributedFailure = undefined; // Rotation already records this physical failure.
663
714
  if (!rotated) break;
664
715
  discard(response);
665
716
  rebuildFor(rotated);
666
717
  response = await send("key-401");
667
718
  }
668
- while ((response.status === 400 || response.status === 402) && hasKeyPoolFailover(activeProvider)) {
719
+ while ((response.status === 400 || response.status === 402) && distributedResponseOwnsKey && hasKeyPoolFailover(activeProvider)) {
669
720
  const rotated = await rotateAnthropicBillingKeyOnResponse(response, config, route.providerName, activeProvider, { signal: upstream.signal });
670
721
  if (!rotated) break;
671
722
  billingRecoveryActive = true;
@@ -674,9 +725,11 @@ export async function handleNativeMessages(options: HandleNativeMessagesOptions)
674
725
  rebuildFor(rotated);
675
726
  response = await send("key-401");
676
727
  }
728
+ if (pass === 0) retryPolicy = oauthBinding ? undefined : rateLimitRetryPolicyFor(activeProvider);
677
729
  // A refusal this proxy synthesized for a reset replay is not a provider rate limit.
678
730
  while (
679
731
  response.status === 429
732
+ && distributedResponseOwnsKey
680
733
  && !isNonReplayableResponse(response)
681
734
  && retryPolicy
682
735
  && retries < retryPolicy.attempts
@@ -691,13 +744,14 @@ export async function handleNativeMessages(options: HandleNativeMessagesOptions)
691
744
  if (upstream.signal.aborted) throw upstream.signal.reason;
692
745
  response = await send("rate-limit-429");
693
746
  }
694
- while (response.status === 429 && !isNonReplayableResponse(response) && hasKeyPoolFailover(activeProvider)) {
747
+ while (response.status === 429 && distributedResponseOwnsKey && !isNonReplayableResponse(response) && hasKeyPoolFailover(activeProvider)) {
695
748
  const rotated = rotateProviderTransportOn429(config, route.providerName, activeProvider, {
696
749
  retryAfter: response.headers.get("retry-after"),
697
750
  now: Date.now(),
698
751
  attemptedKey: activeProvider.apiKey,
699
752
  attemptedSelection: activeProvider._apiKeyAttempt,
700
753
  });
754
+ pendingDistributedFailure = undefined; // Rotation already records this physical failure.
701
755
  if (!rotated) break;
702
756
  // Rotation already recorded the cooldown; keep the terminal 429 when no send remains.
703
757
  if (!keySendAvailable()) break;
@@ -705,13 +759,17 @@ export async function handleNativeMessages(options: HandleNativeMessagesOptions)
705
759
  rebuildFor(rotated);
706
760
  response = await send("key-429");
707
761
  }
708
- // A sibling's next refusal may change class; the same counters survive that transition.
709
- if (response !== priorResponse && (
762
+ if (response === priorResponse || isNonReplayableResponse(response) || !keySendAvailable()) break;
763
+ // Billing class transitions share the physical-send ceiling; distributed-only
764
+ // transitions additionally retain their original bounded pool-pass allowance.
765
+ if (distributedResponseOwnsKey && (
710
766
  (billingRecoveryActive && [401, 429].includes(response.status))
711
767
  || (activeProvider.adapter === "anthropic" && hasKeyPoolFailover(activeProvider) && [400, 402].includes(response.status))
712
768
  )) continue;
713
- break;
769
+ if (!activeProvider._apiKeyAttempt?.distributed || !distributedResponseOwnsKey
770
+ || (response.status !== 401 && response.status !== 429) || pass + 1 >= recoveryPasses) break;
714
771
  }
772
+ settleDistributedFailure();
715
773
  const oauthRetryKey = {};
716
774
  const triedAccountIds = new Set<string>();
717
775
  let oauthFailovers = 0;
@@ -749,7 +807,13 @@ export async function handleNativeMessages(options: HandleNativeMessagesOptions)
749
807
  cleanupAbort();
750
808
  upstream.abort();
751
809
  if (req.signal.aborted) return fail(499, "Client cancelled request", "api_error");
810
+ settleDistributedFailure();
752
811
  const sendError = error instanceof UpstreamRetryEvidenceError ? error.cause : error;
812
+ if (sendError instanceof RequestPacingQueueOverloadError) {
813
+ const refusal = fail(429, sendError.message, "rate_limit_error");
814
+ refusal.headers.set("Retry-After", String(sendError.retryAfterSeconds));
815
+ return refusal;
816
+ }
753
817
  if (sendError instanceof SendBudgetExhaustedError) return fail(429, sendError.message, "send_budget_exhausted", "send_budget_exhausted");
754
818
  if (sendError instanceof NativeMessagesSpendRefusal) {
755
819
  const refusal = workflowRefusalResponse("workflow-spend-exhausted", logCtx);
@@ -782,7 +846,7 @@ export async function handleNativeMessages(options: HandleNativeMessagesOptions)
782
846
  upstream.abort();
783
847
  return fail(499, "Client cancelled request", "api_error");
784
848
  }
785
- return retainUpstreamMessagesRequestId(nativeMessagesErrorResponse(response, bodyText, finishLog), response.headers);
849
+ return retainUpstreamMessagesRequestId(nativeMessagesErrorResponse(response, bodyText, finishLog, route.providerName, activeProvider), response.headers);
786
850
  }
787
851
 
788
852
  const contentType = response.headers.get("content-type")?.toLowerCase() ?? "";
@@ -794,15 +858,22 @@ export async function handleNativeMessages(options: HandleNativeMessagesOptions)
794
858
  const renamed = activeRequest.oauthToolNames
795
859
  ? restoreOAuthToolNamesInSse(observed, activeRequest.oauthToolNames, translatorBudget)
796
860
  : observed;
797
- const source = echoRequestedModel(renamed, requestedModel);
861
+ const echoed = echoRequestedModel(renamed, requestedModel);
862
+ let toolRefusal: string | undefined;
863
+ const source = clientDeclaredEmptyToolCatalog
864
+ ? guardNativeMessagesEmptyToolCatalog(echoed, translatorBudget, message => {
865
+ toolRefusal = message;
866
+ logCtx.errorCode = UNDECLARED_TOOL_CALL_ERROR_CODE;
867
+ }, declaredInlineTools)
868
+ : echoed;
798
869
  if (requestedStream) {
799
870
  transferTurnToStream();
800
871
  const relayed = tapAnthropicSseForLog(source, logCtx, (status, meta) => {
801
872
  try {
802
873
  cleanupAbort();
803
874
  bindUsage(logCtx.usage);
804
- finishLog(status, undefined, meta);
805
- if (meta.closeReason !== "terminal") upstream.abort();
875
+ finishLog(toolRefusal ? 502 : status, toolRefusal, toolRefusal ? { ...meta, terminalStatus: "failed" } : meta);
876
+ if (toolRefusal || meta.closeReason !== "terminal") upstream.abort();
806
877
  } finally {
807
878
  releaseStreamTurn();
808
879
  }
@@ -833,6 +904,11 @@ export async function handleNativeMessages(options: HandleNativeMessagesOptions)
833
904
  if (message.type === "error") {
834
905
  const error = isRec(message.error) ? message.error : {};
835
906
  const text = typeof error.message === "string" ? error.message : "upstream stream failed";
907
+ if (toolRefusal) {
908
+ upstream.abort();
909
+ finishLog(502, toolRefusal, { closeReason: "terminal", terminalStatus: "failed" });
910
+ return fail(502, toolRefusal, "api_error", UNDECLARED_TOOL_CALL_ERROR_CODE);
911
+ }
836
912
  // The tap's own verdict (a reset, a stall, the byte cap) closes this row with the same meta
837
913
  // as the streaming lane's row; a plain upstream error event keeps the non_stream row.
838
914
  const tapMeta = tapState.meta;
@@ -884,6 +960,11 @@ export async function handleNativeMessages(options: HandleNativeMessagesOptions)
884
960
  const message = activeRequest.oauthToolNames
885
961
  ? restoreOAuthToolNamesInMessage(parsedMessage, activeRequest.oauthToolNames)
886
962
  : parsedMessage;
963
+ const undeclaredTool = clientDeclaredEmptyToolCatalog ? nativeMessagesClientToolName(message, declaredInlineTools) : undefined;
964
+ if (undeclaredTool !== undefined) {
965
+ logCtx.errorCode = UNDECLARED_TOOL_CALL_ERROR_CODE;
966
+ return fail(502, undeclaredToolCallMessage(undeclaredTool), "api_error", UNDECLARED_TOOL_CALL_ERROR_CODE);
967
+ }
887
968
  bindUsage(anthropicUsageToOcx(isRec(message.usage) ? message.usage : undefined));
888
969
  if (logIds) recordFirstOutput(logCtx, logIds.start);
889
970
  try {
@@ -919,7 +1000,7 @@ function isNativeOverflowError(error: Rec, message: string): boolean {
919
1000
  * Messages path, so a client sees the same contract on either lane: transient 5xx become 529
920
1001
  * with a retry hint, and a proxy-side replay refusal keeps its no-retry marking.
921
1002
  */
922
- function nativeMessagesErrorResponse(response: Response, bodyText: string, finishLog: FinishLog): Response {
1003
+ function nativeMessagesErrorResponse(response: Response, bodyText: string, finishLog: FinishLog, providerName: string, provider: OcxProviderConfig): Response {
923
1004
  let upstreamType: string | undefined;
924
1005
  let upstreamCode: string | undefined;
925
1006
  let upstreamMessage: string | undefined;
@@ -933,8 +1014,20 @@ function nativeMessagesErrorResponse(response: Response, bodyText: string, finis
933
1014
  }
934
1015
  } catch { /* keep generic classification */ }
935
1016
  const detail = formatAnthropicErrorBody(response.status, response.headers, bodyText);
936
- const message = upstreamMessage
1017
+ const rawMessage = upstreamMessage
937
1018
  ?? (detail ? `Provider error ${response.status}: ${detail}` : `Provider error ${response.status}`);
1019
+ const message = enrichOpenCodeZenUpstreamMessage(rawMessage, {
1020
+ status: response.status,
1021
+ providerName,
1022
+ baseUrl: provider.baseUrl,
1023
+ adapter: provider.adapter,
1024
+ authMode: provider.authMode,
1025
+ hasApiKey: Boolean(provider.apiKey?.trim()),
1026
+ upstreamRetryAfter: response.headers.get("retry-after"),
1027
+ upstreamErrorType: upstreamType,
1028
+ upstreamErrorCode: upstreamCode,
1029
+ supportsHttpSameKeyRetry: true,
1030
+ });
938
1031
  const classified = classifyError(
939
1032
  response.status,
940
1033
  upstreamType ?? (response.status === 401 ? "authentication_error"