@bitkyc08/opencodex 2.80.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 (227) 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/devin/live-models.ts +2 -0
  20. package/src/adapters/kiro-retry.ts +9 -4
  21. package/src/adapters/openai-chat/messages.ts +7 -6
  22. package/src/adapters/physical-send.ts +8 -5
  23. package/src/adapters/qoder/adapter.ts +41 -6
  24. package/src/adapters/qoder/scaffold-guard.ts +2 -3
  25. package/src/adapters/zed.ts +16 -1
  26. package/src/claude/inbound-cache-stabilize.ts +4 -9
  27. package/src/claude/inbound.ts +1 -0
  28. package/src/claude/intercept/model-bindings.ts +1 -0
  29. package/src/claude/intercept/picker-listener.ts +71 -4
  30. package/src/cli/account-anthropic-threshold.ts +14 -5
  31. package/src/cli/account-api.ts +3 -1
  32. package/src/cli/account-extended.ts +13 -9
  33. package/src/cli/account-policy-dto.ts +1 -1
  34. package/src/cli/account-policy.ts +13 -4
  35. package/src/cli/account.ts +3 -3
  36. package/src/cli/capabilities-accounts.ts +2 -2
  37. package/src/cli/capabilities-agents-routing.ts +24 -0
  38. package/src/cli/capabilities-integrations.ts +16 -0
  39. package/src/cli/codex-shim-autorestore.ts +3 -0
  40. package/src/cli/dispatch.ts +10 -1
  41. package/src/cli/help.ts +4 -1
  42. package/src/cli/index.ts +11 -5
  43. package/src/cli/integration-plan-dto.ts +1 -0
  44. package/src/cli/integration-preview.ts +8 -1
  45. package/src/cli/integrations.ts +32 -3
  46. package/src/cli/message-args.ts +34 -0
  47. package/src/cli/message-command.ts +43 -0
  48. package/src/cli/message-runtime.ts +6 -0
  49. package/src/cli/observe.ts +3 -1
  50. package/src/cli/provider.ts +29 -6
  51. package/src/cli/registry.ts +24 -2
  52. package/src/clients/config-export/commandcode.ts +131 -0
  53. package/src/clients/config-export/contracts.ts +12 -0
  54. package/src/clients/config-export.ts +64 -1
  55. package/src/codex/auth-api/account-list.ts +11 -3
  56. package/src/codex/auth-api/main-account-probe.ts +25 -8
  57. package/src/codex/auth-api/pool-mode-gate.ts +33 -9
  58. package/src/codex/auth-api/pool-quota-probe.ts +15 -1
  59. package/src/codex/auth-api/routes.ts +1 -0
  60. package/src/codex/catalog/auto-review.ts +13 -6
  61. package/src/codex/catalog/build-entries.ts +15 -7
  62. package/src/codex/catalog/control-plane.ts +45 -0
  63. package/src/codex/catalog/effort.ts +11 -1
  64. package/src/codex/catalog/gather-capture.ts +5 -3
  65. package/src/codex/catalog/parsing.ts +2 -1
  66. package/src/codex/catalog/provider-models.ts +15 -9
  67. package/src/codex/control-plane-models.ts +6 -0
  68. package/src/combos/jev-decision-contract.ts +25 -2
  69. package/src/combos/jev.ts +3 -2
  70. package/src/combos/types.ts +1 -1
  71. package/src/config/diagnostics.ts +27 -16
  72. package/src/config/load-degrade.ts +34 -0
  73. package/src/config/schema/anthropic-account-pool.ts +143 -0
  74. package/src/config/schema/config-schema.ts +7 -1
  75. package/src/config/schema/leaf-validators.ts +4 -0
  76. package/src/config.ts +2 -0
  77. package/src/generated/compatibility-version.json +320 -184
  78. package/src/generated/model-metadata.ts +5 -4
  79. package/src/images/loop.ts +2 -1
  80. package/src/integrations/catalog-refresh.ts +1 -1
  81. package/src/integrations/mutation-plan.ts +7 -0
  82. package/src/integrations/registry.ts +8 -0
  83. package/src/integrations/state.ts +9 -2
  84. package/src/integrations/writer.ts +8 -2
  85. package/src/lib/lab-live-route-production.ts +9 -1
  86. package/src/lib/request-execution-budget.ts +236 -58
  87. package/src/lib/spend-pool-alias-validation.ts +79 -0
  88. package/src/lib/spend-pool-continuity.ts +34 -0
  89. package/src/lib/spend-reservation-ledger.ts +412 -50
  90. package/src/lib/state-store-registrations.ts +20 -3
  91. package/src/lib/upstream-retry.ts +17 -5
  92. package/src/lib/workflow-budget.ts +53 -11
  93. package/src/messaging/budget.ts +45 -0
  94. package/src/messaging/discovery.ts +57 -0
  95. package/src/messaging/envelope.ts +49 -0
  96. package/src/messaging/input.ts +37 -0
  97. package/src/messaging/rpc.ts +196 -0
  98. package/src/messaging/send.ts +58 -0
  99. package/src/messaging/socket.ts +72 -0
  100. package/src/messaging/types.ts +29 -0
  101. package/src/oauth/anthropic-account-refusal.ts +51 -17
  102. package/src/oauth/anthropic-account-threshold.ts +8 -2
  103. package/src/oauth/anthropic-continuity.ts +15 -1
  104. package/src/oauth/anthropic-model-quota.ts +120 -48
  105. package/src/oauth/anthropic-model-routes.ts +11 -3
  106. package/src/oauth/anthropic-oauth-definitions.ts +98 -0
  107. package/src/oauth/anthropic-pool-config.ts +30 -0
  108. package/src/oauth/anthropic-rate-limit-policy.ts +64 -9
  109. package/src/oauth/anthropic-routing.ts +1202 -866
  110. package/src/oauth/anthropic-send-ownership.ts +41 -0
  111. package/src/oauth/anthropic.ts +4 -1
  112. package/src/oauth/generic-account-failover.ts +3 -1
  113. package/src/oauth/health.ts +8 -6
  114. package/src/oauth/index.ts +90 -173
  115. package/src/oauth/kiro.ts +22 -9
  116. package/src/oauth/login-cli.ts +2 -0
  117. package/src/oauth/model-discovery-auth.ts +43 -0
  118. package/src/oauth/pool-kernel.ts +11 -5
  119. package/src/oauth/pool-settings-capability.ts +5 -3
  120. package/src/oauth/store-anthropic-instance.ts +87 -0
  121. package/src/oauth/store.ts +67 -9
  122. package/src/oauth/token-guardian.ts +2 -0
  123. package/src/oauth/types.ts +2 -0
  124. package/src/protocols/plan-snapshot.ts +5 -0
  125. package/src/protocols/settings.ts +20 -5
  126. package/src/providers/anthropic-instance-id.ts +47 -0
  127. package/src/providers/anthropic-instance.ts +48 -0
  128. package/src/providers/anthropic-reset-grant-ledger.ts +11 -1
  129. package/src/providers/derive.ts +2 -0
  130. package/src/providers/kiro-models.ts +3 -0
  131. package/src/providers/label.ts +1 -1
  132. package/src/providers/model-rename-fields.ts +2 -0
  133. package/src/providers/quota/account-cache.ts +37 -11
  134. package/src/providers/quota/anthropic-account-quota.ts +93 -0
  135. package/src/providers/quota/anthropic-cooldown-recovery.ts +222 -111
  136. package/src/providers/quota/report-cache.ts +24 -1
  137. package/src/providers/quota/vendor-probes-oauth.ts +46 -14
  138. package/src/providers/quota.ts +22 -32
  139. package/src/providers/registry/entries-core.ts +35 -30
  140. package/src/providers/registry/model-ids.ts +1 -0
  141. package/src/providers/registry/model-seeds.ts +7 -4
  142. package/src/providers/registry/types.ts +3 -0
  143. package/src/providers/registry.ts +3 -3
  144. package/src/responses/spill-store.ts +5 -1
  145. package/src/responses/state/metrics.ts +87 -0
  146. package/src/responses/state/snapshot-codec.ts +6 -0
  147. package/src/responses/state/spill-queue.ts +1 -0
  148. package/src/responses/state/unforced-store-false.ts +55 -0
  149. package/src/responses/state.ts +50 -66
  150. package/src/router.ts +28 -4
  151. package/src/routing/compatibility/assemble.ts +1 -1
  152. package/src/routing/quota.ts +7 -3
  153. package/src/server/auth-cors.ts +5 -0
  154. package/src/server/chat-completions.ts +6 -1
  155. package/src/server/chat-native.ts +45 -25
  156. package/src/server/claude-messages.ts +16 -2
  157. package/src/server/gui-static.ts +38 -4
  158. package/src/server/index/spend-ledger-lifecycle.ts +10 -4
  159. package/src/server/index.ts +4 -2
  160. package/src/server/inference/context.ts +1 -1
  161. package/src/server/lifecycle.ts +1 -1
  162. package/src/server/management/agent-settings-routes.ts +30 -6
  163. package/src/server/management/anthropic-account-threshold.ts +12 -9
  164. package/src/server/management/anthropic-pool-settings.ts +72 -0
  165. package/src/server/management/anthropic-reset-grant-routes.ts +98 -39
  166. package/src/server/management/config-routes.ts +27 -7
  167. package/src/server/management/decision-routes.ts +2 -1
  168. package/src/server/management/integration-routes.ts +25 -2
  169. package/src/server/management/oauth-account-routes.ts +79 -98
  170. package/src/server/management/provider-instance-ownership.ts +44 -0
  171. package/src/server/management/provider-routes.ts +21 -28
  172. package/src/server/management/route-registry.ts +3 -1
  173. package/src/server/management/routing-profile-routes.ts +4 -4
  174. package/src/server/management/system-restart.ts +44 -14
  175. package/src/server/management/system-routes.ts +24 -2
  176. package/src/server/management/vision-sidecar-options.ts +9 -6
  177. package/src/server/management/web-search-sidecar-options.ts +51 -2
  178. package/src/server/management-auth.ts +6 -0
  179. package/src/server/messages-native-eligibility.ts +3 -2
  180. package/src/server/messages-native-oauth.ts +86 -41
  181. package/src/server/messages-native-selector.ts +39 -0
  182. package/src/server/messages-native.ts +84 -25
  183. package/src/server/request-log.ts +5 -0
  184. package/src/server/responses/adapter-continuation.ts +29 -17
  185. package/src/server/responses/adapter-dispatch.ts +47 -17
  186. package/src/server/responses/antigravity-validation-refusal.ts +20 -6
  187. package/src/server/responses/compact.ts +25 -8
  188. package/src/server/responses/compaction-recovery.ts +1 -1
  189. package/src/server/responses/core-combo-native.ts +4 -1
  190. package/src/server/responses/core-combo.ts +16 -11
  191. package/src/server/responses/core-normalize.ts +1 -0
  192. package/src/server/responses/core-options.ts +3 -1
  193. package/src/server/responses/core-replay.ts +5 -0
  194. package/src/server/responses/passthrough-delivery.ts +7 -3
  195. package/src/server/responses/passthrough-dispatch.ts +21 -8
  196. package/src/server/responses/request-prepare.ts +37 -5
  197. package/src/server/responses/request-send-budget.ts +74 -37
  198. package/src/server/responses/request-spend.ts +325 -33
  199. package/src/server/responses/request-transport.ts +132 -52
  200. package/src/server/responses/run-turn-execution.ts +11 -3
  201. package/src/server/responses/sidecar-execution.ts +96 -14
  202. package/src/server/responses-undeclared-tool-guard.ts +5 -1
  203. package/src/server/workflow-refusal.ts +30 -0
  204. package/src/service/ownership-mutation-lease.d.mts +1 -1
  205. package/src/service/ownership-mutation-lease.mjs +5 -7
  206. package/src/sidecar/anthropic-binding.ts +79 -0
  207. package/src/sidecar/auth.ts +76 -2
  208. package/src/types/anthropic-account-pool.ts +33 -0
  209. package/src/types/config.ts +12 -28
  210. package/src/types/provider.ts +5 -0
  211. package/src/types/tools.ts +7 -0
  212. package/src/types.ts +4 -0
  213. package/src/usage/cost.ts +3 -2
  214. package/src/usage/expected-prices.ts +66 -14
  215. package/src/vision/anthropic-describe.ts +16 -3
  216. package/src/vision/index.ts +3 -0
  217. package/src/vision/plan.ts +19 -6
  218. package/src/web-search/alpha-search.ts +7 -1
  219. package/src/web-search/anthropic-executor.ts +16 -4
  220. package/src/web-search/index.ts +7 -3
  221. package/src/web-search/loop.ts +27 -5
  222. package/src/web-search/passthrough-bridge.ts +4 -1
  223. package/src/web-search/sidecar-providers.ts +4 -3
  224. package/gui/dist/assets/App-MbDVfUeC.js +0 -54
  225. package/gui/dist/assets/Tray-CzkxK82A.js +0 -1
  226. package/gui/dist/assets/index-UJt6npXk.js +0 -94
  227. package/gui/dist/assets/quota-summary-DU3sD6Z-.js +0 -1
@@ -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
  }
@@ -1,5 +1,5 @@
1
1
  import { classifyAnthropic429 } from "../../oauth/anthropic-rate-limit-policy";
2
- import { rotateAnthropicAccountOnResponse } from "../../oauth/anthropic-account-refusal";
2
+ import { rotateAnthropicAccountOnResponseForInstance } from "../../oauth/anthropic-account-refusal";
3
3
  import { authorizeResendForRecovery } from "../../lib/request-resend-gate";
4
4
  import { ambiguousResendAllowanceFor, selfContainedResponsesBody } from "./reset-replay";
5
5
  import { transientSendCapFor } from "./request-send-budget";
@@ -65,7 +65,7 @@ import { bindRouteReasoningReplayScope } from "./core-replay";
65
65
  import {
66
66
  AnthropicAccountCooldownError,
67
67
  ANTHROPIC_POOL_MAX_FAILOVERS_PER_REQUEST,
68
- getAnthropicPoolAccessSnapshot,
68
+ anthropicRoutingFor,
69
69
  formatAnthropicProviderForLog,
70
70
  } from "../../oauth/anthropic-routing";
71
71
  import {
@@ -106,6 +106,7 @@ import { isCanonicalOpenAiForwardProvider } from "../../providers/openai-tiers";
106
106
  import { mapCodexAuthContextErrorToResponse } from "./codex-auth-error";
107
107
  import { chargeWorkflowSends } from "../../lib/workflow-budget";
108
108
  import { isAntigravityValidationRefusal } from "./antigravity-validation-refusal";
109
+ import { unboundPoolSpendRefusalResponse } from "../workflow-refusal";
109
110
 
110
111
  /** One responsibility of the Responses request pipeline; state owners are explicit. */
111
112
  export async function prepareAdapterExchange(
@@ -139,6 +140,8 @@ export async function prepareAdapterExchange(
139
140
  | "replayOAuthCredentialSnapshot"
140
141
  | "invalidateSameTargetRequest"
141
142
  | "resolveSelectionAdapter"
143
+ | "anthropicInstance"
144
+ | "currentAnthropicRouteDecision"
142
145
  | "anthropicRouteDecision"
143
146
  | "anthropicPoolAccountId"
144
147
  | "anthropicPoolFailovers"
@@ -157,7 +160,8 @@ export async function prepareAdapterExchange(
157
160
  | "noteAdapterPhysicalSend"
158
161
  | "noteAdapterRecoveryWithheld"
159
162
  | "remainingTransientSendBudget"
160
- | "noteTransientSends"
163
+ | "adapterSendBudget"
164
+ | "transientSendReporter"
161
165
  | "recoverySendAllowance"
162
166
  | "recoveryClassFor"
163
167
  | "sendBudgetExhausted"
@@ -175,6 +179,8 @@ export async function prepareAdapterExchange(
175
179
  refreshResolvedOAuthSelection,
176
180
  invalidateSameTargetRequest,
177
181
  resolveSelectionAdapter,
182
+ anthropicInstance,
183
+ currentAnthropicRouteDecision,
178
184
  anthropicSessionKey,
179
185
  commitResolvedOAuthSelection,
180
186
  applyFailoverSnapshot,
@@ -194,7 +200,7 @@ export async function prepareAdapterExchange(
194
200
  noteAdapterPhysicalSend,
195
201
  noteAdapterRecoveryWithheld,
196
202
  remainingTransientSendBudget,
197
- noteTransientSends,
203
+ transientSendReporter,
198
204
  recoverySendAllowance,
199
205
  recoveryClassFor,
200
206
  sendBudgetExhausted,
@@ -333,6 +339,8 @@ export async function prepareAdapterExchange(
333
339
  try {
334
340
  if (transportState.activeAdapter.fetchResponse) {
335
341
  transportState.noteRoutedAttemptSend(inputTokenEstimate);
342
+ const producer = adapterDispatchBudget?.beginSpendProducer?.();
343
+ try {
336
344
  upstreamResponse = await withProviderRequestSlot(route.providerName, route.provider, route.modelId, upstream.signal, pacingSlot =>
337
345
  transportState.activeAdapter.fetchResponse!(builtInitialRequest, {
338
346
  kiroPreferAccountFailover: route.providerName === "kiro" && isGenericOAuthFailoverEnabled(config, "kiro"),
@@ -352,6 +360,7 @@ export async function prepareAdapterExchange(
352
360
  ? createCodexAuthDispatchGuard(admissionState.authCtx, options.codexAuthPolicy ?? config, route.modelId, options.admission, options.visionDescribeTerminal === true) : undefined,
353
361
  }),
354
362
  }));
363
+ } finally { producer?.close(); }
355
364
  } else {
356
365
  // #1851 scope guard: transient-5xx retry on this generic adapter path is opt-in for
357
366
  // direct Google AI Studio only (Vertex/Antigravity use fetchResponse above). Other
@@ -361,13 +370,14 @@ export async function prepareAdapterExchange(
361
370
  // legacy direct-Google exception is preserved exactly; every other adapter still keeps
362
371
  // reset-only semantics so combo failover hops on the first 5xx.
363
372
  const transientPolicy = transientRetryPolicyFor(route.provider);
373
+ const resetPolicy = resetReplayPolicyFor(route.provider);
364
374
  const compactPrepaid = options.compactionRecoveryAttempted ? sendBudgetState.pendingHopPermit : undefined;
365
375
  if (compactPrepaid) sendBudgetState.pendingHopPermit = undefined;
366
376
  let compactPrepaidUsed = false;
367
377
  // Combo and emergency compaction admission already book this target's first send.
368
378
  // Its configured initial ceiling is target-local; the shared remainder below still
369
379
  // accounts for earlier targets without deducting their sends from this target twice.
370
- const initialSendCap = transientPolicy || resetReplayPolicyFor(route.provider)
380
+ const initialSendCap = transientPolicy || resetPolicy
371
381
  ? transientSendCapFor(transientPolicy?.attempts,
372
382
  (options.comboAttempt || compactPrepaid) ? 0 : sendBudgetState.sendsUsed)
373
383
  : 1;
@@ -376,6 +386,11 @@ export async function prepareAdapterExchange(
376
386
  : fetchWithResetRetry;
377
387
  upstreamResponse = await fetchWithRetryPolicy(
378
388
  recovery => {
389
+ // Unconfigured generic sends may omit the counting reporter. Capture policy
390
+ // independently so a later recovery still belongs to this request's start.
391
+ sendBudgetState.adapterSendBudget?.startRequest?.({
392
+ poolId: logCtx.spendPoolId ?? route.providerName, identityId: logCtx.accountLogLabel,
393
+ });
379
394
  if (compactPrepaid && !compactPrepaidUsed) {
380
395
  if (!compactPrepaid.use()) throw new SendBudgetExhaustedError(safeHostLabel(builtInitialRequest.url));
381
396
  compactPrepaidUsed = true;
@@ -398,12 +413,18 @@ export async function prepareAdapterExchange(
398
413
  abortSignal: upstream.signal,
399
414
  label: safeHostLabel(builtInitialRequest.url),
400
415
  claimAmbiguousResend: claimPreHeaderResend,
401
- ...(transientPolicy || resetReplayPolicyFor(route.provider) || compactPrepaid
416
+ ...(sendBudgetState.adapterSendBudget?.spendEnforced || options.comboDispatchPermit || transientPolicy || resetPolicy || compactPrepaid
402
417
  ? {
403
418
  // A pending compaction permit already booked this leg's first physical send.
404
- attempts: Math.min(initialSendCap,
405
- remainingTransientSendBudget(initialSendCap) + (compactPrepaid ? 1 : 0)),
406
- onSendsConsumed: noteTransientSends,
419
+ ...(transientPolicy || resetPolicy || compactPrepaid
420
+ ? {
421
+ attempts: Math.min(initialSendCap,
422
+ remainingTransientSendBudget(initialSendCap) + (compactPrepaid ? 1 : 0)),
423
+ }
424
+ : {}),
425
+ // Keep the exact combo or compaction receipt on the helper callback. The reporter
426
+ // also settles the request and workflow counters for reset-only retries.
427
+ onSendsConsumed: transientSendReporter(compactPrepaid ?? options.comboDispatchPermit),
407
428
  }
408
429
  : {}),
409
430
  },
@@ -437,7 +458,8 @@ export async function prepareAdapterExchange(
437
458
  // blaming the provider makes the caller send the whole turn again -- the amplification this
438
459
  // budget exists to stop. The passthrough path has answered 429 here since #4546.
439
460
  if (err instanceof SendBudgetExhaustedError) {
440
- return formatErrorResponse(429, SEND_BUDGET_EXHAUSTED_CODE, err.message);
461
+ return unboundPoolSpendRefusalResponse(logCtx)
462
+ ?? formatErrorResponse(429, SEND_BUDGET_EXHAUSTED_CODE, err.message);
441
463
  }
442
464
  const msg = describeUpstreamConnectFailure(err, connectMs);
443
465
  return formatErrorResponse(502, "upstream_error", msg);
@@ -541,6 +563,8 @@ export async function prepareAdapterExchange(
541
563
  try {
542
564
  if (transportState.activeAdapter.fetchResponse) {
543
565
  transportState.noteRoutedAttemptSend(retryEstimate, recovery);
566
+ const producer = adapterDispatchBudget?.beginSpendProducer?.();
567
+ try {
544
568
  return await withProviderRequestSlot(route.providerName, route.provider, route.modelId, upstream.signal, pacingSlot => {
545
569
  // The dispatch boundary is HERE, not before the pacing wait: that wait can reject for
546
570
  // an abort, a saturated queue, an expired slot or a removed provider, and none of
@@ -570,6 +594,7 @@ export async function prepareAdapterExchange(
570
594
  }),
571
595
  });
572
596
  });
597
+ } finally { producer?.close(); }
573
598
  }
574
599
  // #2643 review: this leg used to call fetchWithHeaderTimeout directly, so an
575
600
  // opted-in provider's transient-5xx policy applied to the initial send and to
@@ -581,7 +606,7 @@ export async function prepareAdapterExchange(
581
606
  const refetchWithPolicy = (route.provider.adapter === "google" || refetchTransientPolicy)
582
607
  ? fetchWithTransientRetry
583
608
  : fetchWithResetRetry;
584
- const helperCountsSends = refetchTransientPolicy !== null || resetReplayPolicyFor(route.provider) !== null;
609
+ const helperCountsSends = sendBudgetState.adapterSendBudget?.spendEnforced === true || refetchTransientPolicy !== null || resetReplayPolicyFor(route.provider) !== null;
585
610
  const prepaid = sendBudgetState.pendingHopPermit;
586
611
  const configuredTotal = refetchTransientPolicy?.attempts;
587
612
  const refetchCap = transientSendCapFor(configuredTotal,
@@ -601,6 +626,9 @@ export async function prepareAdapterExchange(
601
626
  try {
602
627
  return await refetchWithPolicy(
603
628
  recoveryKind => {
629
+ sendBudgetState.adapterSendBudget?.startRequest?.({
630
+ poolId: logCtx.spendPoolId ?? route.providerName, identityId: logCtx.accountLogLabel,
631
+ });
604
632
  if (refetchAllowance?.permit && !refetchAllowance.permit.use()) {
605
633
  throw new SendBudgetExhaustedError(safeHostLabel(retryRequest.url));
606
634
  }
@@ -629,7 +657,7 @@ export async function prepareAdapterExchange(
629
657
  ...(refetchAllowance
630
658
  ? {
631
659
  attempts: refetchAllowance.attempts,
632
- onSendsConsumed: noteTransientSends,
660
+ onSendsConsumed: transientSendReporter(refetchAllowance.permit),
633
661
  }
634
662
  : {}),
635
663
  },
@@ -673,7 +701,8 @@ export async function prepareAdapterExchange(
673
701
  // Same rule on the recovery leg: the ladder refused to send again, so the answer names
674
702
  // this proxy rather than the provider it never reached.
675
703
  if (err instanceof SendBudgetExhaustedError) {
676
- return { failed: formatErrorResponse(429, SEND_BUDGET_EXHAUSTED_CODE, err.message) };
704
+ return { failed: unboundPoolSpendRefusalResponse(logCtx)
705
+ ?? formatErrorResponse(429, SEND_BUDGET_EXHAUSTED_CODE, err.message) };
677
706
  }
678
707
  const msg = describeUpstreamConnectFailure(err, connectMs);
679
708
  return { failed: formatErrorResponse(502, "upstream_error", msg) };
@@ -1048,23 +1077,24 @@ export async function prepareAdapterExchange(
1048
1077
  // before output, within the shared request and account rotation limits.
1049
1078
  while (
1050
1079
  (upstreamResponse.status === 429 || upstreamResponse.status === 403)
1080
+ && anthropicInstance
1051
1081
  && transportState.anthropicPoolAccountId
1052
1082
  ) {
1053
- const nextAccountId = await rotateAnthropicAccountOnResponse(upstreamResponse, {
1083
+ const nextAccountId = await rotateAnthropicAccountOnResponseForInstance(anthropicInstance, upstreamResponse, {
1054
1084
  config, accountId: transportState.anthropicPoolAccountId, sessionKey: anthropicSessionKey,
1055
- model: route.modelId, requestKey: transportState, decision: transportState.anthropicRouteDecision, signal: upstream.signal,
1085
+ model: route.modelId, requestKey: transportState, decision: transportState.anthropicRouteDecision, currentDecision: currentAnthropicRouteDecision, signal: upstream.signal,
1056
1086
  canRetry: !sendBudgetExhausted() && transportState.anthropicPoolFailovers < ANTHROPIC_POOL_MAX_FAILOVERS_PER_REQUEST,
1057
1087
  });
1058
1088
  if (!nextAccountId) break;
1059
1089
  try {
1060
- const admitted = await commitResolvedOAuthSelection(await getAnthropicPoolAccessSnapshot(nextAccountId));
1090
+ const admitted = await commitResolvedOAuthSelection(await anthropicRoutingFor(anthropicInstance).getAnthropicPoolAccessSnapshot(nextAccountId));
1061
1091
  if (!admitted) throw new Error("OAuth selection changed during recovery");
1062
1092
  try { void upstreamResponse.body?.cancel().catch(() => {}); } catch { /* already consumed/closed */ }
1063
1093
  transportState.anthropicPoolAccountId = admitted.accountId;
1064
1094
  transportState.anthropicPoolFailovers += 1;
1065
1095
  route.provider = { ...route.provider, apiKey: admitted.accessToken };
1066
1096
  invalidateSameTargetRequest();
1067
- logCtx.provider = formatAnthropicProviderForLog("anthropic", admitted.accountId, config);
1097
+ logCtx.provider = formatAnthropicProviderForLog(anthropicInstance, admitted.accountId, config);
1068
1098
  transportState.activeAdapter = resolveSelectionAdapter(
1069
1099
  resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire, route.staticPolicy),
1070
1100
  config.cacheRetention,
@@ -1,19 +1,33 @@
1
- import { ANTIGRAVITY_VALIDATION_REQUIRED_PREFIX } from "../../adapters/google-errors";
1
+ import { ANTIGRAVITY_VALIDATION_REQUIRED_PREFIX, safeAntigravityHttpErrorMessage } from "../../adapters/google-errors";
2
2
  import { readBoundedResponseBody } from "../../lib/bounded-body";
3
3
 
4
+ /** Match only the normalized Antigravity validation-refusal marker. */
5
+ export function hasAntigravityValidationRefusalMarker(text: string): boolean {
6
+ return text.startsWith(`${ANTIGRAVITY_VALIDATION_REQUIRED_PREFIX}: `);
7
+ }
8
+
4
9
  /** Inspect only the Google adapter's normalized, already bounded 403 response. */
5
10
  export async function isAntigravityValidationRefusal(response: Response, signal?: AbortSignal): Promise<boolean> {
11
+ return inspectValidationRefusal(response, signal, false);
12
+ }
13
+
14
+ /** Sidecar dispatch retains the raw Google envelope; normalize only a complete bounded body. */
15
+ export async function isAntigravityRawValidationRefusal(response: Response, signal?: AbortSignal): Promise<boolean> {
16
+ return inspectValidationRefusal(response, signal, true);
17
+ }
18
+
19
+ async function inspectValidationRefusal(response: Response, signal: AbortSignal | undefined, raw: boolean): Promise<boolean> {
6
20
  if (response.status !== 403 || signal?.aborted) return false;
7
21
  try {
8
22
  const body = await readBoundedResponseBody(response.clone(), {
9
- maxBytes: 1024,
10
- totalTimeoutMs: 1000,
11
- firstByteTimeoutMs: 1000,
12
- inactivityTimeoutMs: 1000,
23
+ maxBytes: raw ? 4096 : 1024,
24
+ totalTimeoutMs: raw ? 2000 : 1000,
25
+ firstByteTimeoutMs: raw ? 2000 : 1000,
26
+ inactivityTimeoutMs: raw ? 2000 : 1000,
13
27
  signal,
14
28
  });
15
29
  return !signal?.aborted && body.displaySafe && !body.truncated && !body.timedOut && !body.oversized
16
- && body.text.startsWith(`${ANTIGRAVITY_VALIDATION_REQUIRED_PREFIX}: `);
30
+ && hasAntigravityValidationRefusalMarker(raw ? safeAntigravityHttpErrorMessage(403, body.text) : body.text);
17
31
  } catch {
18
32
  return false;
19
33
  }
@@ -1,3 +1,6 @@
1
+ import { createPhysicalSendReporter } from "../../lib/request-execution-budget";
2
+ import { createInferenceSendBudget } from "../inference/context";
3
+ import { unboundPoolSpendRefusalResponse } from "../workflow-refusal";
1
4
  import { capturePoolQuotaWriter } from "../../codex/account-store";
2
5
  import { previewXaiOauthWireModel } from "./core-normalize";
3
6
  import {
@@ -722,6 +725,7 @@ export async function handleResponsesCompact(
722
725
  logCtx.provider = route.codexAccountNamespace
723
726
  ? `${route.providerName}-${route.codexAccountNamespace}`
724
727
  : route.providerName;
728
+ logCtx.spendPoolId = route.providerName;
725
729
  logCtx.providerAdapter = route.provider.adapter;
726
730
  // #4940, and the same refusal the ordinary Responses path makes in request-prepare.ts. Compact has
727
731
  // to repeat it rather than inherit it: the native branch below dispatches straight to
@@ -804,7 +808,7 @@ export async function handleResponsesCompact(
804
808
  // failure that hands off, continues here. The routed turn used to call handleResponses with
805
809
  // no budget at all, so `handleResponsesInner` minted a fresh four after the native attempt
806
810
  // had already spent some of the first one.
807
- const sendBudget: RequestExecutionBudget = options.sendBudget ?? createRequestExecutionBudget();
811
+ const sendBudget: RequestExecutionBudget = options.sendBudget ?? createInferenceSendBudget(req, logCtx);
808
812
  // A manual override onto another backend must not mint ciphertext the conversation model cannot replay.
809
813
  const manualOverrideCrossesProvider = options.compactionRoutingOverride
810
814
  ? !compactionRoutingKeepsProviderIdentity(config, options.compactionRoutingOverride, route)
@@ -1022,12 +1026,21 @@ export async function handleResponsesCompact(
1022
1026
  // `kind === "pool"` and a main-pool credential left it false -- so 401 then 429 really did
1023
1027
  // reach five. The single sends spend the base allowance first and then the one shared
1024
1028
  // final-recovery reserve, which is the same rule the Responses path follows.
1025
- const sendSingleCompactAttempt = (
1029
+ const sendSingleCompactAttempt = async (
1026
1030
  doFetch: () => Promise<Response>,
1027
1031
  ): Promise<Response> => {
1028
1032
  if (sendBudget.remainingBaseSends(TRANSIENT_RETRY_MAX_ATTEMPTS) > 0) {
1029
- sendBudget.used += 1;
1030
- return doFetch();
1033
+ const report = createPhysicalSendReporter(sendBudget, () => ({ poolId: logCtx.spendPoolId ?? route.providerName,
1034
+ identityId: logCtx.accountLogLabel }));
1035
+ let started = false;
1036
+ try {
1037
+ if (report.beforeSend?.() === false) throw new SendBudgetExhaustedError(safeHostLabel(compactUrl));
1038
+ started = true;
1039
+ return await doFetch();
1040
+ } finally {
1041
+ try { if (started) report(1); }
1042
+ finally { report.close?.(); }
1043
+ }
1031
1044
  }
1032
1045
  const decision = sendBudget.reserveDispatch({
1033
1046
  sendClass: "auth-recovery",
@@ -1076,7 +1089,8 @@ export async function handleResponsesCompact(
1076
1089
  // Draws the shared remainder instead of a fresh three. Compact is a native endpoint
1077
1090
  // of the same logical turn, so its sends belong to the same cap.
1078
1091
  attempts: sendBudget.remainingBaseSends(TRANSIENT_RETRY_MAX_ATTEMPTS),
1079
- onSendsConsumed: (used: number) => { sendBudget.used += Math.max(0, used); },
1092
+ onSendsConsumed: createPhysicalSendReporter(sendBudget, () => ({ poolId: logCtx.spendPoolId ?? route.providerName,
1093
+ identityId: logCtx.accountLogLabel })),
1080
1094
  });
1081
1095
  };
1082
1096
 
@@ -1084,9 +1098,12 @@ export async function handleResponsesCompact(
1084
1098
  // actually happens, so every recorder call names the context that produced it.
1085
1099
  let outcomeCtx = authCtx;
1086
1100
  const localDispatchRefusal = (error: unknown): Response | undefined => {
1087
- const response = mapCodexAuthContextErrorToResponse(unwrapUpstreamRetryEvidenceError(error), {
1088
- now: Date.now(), accountSelector: route.codexAccountNamespace,
1089
- });
1101
+ const cause = unwrapUpstreamRetryEvidenceError(error);
1102
+ const response = cause instanceof SendBudgetExhaustedError
1103
+ ? unboundPoolSpendRefusalResponse(logCtx) ?? formatErrorResponse(429, "request_send_budget_exhausted", cause.message)
1104
+ : mapCodexAuthContextErrorToResponse(cause, {
1105
+ now: Date.now(), accountSelector: route.codexAccountNamespace,
1106
+ });
1090
1107
  if (response) {
1091
1108
  releaseUpstreamHostAdmission(compactHostAdmissionLease);
1092
1109
  compactHostAdmissionLease = null;
@@ -47,7 +47,7 @@ function settlePhysicalSends(log: RequestLogContext, budget: RequestExecutionBud
47
47
  // An external report may settle a prepaid booking without changing used. Its explicit
48
48
  // receipt outranks the numeric delta, or the same source send would be charged twice.
49
49
  const unreported = Math.max(0, sent - Math.max(reported, Math.max(0, budget.used - beforeUsed)));
50
- if (unreported > 0) budget.used += unreported;
50
+ if (unreported > 0 && !budget.spendEnforced) budget.used += unreported;
51
51
  return sent;
52
52
  }
53
53
 
@@ -18,7 +18,7 @@ import type { OcxConfig } from "../../types";
18
18
  import type { RouteResult } from "../../router";
19
19
  import type { DataPlaneAdmission } from "../auth-cors";
20
20
  import type { AdmissionLease } from "../../lib/admission";
21
- import type { RequestExecutionBudget } from "../../lib/request-execution-budget";
21
+ import type { RequestExecutionBudget, SingleUseDispatchPermit } from "../../lib/request-execution-budget";
22
22
  import type { TransientSendBudget } from "../../lib/upstream-retry";
23
23
  import type { ResponsesTerminalStatus } from "../../bridge";
24
24
  import type { NativeChatFinishLog } from "../chat-native";
@@ -62,6 +62,7 @@ export interface NativeComboChildRun {
62
62
  childLog: RequestLogContext;
63
63
  attemptHandle: InferenceAttempt;
64
64
  sendBudget: RequestExecutionBudget;
65
+ comboDispatchPermit?: SingleUseDispatchPermit;
65
66
  turnAdmissionLease?: AdmissionLease;
66
67
  onFirstOutput: () => void;
67
68
  /** Reports to the combo's child callbacks; it never writes the parent's final row itself. */
@@ -272,6 +273,7 @@ export interface ComboChildCallbacks {
272
273
  export async function dispatchNativeComboChild(input: {
273
274
  source: ComboProtocolSource;
274
275
  plan: NativeComboChildPlan;
276
+ comboDispatchPermit?: SingleUseDispatchPermit;
275
277
  logCtx: RequestLogContext;
276
278
  childLog: RequestLogContext;
277
279
  attempt: PersistedUsageAttempt;
@@ -317,6 +319,7 @@ export async function dispatchNativeComboChild(input: {
317
319
  childLog,
318
320
  attemptHandle,
319
321
  sendBudget: plan.sendBudget,
322
+ comboDispatchPermit: input.comboDispatchPermit,
320
323
  ...(input.turnAdmissionLease ? { turnAdmissionLease: input.turnAdmissionLease } : {}),
321
324
  onFirstOutput: () => {
322
325
  outputSeen = true;
@@ -52,11 +52,10 @@ import { routeConcreteModel, comboRouteDecisionTrace } from "../../router";
52
52
  import { memoryModelRouteReason } from "./memory-models";
53
53
  import { poolAccountProviderLabel } from "../../providers/label";
54
54
  import { getAccountSet } from "../../oauth/store";
55
+ import { configuredAnthropicInstance } from "../../providers/anthropic-instance";
55
56
  import {
57
+ anthropicRoutingFor,
56
58
  formatAnthropicProviderForLog,
57
- getAnthropicAccountHealthSnapshot,
58
- getAnthropicPoolRetryAfterSeconds,
59
- getEligibleAnthropicAccounts,
60
59
  } from "../../oauth/anthropic-routing";
61
60
  import { codexAccountLogLabel } from "../../codex/account-label";
62
61
  import { codexQuotaScopeForModel, getCodexQuotaHealthSnapshot } from "../../codex/routing";
@@ -118,10 +117,11 @@ export const COMBO_TARGET_BASE_SENDS = CODEX_TEXT_GUARDED_BUDGET_POLICY.baseSend
118
117
 
119
118
  function cooledPoolAccountLabel(config: OcxConfig, providerName: string, modelId: string, label: string | undefined): string | undefined {
120
119
  if (!label) return undefined;
121
- if (providerName === "anthropic") {
122
- const matches = getAccountSet("anthropic")?.accounts.filter(account =>
123
- formatAnthropicProviderForLog("anthropic", account.id) === label) ?? [];
124
- return matches.length === 1 && getAnthropicAccountHealthSnapshot(matches[0]!.id) ? label : undefined;
120
+ const instance = configuredAnthropicInstance(config, providerName);
121
+ if (instance && config.providers[providerName]?.authMode === "oauth") {
122
+ const matches = getAccountSet(instance)?.accounts.filter(account =>
123
+ formatAnthropicProviderForLog(instance, account.id) === label) ?? [];
124
+ return matches.length === 1 && anthropicRoutingFor(instance).getAnthropicAccountHealthSnapshot(matches[0]!.id) ? label : undefined;
125
125
  }
126
126
  const provider = config.providers[providerName];
127
127
  if (!provider || !isCanonicalOpenAiForwardProvider(provider)) return undefined;
@@ -152,12 +152,13 @@ export function isAnthropicPoolLocalRefusal(
152
152
  failedAccount: string | undefined,
153
153
  now = Date.now(),
154
154
  ): boolean {
155
- return providerName === "anthropic"
155
+ const instance = configuredAnthropicInstance(config, providerName);
156
+ return instance !== undefined
156
157
  && config.providers[providerName]?.authMode === "oauth"
157
158
  && status === 429
158
159
  && failedAccount === undefined
159
- && getEligibleAnthropicAccounts(now).length === 0
160
- && getAnthropicPoolRetryAfterSeconds(now) !== null;
160
+ && anthropicRoutingFor(instance).getEligibleAnthropicAccounts(now).length === 0
161
+ && anthropicRoutingFor(instance).getAnthropicPoolRetryAfterSeconds(now) !== null;
161
162
  }
162
163
 
163
164
  /**
@@ -713,6 +714,9 @@ export async function executeComboResponses(
713
714
  while (pick) {
714
715
  if (options.abortSignal?.aborted) return clientCancelledResponse();
715
716
  const firstComboTarget = comboTargetsDispatched === 0;
717
+ const targetRoute = routeConcreteModel(config, `${pick.target.provider}/${pick.target.model}`);
718
+ // The inherited spend tracker observes this parent log, before the child has its own label.
719
+ logCtx.spendPoolId = targetRoute.providerName;
716
720
  // The first target seeds the ledger's target identity and charges nothing; every later one
717
721
  // is a real transition, refused once the declared hops, the alternate-target ledger or the
718
722
  // request total are spent. `countedExternally` is required: the child charges its own
@@ -744,7 +748,6 @@ export async function executeComboResponses(
744
748
  ...(logCtx.conversationId ? { conversationId: logCtx.conversationId } : {}),
745
749
  ...(logCtx.surface ? { surface: logCtx.surface } : {}),
746
750
  };
747
- const targetRoute = routeConcreteModel(config, `${pick.target.provider}/${pick.target.model}`);
748
751
  const targetReasoningEfforts = supportedLadderFor({
749
752
  provider: targetRoute.provider,
750
753
  modelId: targetRoute.modelId,
@@ -861,6 +864,7 @@ export async function executeComboResponses(
861
864
  response = nativeChild ? await dispatchNativeComboChild({
862
865
  source: options.protocolSource!,
863
866
  plan: nativeChild,
867
+ comboDispatchPermit: hopDecision?.allowed ? hopDecision.permit : undefined,
864
868
  logCtx,
865
869
  childLog,
866
870
  attempt,
@@ -886,6 +890,7 @@ export async function executeComboResponses(
886
890
  // parent arrived with.
887
891
  sendBudget: targetSendBudget,
888
892
  comboAttempt: true,
893
+ comboDispatchPermit: hopDecision?.allowed ? hopDecision.permit : undefined,
889
894
  comboReplaySnapshot,
890
895
  deferCodexResetDerivedCooldown,
891
896
  // Attempt-relative TTFT is recorded HERE (not via childLog.firstOutputMs — a later
@@ -210,6 +210,7 @@ export async function applyFinalRouteRequestNormalization(args: {
210
210
  if (preserveAnthropicResponseModel) parsed._responseModelId = responseModelId;
211
211
  logCtx.model = virtualModel?.selectedModelId ?? route.modelId;
212
212
  logCtx.provider = route.providerName;
213
+ logCtx.spendPoolId = route.providerName;
213
214
  logCtx.providerAdapter = route.provider.adapter;
214
215
  logCtx.routeDecision = route.routeDecision;
215
216
  logCtx.policyEligibility = route.policyEligibility;
@@ -71,7 +71,7 @@ export interface HandleResponsesOptions {
71
71
  compactionRecoveryKind?: "compaction-v1" | "compaction-v2";
72
72
  onCompactionRecoveryRoute?: (route: RouteResult) => void;
73
73
  onCompactionRecoveryAdapterEvent?: (event: AdapterEvent) => void;
74
- /** Physical-send reports already delivered to the shared used setter, including booking settlement. */
74
+ /** Physical-send reports already delivered to the shared ledger, including exact booking settlement. */
75
75
  onCompactionRecoverySendsReported?: (count: number) => void;
76
76
  /** Private holder for the Kiro serving-account lease. */
77
77
  accountLoad?: { lease: AccountLease | null; cancelled: boolean };
@@ -158,6 +158,8 @@ export interface HandleResponsesOptions {
158
158
  callerDirectAuth?: CallerDirectAuth | null;
159
159
  /** Internal recursion guard; callers outside this module must not set it. */
160
160
  comboAttempt?: boolean;
161
+ /** Exact externally booked combo hop, used for its child's spend preflight and send reports. */
162
+ comboDispatchPermit?: SingleUseDispatchPermit;
161
163
  /** Internal handoff: this combo was selected by shadow-call interception. */
162
164
  shadowCallIntercepted?: boolean;
163
165
  /** Internal handoff: the memory phase this turn belongs to, so combo children keep its routing. */
@@ -37,6 +37,11 @@ export function adapterNeedsForcedContinuation(name: string): boolean {
37
37
  return name === "kiro" || name === "cursor";
38
38
  }
39
39
 
40
+ /** Adapters opting into restricted store:false replay for pending client function calls. */
41
+ export function adapterNeedsToolCallContinuation(name: string): boolean {
42
+ return name === "qoder";
43
+ }
44
+
40
45
 
41
46
  export type ContinuationOwnerRead =
42
47
  | { kind: "missing" }