@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";
@@ -24,20 +24,22 @@ import {
24
24
  isGenericOAuthFailoverEnabled,
25
25
  rotateGenericOAuthAccountOn429,
26
26
  rotateGenericOAuthAccountOnRefusal,
27
+ rotateAntigravityAccountOnAuthRefusal,
27
28
  quarantineKiroSuspendedAccount,
28
29
  failoverAccountSnapshot,
29
30
  } from "../../oauth/generic-account-failover";
30
31
  import { classifyKiroRefusal } from "../../adapters/kiro-refusal";
32
+ import { isNonReplayableResponse } from "../../lib/upstream-retry";
31
33
  import { noteKiroMonthlyRefusal, noteKiroServedSuccess } from "../../providers/kiro-usage";
32
34
  import { persistKiroAccountState } from "../../providers/kiro-account-state-disk";
33
35
  import { readDisplaySafeErrorText } from "./core-errors";
34
36
  import {
35
37
  ANTHROPIC_POOL_MAX_FAILOVERS_PER_REQUEST,
36
- getAnthropicPoolAccessSnapshot,
38
+ anthropicRoutingFor,
37
39
  formatAnthropicProviderForLog,
38
40
  } from "../../oauth/anthropic-routing";
39
41
  import { resolveWireProtocolOverride } from "../adapter-resolve";
40
- import { bindRouteReasoningReplayScope, adapterNeedsForcedContinuation } from "./core-replay";
42
+ import { bindRouteReasoningReplayScope, adapterNeedsForcedContinuation, adapterNeedsToolCallContinuation } from "./core-replay";
41
43
  import { namespacedToolName } from "../../types";
42
44
  import { providerFetch } from "./fetch-helpers";
43
45
  import type { AttemptRecoveryKind } from "../../usage/log";
@@ -45,6 +47,7 @@ import { recordAdapterReasoning, recordAdapterTier } from "../request-log";
45
47
  import { normalizeLogConversationId } from "../request-log-conversation";
46
48
  import { rememberResponseState } from "../../responses/state";
47
49
  import { trackStreamLifetime } from "../lifecycle";
50
+ import { isAntigravityRawValidationRefusal } from "./antigravity-validation-refusal";
48
51
 
49
52
  /** One responsibility of the Responses request pipeline; state owners are explicit. */
50
53
  export async function executeResponsesSidecars(
@@ -67,6 +70,8 @@ export async function executeResponsesSidecars(
67
70
  | "genericFailoverLimit"
68
71
  | "replayOAuthCredentialSnapshot"
69
72
  | "applyFailoverSnapshot"
73
+ | "anthropicInstance"
74
+ | "currentAnthropicRouteDecision"
70
75
  | "anthropicRouteDecision"
71
76
  | "anthropicPoolAccountId"
72
77
  | "anthropicPoolFailovers"
@@ -85,11 +90,15 @@ export async function executeResponsesSidecars(
85
90
  | "notifyResponseComplete"
86
91
  | "cancelResponseCompletion"
87
92
  >,
88
- sendBudgetState: Pick<ResponsesSendBudget, "reserveCredentialHop">,
93
+ sendBudgetState: Pick<ResponsesSendBudget, "reserveCredentialHop" | "refundableAdapterDispatchBudget" | "pendingHopPermit" | "noteAdapterPhysicalSend">,
89
94
  ) {
90
95
  const { config, options, logCtx } = requestContext;
96
+ const antigravityPoolActivated = requestState.route.providerName === "google-antigravity"
97
+ && isGenericOAuthFailoverEnabled(config, requestState.route.providerName);
91
98
  const {
92
99
  applyFailoverSnapshot,
100
+ anthropicInstance,
101
+ currentAnthropicRouteDecision,
93
102
  anthropicSessionKey,
94
103
  commitResolvedOAuthSelection,
95
104
  resolveSelectionAdapter,
@@ -182,13 +191,29 @@ export async function executeResponsesSidecars(
182
191
  || (logCtx.activeAttempt?.deliverySummary?.semanticBytes ?? 0) > 0
183
192
  || (logCtx.activeAttempt?.deliverySummary?.sideEffectEvents ?? 0) > 0;
184
193
  const noteSidecarOutput = () => { sidecarOutputStarted = true; options.onFirstOutput?.(); };
194
+ // A single request may rotate once for a verification refusal. Keep this separate from the
195
+ // broader OAuth failover count: a sidecar turn can also spend that count on rate-limit recovery.
196
+ let antigravityValidationRotationAttempted = false;
185
197
  const rotateSidecarProviderOn429 = async (
186
198
  retryAfter: string | null,
187
199
  responseHeaders?: Headers,
188
200
  retryParsed?: OcxParsedRequest,
189
201
  originalResponse?: Response,
190
202
  ): Promise<{ adapter: ProviderAdapter; recoveryKind: AttemptRecoveryKind } | null> => {
191
- if (route.providerName !== "kiro" && !(route.providerName === "anthropic" && originalResponse?.status === 403) && originalResponse && originalResponse.status !== 429) return null;
203
+ const antigravityValidationResponse = canRunWebSearch && route.providerName === "google-antigravity"
204
+ && route.provider.authMode === "oauth" && originalResponse?.status === 403;
205
+ if (route.providerName !== "kiro" && !(anthropicInstance && originalResponse?.status === 403)
206
+ && !antigravityValidationResponse && originalResponse && originalResponse.status !== 429) return null;
207
+ let antigravityVerification = false;
208
+ if (antigravityValidationResponse) {
209
+ if (transportState.adapter.name !== "google" || !antigravityPoolActivated
210
+ || options.abortSignal?.aborted || isNonReplayableResponse(originalResponse)
211
+ || antigravityValidationRotationAttempted
212
+ || transportState.genericFailovers >= transportState.genericFailoverLimit
213
+ || sidecarHasCommittedOutput()) return null;
214
+ antigravityVerification = await isAntigravityRawValidationRefusal(originalResponse, options.abortSignal);
215
+ if (!antigravityVerification) return null;
216
+ }
192
217
  const refusal = route.providerName === "kiro" && originalResponse
193
218
  ? classifyKiroRefusal(originalResponse.status,
194
219
  await readDisplaySafeErrorText(originalResponse.clone(), options.abortSignal ?? new AbortController().signal, "")).kind
@@ -205,6 +230,42 @@ export async function executeResponsesSidecars(
205
230
  // sidecar loops used to flatten all three to `key-429`, so an account rotation read as a key
206
231
  // rotation in the attempt row and in the Logs UI.
207
232
  let recoveryKind: AttemptRecoveryKind = "key-429";
233
+ if (antigravityVerification) {
234
+ const sent = transportState.replayOAuthCredentialSnapshot;
235
+ const failedAccountId = transportState.genericFailoverAccountId;
236
+ if (!sent || !failedAccountId || sent.accountId !== failedAccountId) return null;
237
+ if (options.abortSignal?.aborted || sidecarHasCommittedOutput()) return null;
238
+
239
+ const hop = reserveCredentialHop(
240
+ "auth-recovery",
241
+ `${route.providerName}|${route.modelId}|sidecar-oauth-verify`,
242
+ );
243
+ if (!hop.allowed) return null;
244
+ antigravityValidationRotationAttempted = true;
245
+ const nextAccountId = rotateAntigravityAccountOnAuthRefusal(
246
+ antigravityPoolActivated, sent.accountId, sent.generation, route.modelId,
247
+ );
248
+ if (!nextAccountId) {
249
+ hop.permit?.release();
250
+ return null;
251
+ }
252
+ try {
253
+ // Keep the sibling's bearer and its account-matched project together; never inherit the
254
+ // failed account's identity or project.
255
+ const snapshot = await failoverAccountSnapshot(route.providerName, nextAccountId);
256
+ if (options.abortSignal?.aborted || !await applyFailoverSnapshot(snapshot, retryParsed)
257
+ || transportState.replayOAuthCredentialSnapshot?.accountId !== nextAccountId) {
258
+ hop.permit?.release();
259
+ return null;
260
+ }
261
+ transportState.genericFailovers += 1;
262
+ } catch {
263
+ hop.permit?.release();
264
+ return null;
265
+ }
266
+ recoveryKind = "oauth-account-403";
267
+ sendBudgetState.pendingHopPermit = hop.permit;
268
+ }
208
269
  const rotated = !originalResponse || originalResponse.status === 429
209
270
  ? rotateProviderTransportOn429(config, route.providerName, route.provider, {
210
271
  retryAfter,
@@ -212,7 +273,9 @@ export async function executeResponsesSidecars(
212
273
  attemptedKey: route.provider.apiKey,
213
274
  promptCacheKey: parsed.options.promptCacheKey,
214
275
  }) : null;
215
- if (rotated) {
276
+ if (antigravityVerification) {
277
+ // `applyFailoverSnapshot` has already rebound the route to the sibling identity/project.
278
+ } else if (rotated) {
216
279
  route.provider = rotated;
217
280
  } else if (
218
281
  // A POSITIVE gate, not an early return. An early `return null` here made every later arm
@@ -225,7 +288,8 @@ export async function executeResponsesSidecars(
225
288
  ) {
226
289
  // Intersection with the request's shared budget. The sidecar replay is dispatched by the
227
290
  // web-search/image loop and never reaches `onSendsConsumed`, so this reservation is the
228
- // charge; a refusal returns null and the caller keeps the real 429 it already has.
291
+ // charge for other sidecars. Antigravity web search hands its reservation to the adapter
292
+ // physical-send owner instead; a refusal preserves the real 429.
229
293
  const hop = reserveCredentialHop(
230
294
  "auth-recovery",
231
295
  `${route.providerName}|${route.modelId}|sidecar-oauth-429`,
@@ -259,12 +323,13 @@ export async function executeResponsesSidecars(
259
323
  return null;
260
324
  }
261
325
  recoveryKind = "oauth-account-429";
262
- hop.permit?.use();
326
+ if (canRunWebSearch && route.providerName === "google-antigravity") sendBudgetState.pendingHopPermit = hop.permit;
327
+ else hop.permit?.use();
263
328
  } else if (
264
329
  // Anthropic's pool is excluded from generic failover, so without this arm a 429 inside a
265
330
  // web-search or image-bridge turn was terminal even with the pool fully enabled -- while
266
331
  // the very same 429 on the main response path rotated.
267
- transportState.anthropicPoolAccountId
332
+ anthropicInstance && transportState.anthropicPoolAccountId
268
333
  ) {
269
334
  // Same intersection for the Anthropic roster: its own per-request bound still applies,
270
335
  // and the shared budget decides whether this request may spend another send at all.
@@ -272,10 +337,10 @@ export async function executeResponsesSidecars(
272
337
  "auth-recovery",
273
338
  `${route.providerName}|${route.modelId}|sidecar-anthropic-429`,
274
339
  );
275
- const nextAccountId = await rotateAnthropicAccountOnResponse(
340
+ const nextAccountId = await rotateAnthropicAccountOnResponseForInstance(anthropicInstance,
276
341
  originalResponse ?? new Response(null, { status: 429, headers: responseHeaders ?? (retryAfter ? { "retry-after": retryAfter } : undefined) }), {
277
342
  config, accountId: transportState.anthropicPoolAccountId, sessionKey: anthropicSessionKey,
278
- model: route.modelId, requestKey: transportState, decision: transportState.anthropicRouteDecision, signal: options.abortSignal,
343
+ model: route.modelId, requestKey: transportState, decision: transportState.anthropicRouteDecision, currentDecision: currentAnthropicRouteDecision, signal: options.abortSignal,
279
344
  allow429Recovery: !sidecarHasCommittedOutput(), allowAccountRefusal: !sidecarHasCommittedOutput(),
280
345
  canRetry: hop.allowed && transportState.anthropicPoolFailovers < ANTHROPIC_POOL_MAX_FAILOVERS_PER_REQUEST,
281
346
  });
@@ -289,12 +354,12 @@ export async function executeResponsesSidecars(
289
354
  // carries none, and getAnthropicPoolAccessToken is what enforces its fail-closed
290
355
  // local-cli credential rule. Both existing Anthropic rotation sites apply the token the
291
356
  // same way.
292
- const admitted = await commitResolvedOAuthSelection(await getAnthropicPoolAccessSnapshot(nextAccountId));
357
+ const admitted = await commitResolvedOAuthSelection(await anthropicRoutingFor(anthropicInstance).getAnthropicPoolAccessSnapshot(nextAccountId));
293
358
  if (!admitted) throw new Error("OAuth selection changed during recovery");
294
359
  transportState.anthropicPoolAccountId = admitted.accountId;
295
360
  transportState.anthropicPoolFailovers += 1;
296
361
  route.provider = { ...route.provider, apiKey: admitted.accessToken };
297
- logCtx.provider = formatAnthropicProviderForLog("anthropic", admitted.accountId, config);
362
+ logCtx.provider = formatAnthropicProviderForLog(anthropicInstance, admitted.accountId, config);
298
363
  } catch {
299
364
  hop.permit?.release();
300
365
  return null;
@@ -321,6 +386,7 @@ export async function executeResponsesSidecars(
321
386
  providerName: route.providerName,
322
387
  provider: route.provider,
323
388
  adapterName: rotatedAdapter.name,
389
+ ...(antigravityVerification ? { oauthCredentialSnapshot: transportState.replayOAuthCredentialSnapshot } : {}),
324
390
  });
325
391
  }
326
392
  bindRouteReasoningReplayScope({
@@ -328,6 +394,7 @@ export async function executeResponsesSidecars(
328
394
  providerName: route.providerName,
329
395
  provider: route.provider,
330
396
  adapterName: rotatedAdapter.name,
397
+ ...(antigravityVerification ? { oauthCredentialSnapshot: transportState.replayOAuthCredentialSnapshot } : {}),
331
398
  });
332
399
  return { adapter: rotatedAdapter, recoveryKind };
333
400
  };
@@ -440,7 +507,10 @@ export async function executeResponsesSidecars(
440
507
  parsed._rawBody,
441
508
  response,
442
509
  continuationStateForResponse(providerState),
443
- responseStateOptions(adapterNeedsForcedContinuation(transportState.adapter.name)),
510
+ {
511
+ ...responseStateOptions(adapterNeedsForcedContinuation(transportState.adapter.name)),
512
+ retainForToolContinuation: adapterNeedsToolCallContinuation(transportState.adapter.name),
513
+ },
444
514
  );
445
515
  notifyResponseComplete(response);
446
516
  },
@@ -484,6 +554,11 @@ export async function executeResponsesSidecars(
484
554
  abortSignal: options.abortSignal,
485
555
  translatorBudget,
486
556
  providerFetch: routedProviderFetch,
557
+ ...(route.providerName === "google-antigravity" ? {
558
+ sendBudget: sendBudgetState.refundableAdapterDispatchBudget,
559
+ onPhysicalSend: (send: { ordinal: number; recovery?: AttemptRecoveryKind }) =>
560
+ sendBudgetState.noteAdapterPhysicalSend(logCtx.usageLogInputTokens, send),
561
+ } : {}),
487
562
  },
488
563
  backend: wsPlan.backend,
489
564
  forwardProvider: wsPlan.forwardSidecar?.provider,
@@ -516,6 +591,13 @@ export async function executeResponsesSidecars(
516
591
  stallTimeoutSec: wsPlan.stallTimeoutSec,
517
592
  streamRoutedModelOutput: wsPlan.streamRoutedModelOutput,
518
593
  on429: rotateSidecarProviderOn429,
594
+ ...(route.providerName === "google-antigravity" ? { onIterationEnd: () => {
595
+ // The next Antigravity adapter dispatch claims this permit. If request construction,
596
+ // admission or cancellation stopped it first, return the unused send allowance.
597
+ const permit = sendBudgetState.pendingHopPermit;
598
+ sendBudgetState.pendingHopPermit = undefined;
599
+ permit?.release();
600
+ } } : {}),
519
601
  retryOn429Policy: route.providerName === "kiro" && isGenericOAuthFailoverEnabled(config, "kiro")
520
602
  ? null : rateLimitRetryPolicyFor(route.provider),
521
603
  onCompletedResponse: response => {
@@ -703,6 +703,7 @@ function failedBlocks(name: string, newline: string): readonly string[] {
703
703
  * @param providerExecutedCallTypes - Call types executed by the provider.
704
704
  * @param declaredBare - Explicitly declared bare tool names without namespace provenance.
705
705
  * @param declaredCustom - Current bare custom declarations eligible for code-mode recovery.
706
+ * @param enforceDeclaredToolNames - Refuse undeclared calls; normalization remains active when false.
706
707
  * @returns An SSE block rewrite function.
707
708
  */
708
709
  export function createUndeclaredToolCallGuardBlockRewrite(
@@ -711,6 +712,7 @@ export function createUndeclaredToolCallGuardBlockRewrite(
711
712
  providerExecutedCallTypes: ProviderExecutedCallTypes = EMPTY_PROVIDER_EXECUTED_CALL_TYPES,
712
713
  declaredBare?: ReadonlySet<string>,
713
714
  declaredCustom?: ReadonlySet<string>,
715
+ enforceDeclaredToolNames = true,
714
716
  ): SseBlockRewrite {
715
717
  let tripped = false;
716
718
  return (block: string) => {
@@ -723,7 +725,9 @@ export function createUndeclaredToolCallGuardBlockRewrite(
723
725
  } catch {
724
726
  return [block];
725
727
  }
726
- const name = undeclaredToolCallName(parsed, declared, declaredNamelessClientCallTypes, providerExecutedCallTypes, declaredBare, declaredCustom);
728
+ const name = enforceDeclaredToolNames
729
+ ? undeclaredToolCallName(parsed, declared, declaredNamelessClientCallTypes, providerExecutedCallTypes, declaredBare, declaredCustom)
730
+ : undefined;
727
731
  if (name !== undefined) {
728
732
  tripped = true;
729
733
  return failedBlocks(name, block.includes("\r\n") ? "\r\n" : "\n");
@@ -1,3 +1,4 @@
1
+ import { sharedPoolContinuityDenial } from "../lib/spend-reservation-ledger";
1
2
  /**
2
3
  * The one place that knows how this proxy refuses a turn on its own workflow budget.
3
4
  *
@@ -96,6 +97,22 @@ export function workflowRefusalResponse(
96
97
  return refusal;
97
98
  }
98
99
 
100
+ /** Render the approved conservative-overlay explanation after a dispatch reservation refuses. */
101
+ export function unboundPoolSpendRefusalMessage(
102
+ logCtx: Pick<RequestLogContext, "spendRefusalDetail">,
103
+ ): string | undefined {
104
+ const detail = logCtx.spendRefusalDetail;
105
+ if (!detail?.includesUnboundPoolHistory) return undefined;
106
+ return workflowDenialSummary("workflow-spend-exhausted", detail).message;
107
+ }
108
+
109
+ /** HTTP form for pre-commit send-budget catches; the tracker already recorded the event. */
110
+ export function unboundPoolSpendRefusalResponse(logCtx: RequestLogContext): Response | undefined {
111
+ const detail = logCtx.spendRefusalDetail;
112
+ if (!detail?.includesUnboundPoolHistory) return undefined;
113
+ return workflowRefusalResponse("workflow-spend-exhausted", logCtx, undefined, undefined, detail);
114
+ }
115
+
99
116
  /**
100
117
  * Admit one HTTP turn against its root workflow budget.
101
118
  *
@@ -104,8 +121,20 @@ export function workflowRefusalResponse(
104
121
  * children. A request that names a parent is treated as that fan-out; a top-level request is
105
122
  * the conversation and may use the reserved slots.
106
123
  */
124
+ /** Read-only alias validation: keep replay corruption distinct from conflicting pool owners. */
125
+ export function poolContinuityRefusalReason(): WorkflowDenial | undefined {
126
+ const denial = sharedPoolContinuityDenial();
127
+ if (!denial) return undefined;
128
+ return denial.reason === "pool-history-unresolved" ? "workflow-pool-history-unresolved" : "workflow-spend-undurable";
129
+ }
130
+
107
131
  export function admitHttpWorkflowTurn(headers: Headers): WorkflowDecision | undefined {
108
132
  const rootId = headers.get("x-codex-parent-thread-id")?.trim() || undefined;
133
+ const continuityRefusal = poolContinuityRefusalReason();
134
+ if (continuityRefusal) {
135
+ recordWorkflowRefusalEvent(rootId, continuityRefusal);
136
+ return { admitted: false, reason: continuityRefusal, rootId: rootId ?? "" };
137
+ }
109
138
  const threadId = headers.get("thread-id")?.trim() || undefined;
110
139
  const lane: WorkflowLane = rootId !== undefined && threadId !== undefined && threadId !== rootId
111
140
  ? "worker"
@@ -132,6 +161,7 @@ export function workflowDecisionRefusalResponse(
132
161
  scope: decision.spendScope,
133
162
  limit: decision.spendLimit,
134
163
  ...(decision.spendProjected !== undefined ? { projected: decision.spendProjected } : {}),
164
+ ...(decision.spendIncludesUnboundPoolHistory ? { includesUnboundPoolHistory: true } : {}),
135
165
  }
136
166
  : undefined;
137
167
  return workflowRefusalResponse(decision.reason, logCtx, refusalLog, undefined, spend);
@@ -5,7 +5,7 @@ export interface OwnershipMutationLeaseOptions {
5
5
  readonly processAlive?: (pid: number) => boolean;
6
6
  readonly beforeRelease?: (lockPath: string) => void;
7
7
  readonly joinToken?: string;
8
- /** Names the current executable at the recorded PID, identity unverified; defaults to `tasklist` or `ps`. */
8
+ /** Names the current executable at the recorded PID, identity unverified; uses `ps` on POSIX and omits the default image lookup on Windows. */
9
9
  readonly processImage?: (pid: number) => string | null;
10
10
  }
11
11
 
@@ -112,13 +112,11 @@ function readIncompleteOwner(path) {
112
112
  /** The current executable at the recorded PID, identity unverified; null if unreadable within a second. */
113
113
  function processImage(pid) {
114
114
  try {
115
- if (process.platform === "win32") {
116
- const listed = spawnSync("tasklist", ["/FI", `PID eq ${pid}`, "/FO", "CSV", "/NH"], {
117
- encoding: "utf8", timeout: 1_000, windowsHide: true,
118
- });
119
- const match = /^"([^"]+)","(\d+)"/.exec((listed.stdout ?? "").trim());
120
- return listed.status === 0 && match && Number(match[2]) === pid ? match[1] : null;
121
- }
115
+ // This module also runs in the Node npm/pnpm launcher, before Bun is available.
116
+ // Without a kernel-backed system-directory resolver, omit the optional Windows
117
+ // image label rather than searching the caller's directory/PATH or trusting env roots.
118
+ // PID, liveness, age and lease ownership decisions do not depend on this label.
119
+ if (process.platform === "win32") return null;
122
120
  const listed = spawnSync("ps", ["-o", "comm=", "-p", String(pid)], { encoding: "utf8", timeout: 1_000 });
123
121
  const command = (listed.stdout ?? "").trim();
124
122
  return listed.status === 0 && command ? basename(command) : null;
@@ -0,0 +1,79 @@
1
+ import type { OcxConfig } from "../types";
2
+ import type { AnthropicInstanceId } from "../providers/anthropic-instance-id";
3
+ import { configuredAnthropicInstance } from "../providers/anthropic-instance";
4
+ import { anthropicRoutingFor } from "../oauth/anthropic-routing";
5
+ import { resolveAnthropicModelRouteForInstance, routeCandidates } from "../oauth/anthropic-model-routes";
6
+ import type { OAuthAccessSnapshot } from "../oauth";
7
+ import { AnthropicHelperUnavailableError } from "./auth";
8
+ import { captureAnthropicPhysicalSendOwnership, anthropicPhysicalSendOwnershipIsCurrent } from "../oauth/anthropic-send-ownership";
9
+ import { recordAnthropicAccountQuotaFromHeadersForInstance } from "../providers/quota/account-cache";
10
+ import { captureConfigGeneration } from "../lib/state-store-sweeper";
11
+ import { getAccountCredentialWithStatus } from "../oauth/store";
12
+ import { bindAnthropicRefusalCredentialForSend } from "../oauth/anthropic-account-refusal";
13
+
14
+ /** The one Messages URL builder shared by helper executors and the physical-send fence. */
15
+ export function anthropicHelperMessagesUrl(baseUrl: string): string {
16
+ return `${baseUrl.replace(/\/+$/, "").replace(/\/v1$/, "")}/v1/messages`;
17
+ }
18
+
19
+ /**
20
+ * Select the helper's account the way the legacy sidecar token path does: a read of the pool's
21
+ * selection for this model route, with no promotion. A helper never moves the active pointer and
22
+ * never consumes an operator's one-dispatch manual choice; the physical send is still fenced by
23
+ * the captured target/config, the account's credential generation and its send ownership.
24
+ */
25
+ export async function resolveAnthropicHelperSnapshot(config: OcxConfig, instance: AnthropicInstanceId, model: string): Promise<OAuthAccessSnapshot> {
26
+ const target = config.providers[instance]?.baseUrl;
27
+ const assertConfigured = () => {
28
+ const provider = config.providers[instance];
29
+ if (!provider || provider.disabled || provider.authMode !== "oauth" || provider.adapter !== "anthropic"
30
+ || configuredAnthropicInstance(config, instance) !== instance || provider.baseUrl !== target) {
31
+ throw new AnthropicHelperUnavailableError(instance);
32
+ }
33
+ };
34
+ assertConfigured();
35
+ const routing = anthropicRoutingFor(instance);
36
+ for (let attempt = 0; attempt < 3; attempt++) {
37
+ const route = resolveAnthropicModelRouteForInstance(instance, config, model);
38
+ if (route.error) throw new AnthropicHelperUnavailableError(instance);
39
+ const selection = routing.resolveAnthropicAccountForSession(null, config, Date.now(), route.decision, model);
40
+ if (!selection.accountId) throw new AnthropicHelperUnavailableError(instance);
41
+ const snapshot = await routing.getAnthropicPoolAccessSnapshot(selection.accountId);
42
+ assertConfigured();
43
+ const current = resolveAnthropicModelRouteForInstance(instance, config, model);
44
+ if (current.error || JSON.stringify(current.decision) !== JSON.stringify(route.decision)) continue;
45
+ if (snapshot.provider === instance && snapshot.accountId === selection.accountId) return snapshot;
46
+ }
47
+ throw new AnthropicHelperUnavailableError(instance);
48
+ }
49
+
50
+ /** One physical helper send; reset replays call this boundary again with the same snapshot. */
51
+ export async function fetchAnthropicHelper(
52
+ config: OcxConfig, snapshot: OAuthAccessSnapshot, model: string,
53
+ capturedTarget: string, url: string, init: RequestInit,
54
+ ): Promise<Response> {
55
+ const instance = snapshot.provider;
56
+ if (instance !== "anthropic" && instance !== "anthropic2") throw new Error("Invalid Anthropic helper instance");
57
+ const row = config.providers[instance];
58
+ const live = getAccountCredentialWithStatus(instance, snapshot.accountId);
59
+ if (!row || row.disabled || row.authMode !== "oauth" || row.adapter !== "anthropic"
60
+ || configuredAnthropicInstance(config, instance) !== instance || row.baseUrl !== capturedTarget
61
+ || url !== anthropicHelperMessagesUrl(capturedTarget) || !live || live.paused || live.needsReauth
62
+ || new Headers(init.headers).get("authorization") !== `Bearer ${snapshot.accessToken}`) {
63
+ throw new AnthropicHelperUnavailableError(instance);
64
+ }
65
+ const route = resolveAnthropicModelRouteForInstance(instance, config, model);
66
+ const routing = anthropicRoutingFor(instance);
67
+ if (route.error || !routeCandidates(routing.getEligibleAnthropicAccounts(Date.now(), model), route.decision).includes(snapshot.accountId)) {
68
+ throw new AnthropicHelperUnavailableError(instance);
69
+ }
70
+ const owner = captureAnthropicPhysicalSendOwnership(snapshot);
71
+ if (!owner) throw new AnthropicHelperUnavailableError(instance);
72
+ const writer = captureConfigGeneration();
73
+ const response = await fetch(url, init);
74
+ if (anthropicPhysicalSendOwnershipIsCurrent(owner)) {
75
+ recordAnthropicAccountQuotaFromHeadersForInstance(instance, snapshot.accountId, response.headers, writer, response.status, model);
76
+ bindAnthropicRefusalCredentialForSend(response, owner);
77
+ }
78
+ return response;
79
+ }
@@ -17,6 +17,73 @@ import { OPENAI_CODEX_PROVIDER_ID } from "../providers/openai-tiers";
17
17
  import { isCodexAccountUsable } from "../codex/account-usability";
18
18
  import { MAIN_CODEX_ACCOUNT_ID, isSelectableCodexPoolAccount } from "../codex/account-id";
19
19
  import { getAccountSet } from "../oauth/store";
20
+ import { configuredAnthropicInstance, type AnthropicInstanceId } from "../providers/anthropic-instance";
21
+ import { isAnthropicInstanceId } from "../providers/anthropic-instance-id";
22
+
23
+ /**
24
+ * The parent request's pool for an inheriting helper. A missing or disabled row keeps the parent's
25
+ * identity, so a pool removed mid-request refuses instead of discovering another pool; only a present
26
+ * custom row that is not the builtin instance (an unmarked custom `anthropic2`) stops inheritance.
27
+ */
28
+ export function inheritedAnthropicInstance(config: OcxConfig, parentProviderName: string | undefined): AnthropicInstanceId | undefined {
29
+ if (!isAnthropicInstanceId(parentProviderName)) return undefined;
30
+ const row = config.providers?.[parentProviderName];
31
+ if (!row || row.disabled === true) return parentProviderName;
32
+ return configuredAnthropicInstance(config, parentProviderName);
33
+ }
34
+
35
+ export class AnthropicHelperUnavailableError extends Error {
36
+ readonly code = "anthropic_helper_unavailable";
37
+ constructor(readonly instance: AnthropicInstanceId) {
38
+ super(`The selected ${instance} helper account pool is unavailable`);
39
+ this.name = "AnthropicHelperUnavailableError";
40
+ }
41
+ }
42
+ export type AnthropicHelperContext = {
43
+ backendFamily: string;
44
+ anthropicInstance?: AnthropicInstanceId;
45
+ parentProviderName?: string;
46
+ };
47
+
48
+ /**
49
+ * Availability of one selected pool: the configured builtin row plus at least one stored account the
50
+ * pool could select. Which account actually sends (model route, cooldown, manual choice) is decided
51
+ * by snapshot resolution at send time, exactly as the main request path does, so a paused or
52
+ * reauth-pending ACTIVE account does not make a pool with another usable account unavailable.
53
+ */
54
+ export function resolveAnthropicSidecarAuth(config: OcxConfig, instance: AnthropicInstanceId):
55
+ { anthropicProviderName: AnthropicInstanceId; anthropicProvider: OcxProviderConfig } | undefined {
56
+ const provider = config.providers[instance];
57
+ if (!provider || provider.disabled || provider.authMode !== "oauth" || provider.adapter !== "anthropic"
58
+ || configuredAnthropicInstance(config, instance) !== instance) return undefined;
59
+ if (!getAccountSet(instance)?.accounts.some(row => row.paused !== true && row.needsReauth !== true)) return undefined;
60
+ return { anthropicProviderName: instance, anthropicProvider: provider };
61
+ }
62
+
63
+ /**
64
+ * Explicit target, then the parent's CONFIGURED builtin instance. The parent name alone is not
65
+ * enough: a custom provider that happens to be named `anthropic2` is not Pool 2 and is never inherited.
66
+ */
67
+ export function resolveAnthropicHelperInstance(config: OcxConfig, context: AnthropicHelperContext): AnthropicInstanceId | undefined {
68
+ if (context.backendFamily !== "anthropic") return undefined;
69
+ const instance = context.anthropicInstance ?? inheritedAnthropicInstance(config, context.parentProviderName);
70
+ if (instance && !resolveAnthropicSidecarAuth(config, instance)) throw new AnthropicHelperUnavailableError(instance);
71
+ return instance;
72
+ }
73
+
74
+ /**
75
+ * Planner boundary for the typed refusal: an unavailable selected pool means "this helper does not
76
+ * run", never "fail the main request" and never "discover another pool". Other errors propagate.
77
+ */
78
+ export function withAnthropicHelperRefusal<T>(surface: string, resolve: () => T): T | undefined {
79
+ try {
80
+ return resolve();
81
+ } catch (error) {
82
+ if (!(error instanceof AnthropicHelperUnavailableError)) throw error;
83
+ console.warn(`[${surface}] ${error.message}; the request continues without this helper`);
84
+ return undefined;
85
+ }
86
+ }
20
87
 
21
88
  export interface SidecarAuthState {
22
89
  /** ChatGPT login usable: canonical forward provider AND a live stored credential. */
@@ -63,6 +130,10 @@ function findAnthropicAuthProvider(
63
130
  config: OcxConfig,
64
131
  ): { providerName: string; provider: OcxProviderConfig } | undefined {
65
132
  for (const [providerName, provider] of Object.entries(config.providers)) {
133
+ // Never adds Pool 2 automatically. The OAuth store key `anthropic2` belongs to the builtin pool:
134
+ // its token path and the token guardian refuse an unmarked row of that name, so offering such a
135
+ // custom row here would advertise credentials no helper executor will spend.
136
+ if (providerName === "anthropic2") continue;
66
137
  if (provider.disabled === true) continue;
67
138
  if (provider.adapter !== "anthropic" || provider.authMode !== "oauth") continue;
68
139
  const set = getAccountSet(providerName);
@@ -72,8 +143,11 @@ function findAnthropicAuthProvider(
72
143
  return undefined;
73
144
  }
74
145
 
75
- export function resolveSidecarAuth(config: OcxConfig): SidecarAuthState {
76
- const anthropic = findAnthropicAuthProvider(config);
146
+ export function resolveSidecarAuth(config: OcxConfig, instance?: AnthropicInstanceId): SidecarAuthState {
147
+ const exact = instance ? resolveAnthropicSidecarAuth(config, instance) : undefined;
148
+ if (instance && !exact) throw new AnthropicHelperUnavailableError(instance);
149
+ const anthropic = exact ? { providerName: exact.anthropicProviderName, provider: exact.anthropicProvider }
150
+ : findAnthropicAuthProvider(config);
77
151
  return {
78
152
  isCodexAuth: hasUsableCodexLogin(config),
79
153
  isAnthropicAuth: anthropic !== undefined,
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Anthropic OAuth account-pool settings. One shape, two locations:
3
+ * `config.anthropicAccountPool` for the `anthropic` instance and
4
+ * `config.providers.anthropic2.anthropicAccountPool` for `anthropic2`. Neither inherits from the other.
5
+ *
6
+ * Leaf module: `config.ts` and `provider.ts` import these types, never the reverse.
7
+ */
8
+ export type OcxAccountPoolRotationStrategy = "quota" | "round-robin" | "fill-first";
9
+
10
+ export type OcxAccountPoolQuotaWindow = "five-hour" | "weekly" | "max-utilization";
11
+
12
+ export interface AnthropicModelRoute {
13
+ name: string;
14
+ match: string;
15
+ accounts: string[];
16
+ fallback?: boolean;
17
+ }
18
+
19
+ export interface AnthropicAccountPoolConfig {
20
+ enabled?: boolean;
21
+ /** Preserve native Claude Messages while the pool is enabled. Default true; false selects legacy translation. */
22
+ nativeMessages?: boolean;
23
+ /** Usage % threshold for new-session auto-pick. Default 80. 0 = disabled (affinity/active only). */
24
+ autoSwitchThreshold?: number;
25
+ /** New-session rotation strategy. Default quota (today's behaviour). */
26
+ strategy?: OcxAccountPoolRotationStrategy;
27
+ /** Successful new-session binds retained on one round-robin selection. Default 1; range 1..100. */
28
+ stickyLimit?: number;
29
+ /** Usage window for quota-based scoring. Default "five-hour" (today's behaviour). */
30
+ quotaWindow?: OcxAccountPoolQuotaWindow;
31
+ /** Ordered model allowlists; inactive while the pool is disabled. Stored account IDs only. */
32
+ routes?: AnthropicModelRoute[];
33
+ }