@bitkyc08/opencodex 2.79.0 → 2.81.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 (245) hide show
  1. package/README.md +3 -3
  2. package/gui/dist/assets/App-7yhoys8a.js +54 -0
  3. package/gui/dist/assets/Tray-EwAOsP0S.js +1 -0
  4. package/gui/dist/assets/index-RsoaAxXY.js +94 -0
  5. package/gui/dist/assets/quota-summary-D2lN8tDm.js +1 -0
  6. package/gui/dist/index.html +1 -1
  7. package/gui/dist/provider-icons/claude-green.svg +1 -0
  8. package/package.json +3 -2
  9. package/src/adapters/anthropic-model-contract.ts +8 -2
  10. package/src/adapters/anthropic.ts +7 -2
  11. package/src/adapters/codebuddy/mcp-server.ts +2 -25
  12. package/src/adapters/codebuddy/tool-bridge.ts +1 -7
  13. package/src/adapters/coding-agent/mcp-server.ts +33 -0
  14. package/src/adapters/coding-agent/protocol.ts +122 -17
  15. package/src/adapters/coding-agent/tool-catalog.ts +75 -0
  16. package/src/adapters/coding-agent/turn.ts +101 -44
  17. package/src/adapters/cursor/catalog.ts +8 -0
  18. package/src/adapters/cursor/effort-map.ts +2 -0
  19. package/src/adapters/cursor/live-transport.ts +5 -3
  20. package/src/adapters/cursor/native-exec-fs.ts +12 -9
  21. package/src/adapters/cursor/native-exec.ts +13 -5
  22. package/src/adapters/cursor/protobuf-request.ts +13 -7
  23. package/src/adapters/cursor/request-builder.ts +23 -10
  24. package/src/adapters/cursor/tool-definitions.ts +9 -6
  25. package/src/adapters/cursor/tool-guidance.ts +29 -8
  26. package/src/adapters/cursor/tool-naming.ts +40 -7
  27. package/src/adapters/cursor/tool-wording.ts +46 -0
  28. package/src/adapters/cursor.ts +3 -0
  29. package/src/adapters/devin/live-models.ts +2 -0
  30. package/src/adapters/devin.ts +25 -11
  31. package/src/adapters/kiro-retry.ts +9 -4
  32. package/src/adapters/openai-chat/messages.ts +7 -4
  33. package/src/adapters/physical-send.ts +8 -5
  34. package/src/adapters/qoder/adapter.ts +41 -6
  35. package/src/adapters/qoder/scaffold-guard.ts +2 -3
  36. package/src/adapters/zed.ts +16 -1
  37. package/src/claude/inbound-cache-stabilize.ts +4 -9
  38. package/src/claude/inbound.ts +10 -0
  39. package/src/claude/intercept/model-bindings.ts +1 -0
  40. package/src/claude/intercept/picker-listener.ts +71 -4
  41. package/src/cli/account-anthropic-threshold.ts +14 -5
  42. package/src/cli/account-api.ts +3 -1
  43. package/src/cli/account-extended.ts +13 -9
  44. package/src/cli/account-policy-dto.ts +1 -1
  45. package/src/cli/account-policy.ts +13 -4
  46. package/src/cli/account.ts +3 -3
  47. package/src/cli/capabilities-accounts.ts +2 -2
  48. package/src/cli/capabilities-agents-routing.ts +24 -0
  49. package/src/cli/capabilities-integrations.ts +16 -0
  50. package/src/cli/codex-shim-autorestore.ts +3 -0
  51. package/src/cli/dispatch.ts +10 -1
  52. package/src/cli/help.ts +4 -1
  53. package/src/cli/index.ts +11 -5
  54. package/src/cli/integration-plan-dto.ts +1 -0
  55. package/src/cli/integration-preview.ts +8 -1
  56. package/src/cli/integrations.ts +32 -3
  57. package/src/cli/message-args.ts +34 -0
  58. package/src/cli/message-command.ts +43 -0
  59. package/src/cli/message-runtime.ts +6 -0
  60. package/src/cli/observe.ts +3 -1
  61. package/src/cli/provider.ts +29 -6
  62. package/src/cli/registry.ts +24 -2
  63. package/src/clients/config-export/commandcode.ts +131 -0
  64. package/src/clients/config-export/contracts.ts +12 -0
  65. package/src/clients/config-export.ts +64 -1
  66. package/src/codex/auth-api/account-list.ts +11 -3
  67. package/src/codex/auth-api/main-account-probe.ts +25 -8
  68. package/src/codex/auth-api/pool-mode-gate.ts +33 -9
  69. package/src/codex/auth-api/pool-quota-probe.ts +15 -1
  70. package/src/codex/auth-api/routes.ts +1 -0
  71. package/src/codex/autostart-health.ts +3 -3
  72. package/src/codex/catalog/auto-review.ts +13 -6
  73. package/src/codex/catalog/build-entries.ts +15 -7
  74. package/src/codex/catalog/control-plane.ts +45 -0
  75. package/src/codex/catalog/effort.ts +11 -1
  76. package/src/codex/catalog/gather-capture.ts +5 -3
  77. package/src/codex/catalog/parsing.ts +2 -1
  78. package/src/codex/catalog/provider-models.ts +15 -9
  79. package/src/codex/control-plane-models.ts +6 -0
  80. package/src/codex/journal.ts +36 -5
  81. package/src/codex/shim-overlay.ts +9 -1
  82. package/src/combos/jev-decision-contract.ts +25 -2
  83. package/src/combos/jev.ts +6 -3
  84. package/src/combos/types.ts +1 -1
  85. package/src/config/diagnostics.ts +27 -16
  86. package/src/config/load-degrade.ts +34 -0
  87. package/src/config/schema/anthropic-account-pool.ts +143 -0
  88. package/src/config/schema/config-schema.ts +7 -1
  89. package/src/config/schema/leaf-validators.ts +4 -0
  90. package/src/config.ts +2 -0
  91. package/src/generated/compatibility-version.json +347 -199
  92. package/src/generated/model-metadata.ts +5 -4
  93. package/src/images/loop.ts +2 -1
  94. package/src/integrations/catalog-refresh.ts +1 -1
  95. package/src/integrations/mutation-plan.ts +7 -0
  96. package/src/integrations/registry.ts +8 -0
  97. package/src/integrations/state.ts +9 -2
  98. package/src/integrations/writer.ts +8 -2
  99. package/src/lib/lab-live-route-production.ts +9 -1
  100. package/src/lib/request-execution-budget.ts +236 -58
  101. package/src/lib/spend-pool-alias-validation.ts +79 -0
  102. package/src/lib/spend-pool-continuity.ts +34 -0
  103. package/src/lib/spend-reservation-ledger.ts +412 -50
  104. package/src/lib/state-store-registrations.ts +20 -3
  105. package/src/lib/upstream-retry.ts +17 -5
  106. package/src/lib/workflow-budget.ts +53 -11
  107. package/src/messaging/budget.ts +45 -0
  108. package/src/messaging/discovery.ts +57 -0
  109. package/src/messaging/envelope.ts +49 -0
  110. package/src/messaging/input.ts +37 -0
  111. package/src/messaging/rpc.ts +196 -0
  112. package/src/messaging/send.ts +58 -0
  113. package/src/messaging/socket.ts +72 -0
  114. package/src/messaging/types.ts +29 -0
  115. package/src/oauth/anthropic-account-refusal.ts +51 -17
  116. package/src/oauth/anthropic-account-threshold.ts +8 -2
  117. package/src/oauth/anthropic-continuity.ts +15 -1
  118. package/src/oauth/anthropic-model-quota.ts +120 -48
  119. package/src/oauth/anthropic-model-routes.ts +11 -3
  120. package/src/oauth/anthropic-oauth-definitions.ts +98 -0
  121. package/src/oauth/anthropic-pool-config.ts +30 -0
  122. package/src/oauth/anthropic-rate-limit-policy.ts +64 -9
  123. package/src/oauth/anthropic-routing.ts +1202 -866
  124. package/src/oauth/anthropic-send-ownership.ts +41 -0
  125. package/src/oauth/anthropic.ts +4 -1
  126. package/src/oauth/generic-account-failover.ts +3 -1
  127. package/src/oauth/health.ts +8 -6
  128. package/src/oauth/index.ts +90 -173
  129. package/src/oauth/kiro.ts +22 -9
  130. package/src/oauth/login-cli.ts +2 -0
  131. package/src/oauth/model-discovery-auth.ts +43 -0
  132. package/src/oauth/pool-kernel.ts +11 -5
  133. package/src/oauth/pool-settings-capability.ts +5 -3
  134. package/src/oauth/store-anthropic-instance.ts +87 -0
  135. package/src/oauth/store.ts +67 -9
  136. package/src/oauth/token-guardian.ts +2 -0
  137. package/src/oauth/types.ts +2 -0
  138. package/src/protocols/plan-snapshot.ts +5 -0
  139. package/src/protocols/settings.ts +20 -5
  140. package/src/providers/anthropic-instance-id.ts +47 -0
  141. package/src/providers/anthropic-instance.ts +48 -0
  142. package/src/providers/anthropic-reset-grant-ledger.ts +11 -1
  143. package/src/providers/derive.ts +2 -0
  144. package/src/providers/kiro-models.ts +3 -0
  145. package/src/providers/label.ts +1 -1
  146. package/src/providers/model-rename-fields.ts +2 -0
  147. package/src/providers/quota/account-cache.ts +37 -11
  148. package/src/providers/quota/anthropic-account-quota.ts +93 -0
  149. package/src/providers/quota/anthropic-cooldown-recovery.ts +222 -111
  150. package/src/providers/quota/report-cache.ts +24 -1
  151. package/src/providers/quota/vendor-probes-oauth.ts +46 -14
  152. package/src/providers/quota.ts +22 -32
  153. package/src/providers/registry/entries-core.ts +35 -30
  154. package/src/providers/registry/model-ids.ts +1 -0
  155. package/src/providers/registry/model-seeds.ts +7 -4
  156. package/src/providers/registry/types.ts +3 -0
  157. package/src/providers/registry.ts +3 -3
  158. package/src/responses/spill-store.ts +5 -1
  159. package/src/responses/state/metrics.ts +87 -0
  160. package/src/responses/state/snapshot-codec.ts +6 -0
  161. package/src/responses/state/spill-queue.ts +1 -0
  162. package/src/responses/state/unforced-store-false.ts +55 -0
  163. package/src/responses/state.ts +50 -66
  164. package/src/router.ts +28 -4
  165. package/src/routing/compatibility/assemble.ts +1 -1
  166. package/src/routing/quota.ts +7 -3
  167. package/src/server/auth-cors.ts +5 -0
  168. package/src/server/chat-completions.ts +6 -1
  169. package/src/server/chat-native.ts +45 -25
  170. package/src/server/claude-messages.ts +16 -2
  171. package/src/server/gui-static.ts +38 -4
  172. package/src/server/index/spend-ledger-lifecycle.ts +10 -4
  173. package/src/server/index.ts +16 -16
  174. package/src/server/inference/context.ts +1 -1
  175. package/src/server/lifecycle.ts +1 -1
  176. package/src/server/management/agent-settings-routes.ts +30 -6
  177. package/src/server/management/anthropic-account-threshold.ts +12 -9
  178. package/src/server/management/anthropic-pool-settings.ts +72 -0
  179. package/src/server/management/anthropic-reset-grant-routes.ts +98 -39
  180. package/src/server/management/config-routes.ts +27 -7
  181. package/src/server/management/decision-routes.ts +2 -1
  182. package/src/server/management/integration-routes.ts +25 -2
  183. package/src/server/management/oauth-account-routes.ts +79 -98
  184. package/src/server/management/provider-instance-ownership.ts +44 -0
  185. package/src/server/management/provider-routes.ts +21 -28
  186. package/src/server/management/route-registry.ts +3 -1
  187. package/src/server/management/routing-profile-routes.ts +4 -4
  188. package/src/server/management/system-restart.ts +44 -14
  189. package/src/server/management/system-routes.ts +24 -2
  190. package/src/server/management/vision-sidecar-options.ts +9 -6
  191. package/src/server/management/web-search-sidecar-options.ts +51 -2
  192. package/src/server/management-auth.ts +6 -0
  193. package/src/server/messages-native-eligibility.ts +3 -2
  194. package/src/server/messages-native-oauth.ts +86 -41
  195. package/src/server/messages-native-scope.ts +4 -1
  196. package/src/server/messages-native-selector.ts +39 -0
  197. package/src/server/messages-native.ts +84 -25
  198. package/src/server/request-log.ts +5 -0
  199. package/src/server/responses/adapter-continuation.ts +29 -17
  200. package/src/server/responses/adapter-dispatch.ts +47 -17
  201. package/src/server/responses/antigravity-validation-refusal.ts +20 -6
  202. package/src/server/responses/combo-requested-effort.ts +14 -0
  203. package/src/server/responses/compact.ts +25 -8
  204. package/src/server/responses/compaction-recovery.ts +1 -1
  205. package/src/server/responses/core-combo-native.ts +4 -1
  206. package/src/server/responses/core-combo.ts +21 -18
  207. package/src/server/responses/core-normalize.ts +1 -0
  208. package/src/server/responses/core-options.ts +3 -1
  209. package/src/server/responses/core-replay.ts +5 -0
  210. package/src/server/responses/passthrough-delivery.ts +7 -3
  211. package/src/server/responses/passthrough-dispatch.ts +21 -8
  212. package/src/server/responses/request-prepare.ts +37 -5
  213. package/src/server/responses/request-send-budget.ts +74 -37
  214. package/src/server/responses/request-spend.ts +325 -33
  215. package/src/server/responses/request-transport.ts +132 -52
  216. package/src/server/responses/run-turn-execution.ts +11 -3
  217. package/src/server/responses/sidecar-execution.ts +96 -14
  218. package/src/server/responses-undeclared-tool-guard.ts +5 -1
  219. package/src/server/workflow-refusal.ts +30 -0
  220. package/src/service/desktop-startup.ts +100 -3
  221. package/src/service/ownership-mutation-lease.d.mts +1 -1
  222. package/src/service/ownership-mutation-lease.mjs +5 -7
  223. package/src/sidecar/anthropic-binding.ts +79 -0
  224. package/src/sidecar/auth.ts +76 -2
  225. package/src/types/anthropic-account-pool.ts +33 -0
  226. package/src/types/config.ts +12 -28
  227. package/src/types/provider.ts +5 -0
  228. package/src/types/tools.ts +7 -0
  229. package/src/types.ts +4 -0
  230. package/src/usage/cost.ts +3 -2
  231. package/src/usage/expected-prices.ts +66 -14
  232. package/src/vision/anthropic-describe.ts +16 -3
  233. package/src/vision/index.ts +3 -0
  234. package/src/vision/plan.ts +19 -6
  235. package/src/web-search/alpha-search.ts +7 -1
  236. package/src/web-search/anthropic-executor.ts +16 -4
  237. package/src/web-search/header-deadline.ts +67 -0
  238. package/src/web-search/index.ts +7 -3
  239. package/src/web-search/loop.ts +32 -10
  240. package/src/web-search/passthrough-bridge.ts +4 -1
  241. package/src/web-search/sidecar-providers.ts +4 -3
  242. package/gui/dist/assets/App-BOUa-ek3.js +0 -54
  243. package/gui/dist/assets/Tray-DUtzB4zF.js +0 -1
  244. package/gui/dist/assets/index-DwHNDF4s.js +0 -94
  245. package/gui/dist/assets/quota-summary-BuL4WIBo.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,16 +19,18 @@
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,
@@ -43,6 +47,7 @@ import { isTranslatorBudgetExceededError, type TranslatorBudget } from "../lib/t
43
47
  import {
44
48
  applyReplayRefusalClientHeaders,
45
49
  applyUpstreamRecoveryInit,
50
+ SendBudgetExhaustedError,
46
51
  fetchWithResetRetry,
47
52
  fetchWithTransientRetry,
48
53
  isNonReplayableResponse,
@@ -67,12 +72,13 @@ import {
67
72
  } from "../providers/key-failover";
68
73
  import { stampApiKeyAccountLabel } from "../providers/label";
69
74
  import { OAuthAccountPausedError, OAuthLoginRequiredError, publicOAuthAuthenticationErrorMessage } from "../oauth";
70
- import { AnthropicAccountCooldownError, formatAnthropicProviderForLog, hasAnthropicFailoverQuorum } from "../oauth/anthropic-routing";
75
+ import { AnthropicAccountCooldownError, formatAnthropicProviderForLog, anthropicRoutingFor } from "../oauth/anthropic-routing";
76
+ import { configuredAnthropicInstance } from "../providers/anthropic-instance";
71
77
  import { resolveProtocolSettings } from "../protocols/settings";
72
78
  import { addProtocolEntryReason, markProtocolBlocked } from "../protocols/trace";
73
79
  import type { OcxProviderTransport } from "../providers/xai-transport";
74
80
  import { preservesPhysicalComboProvider, resolveComboId } from "../combos";
75
- import { captureRouteStaticPolicy, routeModel, type RouteResult } from "../router";
81
+ import { captureRouteStaticPolicy, routedProviderConfig, routeModel, type RouteResult } from "../router";
76
82
  import { POLICY_NAMESPACE, resolvePolicyProfileId } from "../routing/profile";
77
83
  import type { AnthropicClientIdentity } from "../adapters/anthropic/client-identity";
78
84
  import type { OcxConfig, OcxProviderConfig, OcxUsage } from "../types";
@@ -88,11 +94,12 @@ import { beginInferenceAttempt } from "./inference/attempt";
88
94
  import { createFinalRequestLog, type FinalRequestLogMeta } from "./inference/final-log";
89
95
  import { registerTurn, unregisterTurn } from "./lifecycle";
90
96
  import { nativeMessagesDeclineReason, type NativeMessagesSelector } from "./messages-native-eligibility";
97
+ import { messagesSecondaryInstanceUnavailable } from "./messages-native-selector";
91
98
  import {
92
99
  nativeOAuthBindingIsCurrent,
93
100
  NativeOAuthSelectionChangedError,
94
101
  NativeOAuthModelRouteError,
95
- resolveNativeOAuthBinding,
102
+ resolveNativeOAuthBindingForInstance,
96
103
  restoreOAuthToolNamesInMessage,
97
104
  restoreOAuthToolNamesInSse,
98
105
  type NativeOAuthBinding,
@@ -300,10 +307,12 @@ async function prepareNativeBody(body: Rec, signal: AbortSignal): Promise<void>
300
307
  */
301
308
  export async function handleNativeMessages(options: HandleNativeMessagesOptions): Promise<Response> {
302
309
  const { req, config, logCtx, logIds, route, body, requestedModel, translatorBudget } = options;
310
+ const nativeInstance = configuredAnthropicInstance(config, route.providerName);
303
311
  const requestedStream = body.stream === true;
304
312
  logCtx.inboundProtocol = "messages";
305
313
  logCtx.model = route.modelId;
306
314
  logCtx.provider = route.providerName;
315
+ logCtx.spendPoolId = route.providerName;
307
316
  logCtx.providerAdapter = route.provider.adapter;
308
317
  logCtx.requestedModel = requestedModel;
309
318
  if (route.routeReason === "model-alias" || route.modelId !== requestedModel) logCtx.requestedAlias = requestedModel;
@@ -353,11 +362,18 @@ export async function handleNativeMessages(options: HandleNativeMessagesOptions)
353
362
  return null;
354
363
  };
355
364
 
365
+ if (route.provider.authMode === "oauth" && !nativeInstance) {
366
+ return fail(401, "Configured Anthropic OAuth instance is unavailable", "authentication_error");
367
+ }
368
+
356
369
  const toolScopeDenial = nativeMessagesToolScopeDenial(options.modelScope, route.providerName, requestedModel, body);
357
370
  if (toolScopeDenial) {
358
371
  finishLog(403, toolScopeDenial.message);
359
372
  return admissionModelDeniedResponse(toolScopeDenial);
360
373
  }
374
+ let routeTarget: string;
375
+ try { routeTarget = resolveAnthropicMessagesUrl(route.provider); }
376
+ catch { return fail(400, "Invalid Anthropic Messages destination", "invalid_request_error"); }
361
377
 
362
378
  try {
363
379
  await prepareNativeBody(body, req.signal);
@@ -391,7 +407,7 @@ export async function handleNativeMessages(options: HandleNativeMessagesOptions)
391
407
  const oauthProvider = (binding: NativeOAuthBinding): OcxProviderConfig => ({ ...route.provider, apiKey: binding.snapshot.accessToken });
392
408
  if (route.provider.authMode === "oauth") {
393
409
  try {
394
- oauthBinding = await resolveNativeOAuthBinding(config, { sessionKey: options.sessionKey, model: route.modelId });
410
+ oauthBinding = await resolveNativeOAuthBindingForInstance(nativeInstance!, config, { routeTarget, sessionKey: options.sessionKey, model: route.modelId });
395
411
  } catch (error) {
396
412
  cleanupAbort();
397
413
  upstream.abort();
@@ -411,6 +427,7 @@ export async function handleNativeMessages(options: HandleNativeMessagesOptions)
411
427
  stampApiKeyAccountLabel(logCtx, route.providerName, activeProvider);
412
428
  if (oauthBinding) logCtx.provider = formatAnthropicProviderForLog(route.providerName, oauthBinding.snapshot.accountId, config);
413
429
  const spendTracker = attachRequestSpendTracker(req, logCtx);
430
+ const physicalBudget = createInferenceSendBudget(req, logCtx);
414
431
  let activeRequest: AnthropicMessagesPassthroughRequest;
415
432
  let retainedRequestBytes = 0;
416
433
  const releaseRetainedRequest = () => {
@@ -473,6 +490,15 @@ export async function handleNativeMessages(options: HandleNativeMessagesOptions)
473
490
  : Number.POSITIVE_INFINITY;
474
491
  const transientSendAvailable = (): boolean => remainingTransientSends() > 0;
475
492
  const selector: NativeMessagesSelector = options.selector ?? {};
493
+ const nativeOAuthRouteIsCurrent = (binding: NativeOAuthBinding): boolean => {
494
+ const provider = config.providers[binding.instance];
495
+ if (configuredAnthropicInstance(config, binding.instance) !== binding.instance || !provider) return false;
496
+ // Same normalization as the bound target (nativeOAuthRouteTarget): compare routed destinations.
497
+ let routed: OcxProviderConfig;
498
+ try { routed = routedProviderConfig(binding.instance, provider); } catch { return false; }
499
+ return resolveAnthropicMessagesUrl(routed) === routeTarget
500
+ && nativeMessagesDeclineReason({ ...route, provider: routed }, body, config, selector) === undefined;
501
+ };
476
502
 
477
503
  const send = async (recovery?: "rate-limit-429" | "oauth-account-403" | "key-429" | "key-401"): Promise<Response> => {
478
504
  const remaining = remainingTransientSends();
@@ -493,12 +519,13 @@ export async function handleNativeMessages(options: HandleNativeMessagesOptions)
493
519
  modelId: route.modelId,
494
520
  dispatchOverride: async (_input, init, execute) => {
495
521
  if (oauthBinding) {
522
+ if (!nativeOAuthRouteIsCurrent(oauthBinding)) throw new NativeOAuthSelectionChangedError();
496
523
  // The OAuth twin of the key check below: re-resolve through the same selection
497
524
  // owner when the committed account or its credential moved since the build.
498
525
  for (let attempt = 0; !nativeOAuthBindingIsCurrent(oauthBinding); attempt++) {
499
526
  if (attempt >= 3) throw new NativeOAuthSelectionChangedError();
500
527
  try {
501
- oauthBinding = await resolveNativeOAuthBinding(config, { sessionKey: options.sessionKey, model: route.modelId });
528
+ oauthBinding = await resolveNativeOAuthBindingForInstance(nativeInstance!, config, { routeTarget, sessionKey: options.sessionKey, model: route.modelId });
502
529
  } catch (error) {
503
530
  if (error instanceof OAuthAccountPausedError || error instanceof OAuthLoginRequiredError
504
531
  || error instanceof AnthropicAccountCooldownError || error instanceof NativeOAuthModelRouteError) throw error;
@@ -519,31 +546,58 @@ export async function handleNativeMessages(options: HandleNativeMessagesOptions)
519
546
  const headers = new Headers(wire.headers);
520
547
  const encoding = new Headers(init.headers).get("accept-encoding");
521
548
  if (!headers.has("accept-encoding") && encoding) headers.set("accept-encoding", encoding);
522
- const snapshot = oauthBinding?.snapshot;
523
- const providerAccountUuid = oauthBinding?.providerAccountUuid;
549
+ const sendingBinding = oauthBinding;
550
+ const snapshot = sendingBinding?.snapshot;
551
+ const providerAccountUuid = sendingBinding?.providerAccountUuid;
524
552
  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) : () => {};
553
+ const ownsBearer = snapshot && snapshot.provider === nativeInstance
554
+ && headers.get("authorization") === `Bearer ${snapshot.accessToken}` && !headers.has("x-api-key");
555
+ if (oauthBinding && (!ownsBearer || wire.url !== oauthBinding.routeTarget
556
+ || !nativeOAuthRouteIsCurrent(oauthBinding) || !nativeOAuthBindingIsCurrent(oauthBinding))) {
557
+ throw new NativeOAuthSelectionChangedError();
558
+ }
559
+ const releaseFamily = snapshot && oauthBinding ? anthropicModelQuotaFor(oauthBinding.instance).claimAnthropicFamilyRevalidation(snapshot.accountId, route.modelId) : () => {};
527
560
  if (!releaseFamily) throw new AnthropicAccountCooldownError(1);
528
561
  let dispatched: Response;
562
+ const spendReport = createPhysicalSendReporter(physicalBudget, () => ({
563
+ poolId: logCtx.spendPoolId ?? route.providerName, identityId: logCtx.accountLogLabel,
564
+ }));
565
+ let sendOwner: AnthropicPhysicalSendOwnership | null = null;
529
566
  try {
530
567
  if (init.signal?.aborted) throw init.signal.reason;
531
- if (!spendTracker.charge()) throw new NativeMessagesSpendRefusal();
568
+ // Capture before either accounting path so this request retains its start policy.
569
+ if (spendReport.beforeSend && !spendReport.beforeSend()) {
570
+ if (spendTracker.refusals) throw new NativeMessagesSpendRefusal();
571
+ throw new SendBudgetExhaustedError();
572
+ }
573
+ if (!physicalBudget.spendEnforced && !spendTracker.charge()) throw new NativeMessagesSpendRefusal();
532
574
  noteProviderAttemptSend(logCtx, route.providerName, activeProvider, logCtx.usageLogInputTokens, transportRecovery ?? recovery);
575
+ // Last synchronous ownership reservation before the physical fetch. Retain this
576
+ // exact incarnation across its await; a returned response can never reserve one.
577
+ sendOwner = snapshot ? captureAnthropicPhysicalSendOwnership(snapshot) : null;
578
+ if (snapshot && !sendOwner) throw new NativeOAuthSelectionChangedError();
533
579
  dispatched = await sendWithConnectionPolicy(
534
580
  (activeProvider as OcxProviderTransport).fetch ?? execute,
535
581
  wire.url,
536
582
  applyUpstreamRecoveryInit({ ...init, method: "POST", headers, body: wire.body }, transportRecovery),
537
583
  { providerName: route.providerName, provider: activeProvider },
538
584
  );
539
- } finally { releaseFamily(); }
540
- if (ownsBearer && snapshot) {
585
+ } finally {
586
+ try { if (physicalBudget.spendEnforced) spendReport(1); }
587
+ finally {
588
+ try { spendReport.close?.(); }
589
+ finally { releaseFamily(); }
590
+ }
591
+ }
592
+ if (ownsBearer && snapshot && sendOwner && sendingBinding) {
541
593
  try {
542
- const current = getAccountCredentialWithStatus("anthropic", snapshot.accountId);
543
- if (current && !current.needsReauth && credentialGeneration(current.credential) === snapshot.generation
594
+ const current = getAccountCredentialWithStatus(snapshot.provider, snapshot.accountId);
595
+ if (anthropicPhysicalSendOwnershipIsCurrent(sendOwner) && nativeOAuthRouteIsCurrent(sendingBinding)
596
+ && current && !current.needsReauth && current.credential.access === snapshot.accessToken
597
+ && credentialGeneration(current.credential) === snapshot.generation
544
598
  && current.credential.accountId === providerAccountUuid) {
545
- bindAnthropicRefusalCredential(dispatched, snapshot, providerAccountUuid);
546
- recordAnthropicAccountQuotaFromHeaders(snapshot.accountId, dispatched.headers, writerGeneration, dispatched.status, route.modelId);
599
+ bindAnthropicRefusalCredentialForSend(dispatched, sendOwner, providerAccountUuid);
600
+ recordAnthropicAccountQuotaFromHeadersForInstance(nativeInstance!, snapshot.accountId, dispatched.headers, writerGeneration, dispatched.status, route.modelId);
547
601
  }
548
602
  } catch { /* Passive observation must not fail the response. */ }
549
603
  }
@@ -622,12 +676,13 @@ export async function handleNativeMessages(options: HandleNativeMessagesOptions)
622
676
  const sendingBinding = oauthBinding;
623
677
  const expectedRecoverySelection = sendingBinding.selection;
624
678
  triedAccountIds.add(sendingBinding.snapshot.accountId);
625
- const nextAccountId = await rotateAnthropicAccountOnResponse(response, {
679
+ const nextAccountId = await rotateAnthropicAccountOnResponseForInstance(sendingBinding.instance, response, {
626
680
  config, accountId: sendingBinding.snapshot.accountId, model: route.modelId,
627
681
  sessionKey: sendingBinding.sessionKey, decision: sendingBinding.routeDecision,
628
682
  excludedAccountIds: triedAccountIds,
629
683
  currentDecision: () => {
630
- const resolved = resolveAnthropicModelRoute(config, route.modelId);
684
+ if (!nativeOAuthRouteIsCurrent(sendingBinding)) throw new NativeOAuthSelectionChangedError();
685
+ const resolved = resolveAnthropicModelRouteForInstance(sendingBinding.instance, config, route.modelId);
631
686
  if (resolved.error) throw new NativeOAuthModelRouteError(`Invalid Anthropic model routes: ${resolved.error}`);
632
687
  return resolved.decision;
633
688
  },
@@ -637,7 +692,7 @@ export async function handleNativeMessages(options: HandleNativeMessagesOptions)
637
692
  if (!nextAccountId) break;
638
693
  const recovery = response.status === 403 ? "oauth-account-403" : "rate-limit-429";
639
694
  discard(response);
640
- oauthBinding = await resolveNativeOAuthBinding(config, {
695
+ oauthBinding = await resolveNativeOAuthBindingForInstance(nativeInstance!, config, { routeTarget,
641
696
  sessionKey: options.sessionKey, model: route.modelId, candidateAccountId: nextAccountId,
642
697
  expectedRecoverySelection, expectedRecoveryRouteDecision: sendingBinding.routeDecision,
643
698
  });
@@ -652,6 +707,7 @@ export async function handleNativeMessages(options: HandleNativeMessagesOptions)
652
707
  upstream.abort();
653
708
  if (req.signal.aborted) return fail(499, "Client cancelled request", "api_error");
654
709
  const sendError = error instanceof UpstreamRetryEvidenceError ? error.cause : error;
710
+ if (sendError instanceof SendBudgetExhaustedError) return fail(429, sendError.message, "send_budget_exhausted", "send_budget_exhausted");
655
711
  if (sendError instanceof NativeMessagesSpendRefusal) {
656
712
  const refusal = workflowRefusalResponse("workflow-spend-exhausted", logCtx);
657
713
  finishLog(429);
@@ -865,6 +921,7 @@ export function nativeMessagesCountBody(
865
921
  ): Rec | undefined {
866
922
  if (typeof body.model !== "string") return undefined;
867
923
  try {
924
+ if (messagesSecondaryInstanceUnavailable(config, body.model, cc)) return undefined;
868
925
  const selectorId = resolveInboundModel(body.model, cc);
869
926
  // Combo and policy selectors are never eligible, and routing them would advance round-robin
870
927
  // state or run the policy evaluator for a request that sends nothing.
@@ -875,11 +932,13 @@ export function nativeMessagesCountBody(
875
932
  route.providerName, route.modelId, route.provider, route.staticPolicy.effectiveAlias, "anthropic",
876
933
  );
877
934
  route.provider = resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, "anthropic", route.staticPolicy);
935
+ const instance = configuredAnthropicInstance(config, route.providerName);
878
936
  const selector: NativeMessagesSelector = {
879
937
  ...rows,
880
938
  routeSelector: selectorId,
881
939
  claudeCode: cc,
882
- ...(route.provider.authMode === "oauth" ? { oauthFailoverQuorum: hasAnthropicFailoverQuorum() } : {}),
940
+ ...(route.provider.authMode === "oauth" && instance
941
+ ? { oauthFailoverQuorum: anthropicRoutingFor(instance).hasAnthropicFailoverQuorum() } : {}),
883
942
  };
884
943
  if (nativeMessagesDeclineReason(route, body, config, selector) !== undefined) return undefined;
885
944
  // The body without a credential: counting never resolves or refreshes an OAuth account.
@@ -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[];
@@ -1,4 +1,4 @@
1
- import { rotateAnthropicAccountOnResponse } from "../../oauth/anthropic-account-refusal";
1
+ import { rotateAnthropicAccountOnResponseForInstance } from "../../oauth/anthropic-account-refusal";
2
2
  import type { ResponsesRequestContext } from "./core-options";
3
3
  import type { PreparedResponsesRequest } from "./request-prepare";
4
4
  import type { ResponsesTransport } from "./request-transport";
@@ -35,7 +35,7 @@ import { resolveWireProtocolOverride } from "../adapter-resolve";
35
35
  import { bindRouteReasoningReplayScope } from "./core-replay";
36
36
  import {
37
37
  ANTHROPIC_POOL_MAX_FAILOVERS_PER_REQUEST,
38
- getAnthropicPoolAccessSnapshot,
38
+ anthropicRoutingFor,
39
39
  formatAnthropicProviderForLog,
40
40
  } from "../../oauth/anthropic-routing";
41
41
  import {
@@ -83,6 +83,8 @@ export function createAdapterContinuations(
83
83
  | "oauthDispatch"
84
84
  | "invalidateSameTargetRequest"
85
85
  | "resolveSelectionAdapter"
86
+ | "anthropicInstance"
87
+ | "currentAnthropicRouteDecision"
86
88
  | "anthropicRouteDecision"
87
89
  | "anthropicPoolAccountId"
88
90
  | "anthropicPoolFailovers"
@@ -102,7 +104,8 @@ export function createAdapterContinuations(
102
104
  | "noteAdapterPhysicalSend"
103
105
  | "noteAdapterRecoveryWithheld"
104
106
  | "remainingTransientSendBudget"
105
- | "noteTransientSends"
107
+ | "adapterSendBudget"
108
+ | "transientSendReporter"
106
109
  | "reserveCredentialHop"
107
110
  | "pendingHopPermit"
108
111
  | "sendBudgetExhausted"
@@ -122,6 +125,8 @@ export function createAdapterContinuations(
122
125
  oauthDispatch,
123
126
  invalidateSameTargetRequest,
124
127
  resolveSelectionAdapter,
128
+ anthropicInstance,
129
+ currentAnthropicRouteDecision,
125
130
  anthropicSessionKey,
126
131
  commitResolvedOAuthSelection,
127
132
  applyFailoverSnapshot,
@@ -135,7 +140,7 @@ export function createAdapterContinuations(
135
140
  noteAdapterPhysicalSend,
136
141
  noteAdapterRecoveryWithheld,
137
142
  remainingTransientSendBudget,
138
- noteTransientSends,
143
+ transientSendReporter,
139
144
  reserveCredentialHop,
140
145
  sendBudgetExhausted,
141
146
  } = sendBudgetState;
@@ -211,6 +216,8 @@ export function createAdapterContinuations(
211
216
  try {
212
217
  if (transportState.activeAdapter.fetchResponse) {
213
218
  transportState.noteRoutedAttemptSend(continuationEstimate, replayKind);
219
+ const producer = adapterDispatchBudget?.beginSpendProducer?.();
220
+ try {
214
221
  return await withProviderRequestSlot(route.providerName, route.provider, nextParsed.modelId, upstream.signal, pacingSlot =>
215
222
  transportState.activeAdapter.fetchResponse!(builtContinuationRequest, {
216
223
  kiroPreferAccountFailover: route.providerName === "kiro" && isGenericOAuthFailoverEnabled(config, "kiro"),
@@ -228,6 +235,7 @@ export function createAdapterContinuations(
228
235
  modelId: nextParsed.modelId,
229
236
  }),
230
237
  }));
238
+ } finally { producer?.close(); }
231
239
  }
232
240
  // Same #1851 scope guard as the initial send: transient-5xx retry only for direct
233
241
  // Google AI Studio; every other adapter keeps reset-only semantics here.
@@ -237,6 +245,9 @@ export function createAdapterContinuations(
237
245
  : fetchWithResetRetry;
238
246
  return await fetchContinuationWithRetryPolicy(
239
247
  recovery => {
248
+ sendBudgetState.adapterSendBudget?.startRequest?.({
249
+ poolId: logCtx.spendPoolId ?? route.providerName, identityId: logCtx.accountLogLabel,
250
+ });
240
251
  transportState.noteRoutedAttemptSend(continuationEstimate, recovery ?? replayKind);
241
252
  return fetchWithHeaderTimeout(
242
253
  builtContinuationRequest.url,
@@ -261,10 +272,10 @@ export function createAdapterContinuations(
261
272
  // Same request-scoped budget as the initial send and the 429/rotation refetches:
262
273
  // a terminal-guard continuation is another leg of ONE request, so handing it a
263
274
  // fresh `attempts` would let one request exceed the configured total-send ceiling.
264
- ...(continuationTransientPolicy
275
+ ...(continuationTransientPolicy || sendBudgetState.adapterSendBudget?.spendEnforced
265
276
  ? {
266
- attempts: remainingTransientSendBudget(continuationTransientPolicy.attempts),
267
- onSendsConsumed: noteTransientSends,
277
+ attempts: remainingTransientSendBudget(continuationTransientPolicy?.attempts ?? 1),
278
+ onSendsConsumed: transientSendReporter(),
268
279
  }
269
280
  : {}),
270
281
  },
@@ -391,25 +402,26 @@ export function createAdapterContinuations(
391
402
  }
392
403
  if (
393
404
  (response.status === 429 || response.status === 403)
405
+ && anthropicInstance
394
406
  && transportState.anthropicPoolAccountId
395
407
  && !isNonReplayableResponse(response)
396
408
  ) {
397
- const nextAccountId = await rotateAnthropicAccountOnResponse(response, {
409
+ const nextAccountId = await rotateAnthropicAccountOnResponseForInstance(anthropicInstance, response, {
398
410
  config, accountId: transportState.anthropicPoolAccountId, sessionKey: anthropicSessionKey,
399
- model: route.modelId, requestKey: transportState, decision: transportState.anthropicRouteDecision, signal: upstream.signal,
411
+ model: route.modelId, requestKey: transportState, decision: transportState.anthropicRouteDecision, currentDecision: currentAnthropicRouteDecision, signal: upstream.signal,
400
412
  canRetry: !sendBudgetExhausted() && transportState.anthropicPoolFailovers < ANTHROPIC_POOL_MAX_FAILOVERS_PER_REQUEST,
401
413
  allowAccountRefusal, allow429Recovery: allowAccountRefusal,
402
414
  });
403
415
  if (nextAccountId) {
404
416
  try {
405
- const admitted = await commitResolvedOAuthSelection(await getAnthropicPoolAccessSnapshot(nextAccountId));
417
+ const admitted = await commitResolvedOAuthSelection(await anthropicRoutingFor(anthropicInstance).getAnthropicPoolAccessSnapshot(nextAccountId));
406
418
  if (!admitted) throw new Error("OAuth selection changed during recovery");
407
419
  try { void response.body?.cancel().catch(() => {}); } catch { /* already consumed/closed */ }
408
420
  transportState.anthropicPoolAccountId = admitted.accountId;
409
421
  transportState.anthropicPoolFailovers += 1;
410
422
  route.provider = { ...route.provider, apiKey: admitted.accessToken };
411
423
  invalidateSameTargetRequest();
412
- logCtx.provider = formatAnthropicProviderForLog("anthropic", admitted.accountId, config);
424
+ logCtx.provider = formatAnthropicProviderForLog(anthropicInstance, admitted.accountId, config);
413
425
  transportState.activeAdapter = resolveSelectionAdapter(
414
426
  resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire, route.staticPolicy),
415
427
  config.cacheRetention,
@@ -513,9 +525,9 @@ export function createAdapterContinuations(
513
525
  recordAttemptCredentialSource(logCtx.activeAttempt, route.providerName, route.provider, transportState.activeAdapter.name);
514
526
  // The replay goes out on the next iteration. An adapter that owns its ladder
515
527
  // reserves for that send itself, so hand this reservation down rather than let it
516
- // take a second one for the same replay. A helper-routed replay needs no handoff:
517
- // its reporter settles the booking made above.
518
- if (adapterOwnsDispatch) sendBudgetState.pendingHopPermit = hop.permit;
528
+ // take a second one for the same replay. A helper-routed replay carries the
529
+ // same permit so its reporter settles only this booking.
530
+ sendBudgetState.pendingHopPermit = hop.permit;
519
531
  nextContinuationRecoveryKind = "oauth-account-429";
520
532
  kiroRefusalPendingReplay = response;
521
533
  continue;
@@ -604,9 +616,9 @@ export function createAdapterContinuations(
604
616
  recordAttemptCredentialSource(logCtx.activeAttempt, route.providerName, route.provider, transportState.activeAdapter.name);
605
617
  // The replay goes out on the next iteration. An adapter that owns its ladder
606
618
  // reserves for that send itself, so hand this reservation down rather than let it
607
- // take a second one for the same replay. A helper-routed replay needs no handoff:
608
- // its reporter settles the booking made above.
609
- if (adapterOwnsDispatch) sendBudgetState.pendingHopPermit = hop.permit;
619
+ // take a second one for the same replay. A helper-routed replay carries the
620
+ // same permit so its reporter settles only this booking.
621
+ sendBudgetState.pendingHopPermit = hop.permit;
610
622
  nextContinuationRecoveryKind = "oauth-account-429";
611
623
  continue;
612
624
  }