@bitkyc08/opencodex 2.79.0 → 2.81.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (245) hide show
  1. package/README.md +3 -3
  2. package/gui/dist/assets/App-7yhoys8a.js +54 -0
  3. package/gui/dist/assets/Tray-EwAOsP0S.js +1 -0
  4. package/gui/dist/assets/index-RsoaAxXY.js +94 -0
  5. package/gui/dist/assets/quota-summary-D2lN8tDm.js +1 -0
  6. package/gui/dist/index.html +1 -1
  7. package/gui/dist/provider-icons/claude-green.svg +1 -0
  8. package/package.json +3 -2
  9. package/src/adapters/anthropic-model-contract.ts +8 -2
  10. package/src/adapters/anthropic.ts +7 -2
  11. package/src/adapters/codebuddy/mcp-server.ts +2 -25
  12. package/src/adapters/codebuddy/tool-bridge.ts +1 -7
  13. package/src/adapters/coding-agent/mcp-server.ts +33 -0
  14. package/src/adapters/coding-agent/protocol.ts +122 -17
  15. package/src/adapters/coding-agent/tool-catalog.ts +75 -0
  16. package/src/adapters/coding-agent/turn.ts +101 -44
  17. package/src/adapters/cursor/catalog.ts +8 -0
  18. package/src/adapters/cursor/effort-map.ts +2 -0
  19. package/src/adapters/cursor/live-transport.ts +5 -3
  20. package/src/adapters/cursor/native-exec-fs.ts +12 -9
  21. package/src/adapters/cursor/native-exec.ts +13 -5
  22. package/src/adapters/cursor/protobuf-request.ts +13 -7
  23. package/src/adapters/cursor/request-builder.ts +23 -10
  24. package/src/adapters/cursor/tool-definitions.ts +9 -6
  25. package/src/adapters/cursor/tool-guidance.ts +29 -8
  26. package/src/adapters/cursor/tool-naming.ts +40 -7
  27. package/src/adapters/cursor/tool-wording.ts +46 -0
  28. package/src/adapters/cursor.ts +3 -0
  29. package/src/adapters/devin/live-models.ts +2 -0
  30. package/src/adapters/devin.ts +25 -11
  31. package/src/adapters/kiro-retry.ts +9 -4
  32. package/src/adapters/openai-chat/messages.ts +7 -4
  33. package/src/adapters/physical-send.ts +8 -5
  34. package/src/adapters/qoder/adapter.ts +41 -6
  35. package/src/adapters/qoder/scaffold-guard.ts +2 -3
  36. package/src/adapters/zed.ts +16 -1
  37. package/src/claude/inbound-cache-stabilize.ts +4 -9
  38. package/src/claude/inbound.ts +10 -0
  39. package/src/claude/intercept/model-bindings.ts +1 -0
  40. package/src/claude/intercept/picker-listener.ts +71 -4
  41. package/src/cli/account-anthropic-threshold.ts +14 -5
  42. package/src/cli/account-api.ts +3 -1
  43. package/src/cli/account-extended.ts +13 -9
  44. package/src/cli/account-policy-dto.ts +1 -1
  45. package/src/cli/account-policy.ts +13 -4
  46. package/src/cli/account.ts +3 -3
  47. package/src/cli/capabilities-accounts.ts +2 -2
  48. package/src/cli/capabilities-agents-routing.ts +24 -0
  49. package/src/cli/capabilities-integrations.ts +16 -0
  50. package/src/cli/codex-shim-autorestore.ts +3 -0
  51. package/src/cli/dispatch.ts +10 -1
  52. package/src/cli/help.ts +4 -1
  53. package/src/cli/index.ts +11 -5
  54. package/src/cli/integration-plan-dto.ts +1 -0
  55. package/src/cli/integration-preview.ts +8 -1
  56. package/src/cli/integrations.ts +32 -3
  57. package/src/cli/message-args.ts +34 -0
  58. package/src/cli/message-command.ts +43 -0
  59. package/src/cli/message-runtime.ts +6 -0
  60. package/src/cli/observe.ts +3 -1
  61. package/src/cli/provider.ts +29 -6
  62. package/src/cli/registry.ts +24 -2
  63. package/src/clients/config-export/commandcode.ts +131 -0
  64. package/src/clients/config-export/contracts.ts +12 -0
  65. package/src/clients/config-export.ts +64 -1
  66. package/src/codex/auth-api/account-list.ts +11 -3
  67. package/src/codex/auth-api/main-account-probe.ts +25 -8
  68. package/src/codex/auth-api/pool-mode-gate.ts +33 -9
  69. package/src/codex/auth-api/pool-quota-probe.ts +15 -1
  70. package/src/codex/auth-api/routes.ts +1 -0
  71. package/src/codex/autostart-health.ts +3 -3
  72. package/src/codex/catalog/auto-review.ts +13 -6
  73. package/src/codex/catalog/build-entries.ts +15 -7
  74. package/src/codex/catalog/control-plane.ts +45 -0
  75. package/src/codex/catalog/effort.ts +11 -1
  76. package/src/codex/catalog/gather-capture.ts +5 -3
  77. package/src/codex/catalog/parsing.ts +2 -1
  78. package/src/codex/catalog/provider-models.ts +15 -9
  79. package/src/codex/control-plane-models.ts +6 -0
  80. package/src/codex/journal.ts +36 -5
  81. package/src/codex/shim-overlay.ts +9 -1
  82. package/src/combos/jev-decision-contract.ts +25 -2
  83. package/src/combos/jev.ts +6 -3
  84. package/src/combos/types.ts +1 -1
  85. package/src/config/diagnostics.ts +27 -16
  86. package/src/config/load-degrade.ts +34 -0
  87. package/src/config/schema/anthropic-account-pool.ts +143 -0
  88. package/src/config/schema/config-schema.ts +7 -1
  89. package/src/config/schema/leaf-validators.ts +4 -0
  90. package/src/config.ts +2 -0
  91. package/src/generated/compatibility-version.json +347 -199
  92. package/src/generated/model-metadata.ts +5 -4
  93. package/src/images/loop.ts +2 -1
  94. package/src/integrations/catalog-refresh.ts +1 -1
  95. package/src/integrations/mutation-plan.ts +7 -0
  96. package/src/integrations/registry.ts +8 -0
  97. package/src/integrations/state.ts +9 -2
  98. package/src/integrations/writer.ts +8 -2
  99. package/src/lib/lab-live-route-production.ts +9 -1
  100. package/src/lib/request-execution-budget.ts +236 -58
  101. package/src/lib/spend-pool-alias-validation.ts +79 -0
  102. package/src/lib/spend-pool-continuity.ts +34 -0
  103. package/src/lib/spend-reservation-ledger.ts +412 -50
  104. package/src/lib/state-store-registrations.ts +20 -3
  105. package/src/lib/upstream-retry.ts +17 -5
  106. package/src/lib/workflow-budget.ts +53 -11
  107. package/src/messaging/budget.ts +45 -0
  108. package/src/messaging/discovery.ts +57 -0
  109. package/src/messaging/envelope.ts +49 -0
  110. package/src/messaging/input.ts +37 -0
  111. package/src/messaging/rpc.ts +196 -0
  112. package/src/messaging/send.ts +58 -0
  113. package/src/messaging/socket.ts +72 -0
  114. package/src/messaging/types.ts +29 -0
  115. package/src/oauth/anthropic-account-refusal.ts +51 -17
  116. package/src/oauth/anthropic-account-threshold.ts +8 -2
  117. package/src/oauth/anthropic-continuity.ts +15 -1
  118. package/src/oauth/anthropic-model-quota.ts +120 -48
  119. package/src/oauth/anthropic-model-routes.ts +11 -3
  120. package/src/oauth/anthropic-oauth-definitions.ts +98 -0
  121. package/src/oauth/anthropic-pool-config.ts +30 -0
  122. package/src/oauth/anthropic-rate-limit-policy.ts +64 -9
  123. package/src/oauth/anthropic-routing.ts +1202 -866
  124. package/src/oauth/anthropic-send-ownership.ts +41 -0
  125. package/src/oauth/anthropic.ts +4 -1
  126. package/src/oauth/generic-account-failover.ts +3 -1
  127. package/src/oauth/health.ts +8 -6
  128. package/src/oauth/index.ts +90 -173
  129. package/src/oauth/kiro.ts +22 -9
  130. package/src/oauth/login-cli.ts +2 -0
  131. package/src/oauth/model-discovery-auth.ts +43 -0
  132. package/src/oauth/pool-kernel.ts +11 -5
  133. package/src/oauth/pool-settings-capability.ts +5 -3
  134. package/src/oauth/store-anthropic-instance.ts +87 -0
  135. package/src/oauth/store.ts +67 -9
  136. package/src/oauth/token-guardian.ts +2 -0
  137. package/src/oauth/types.ts +2 -0
  138. package/src/protocols/plan-snapshot.ts +5 -0
  139. package/src/protocols/settings.ts +20 -5
  140. package/src/providers/anthropic-instance-id.ts +47 -0
  141. package/src/providers/anthropic-instance.ts +48 -0
  142. package/src/providers/anthropic-reset-grant-ledger.ts +11 -1
  143. package/src/providers/derive.ts +2 -0
  144. package/src/providers/kiro-models.ts +3 -0
  145. package/src/providers/label.ts +1 -1
  146. package/src/providers/model-rename-fields.ts +2 -0
  147. package/src/providers/quota/account-cache.ts +37 -11
  148. package/src/providers/quota/anthropic-account-quota.ts +93 -0
  149. package/src/providers/quota/anthropic-cooldown-recovery.ts +222 -111
  150. package/src/providers/quota/report-cache.ts +24 -1
  151. package/src/providers/quota/vendor-probes-oauth.ts +46 -14
  152. package/src/providers/quota.ts +22 -32
  153. package/src/providers/registry/entries-core.ts +35 -30
  154. package/src/providers/registry/model-ids.ts +1 -0
  155. package/src/providers/registry/model-seeds.ts +7 -4
  156. package/src/providers/registry/types.ts +3 -0
  157. package/src/providers/registry.ts +3 -3
  158. package/src/responses/spill-store.ts +5 -1
  159. package/src/responses/state/metrics.ts +87 -0
  160. package/src/responses/state/snapshot-codec.ts +6 -0
  161. package/src/responses/state/spill-queue.ts +1 -0
  162. package/src/responses/state/unforced-store-false.ts +55 -0
  163. package/src/responses/state.ts +50 -66
  164. package/src/router.ts +28 -4
  165. package/src/routing/compatibility/assemble.ts +1 -1
  166. package/src/routing/quota.ts +7 -3
  167. package/src/server/auth-cors.ts +5 -0
  168. package/src/server/chat-completions.ts +6 -1
  169. package/src/server/chat-native.ts +45 -25
  170. package/src/server/claude-messages.ts +16 -2
  171. package/src/server/gui-static.ts +38 -4
  172. package/src/server/index/spend-ledger-lifecycle.ts +10 -4
  173. package/src/server/index.ts +16 -16
  174. package/src/server/inference/context.ts +1 -1
  175. package/src/server/lifecycle.ts +1 -1
  176. package/src/server/management/agent-settings-routes.ts +30 -6
  177. package/src/server/management/anthropic-account-threshold.ts +12 -9
  178. package/src/server/management/anthropic-pool-settings.ts +72 -0
  179. package/src/server/management/anthropic-reset-grant-routes.ts +98 -39
  180. package/src/server/management/config-routes.ts +27 -7
  181. package/src/server/management/decision-routes.ts +2 -1
  182. package/src/server/management/integration-routes.ts +25 -2
  183. package/src/server/management/oauth-account-routes.ts +79 -98
  184. package/src/server/management/provider-instance-ownership.ts +44 -0
  185. package/src/server/management/provider-routes.ts +21 -28
  186. package/src/server/management/route-registry.ts +3 -1
  187. package/src/server/management/routing-profile-routes.ts +4 -4
  188. package/src/server/management/system-restart.ts +44 -14
  189. package/src/server/management/system-routes.ts +24 -2
  190. package/src/server/management/vision-sidecar-options.ts +9 -6
  191. package/src/server/management/web-search-sidecar-options.ts +51 -2
  192. package/src/server/management-auth.ts +6 -0
  193. package/src/server/messages-native-eligibility.ts +3 -2
  194. package/src/server/messages-native-oauth.ts +86 -41
  195. package/src/server/messages-native-scope.ts +4 -1
  196. package/src/server/messages-native-selector.ts +39 -0
  197. package/src/server/messages-native.ts +84 -25
  198. package/src/server/request-log.ts +5 -0
  199. package/src/server/responses/adapter-continuation.ts +29 -17
  200. package/src/server/responses/adapter-dispatch.ts +47 -17
  201. package/src/server/responses/antigravity-validation-refusal.ts +20 -6
  202. package/src/server/responses/combo-requested-effort.ts +14 -0
  203. package/src/server/responses/compact.ts +25 -8
  204. package/src/server/responses/compaction-recovery.ts +1 -1
  205. package/src/server/responses/core-combo-native.ts +4 -1
  206. package/src/server/responses/core-combo.ts +21 -18
  207. package/src/server/responses/core-normalize.ts +1 -0
  208. package/src/server/responses/core-options.ts +3 -1
  209. package/src/server/responses/core-replay.ts +5 -0
  210. package/src/server/responses/passthrough-delivery.ts +7 -3
  211. package/src/server/responses/passthrough-dispatch.ts +21 -8
  212. package/src/server/responses/request-prepare.ts +37 -5
  213. package/src/server/responses/request-send-budget.ts +74 -37
  214. package/src/server/responses/request-spend.ts +325 -33
  215. package/src/server/responses/request-transport.ts +132 -52
  216. package/src/server/responses/run-turn-execution.ts +11 -3
  217. package/src/server/responses/sidecar-execution.ts +96 -14
  218. package/src/server/responses-undeclared-tool-guard.ts +5 -1
  219. package/src/server/workflow-refusal.ts +30 -0
  220. package/src/service/desktop-startup.ts +100 -3
  221. package/src/service/ownership-mutation-lease.d.mts +1 -1
  222. package/src/service/ownership-mutation-lease.mjs +5 -7
  223. package/src/sidecar/anthropic-binding.ts +79 -0
  224. package/src/sidecar/auth.ts +76 -2
  225. package/src/types/anthropic-account-pool.ts +33 -0
  226. package/src/types/config.ts +12 -28
  227. package/src/types/provider.ts +5 -0
  228. package/src/types/tools.ts +7 -0
  229. package/src/types.ts +4 -0
  230. package/src/usage/cost.ts +3 -2
  231. package/src/usage/expected-prices.ts +66 -14
  232. package/src/vision/anthropic-describe.ts +16 -3
  233. package/src/vision/index.ts +3 -0
  234. package/src/vision/plan.ts +19 -6
  235. package/src/web-search/alpha-search.ts +7 -1
  236. package/src/web-search/anthropic-executor.ts +16 -4
  237. package/src/web-search/header-deadline.ts +67 -0
  238. package/src/web-search/index.ts +7 -3
  239. package/src/web-search/loop.ts +32 -10
  240. package/src/web-search/passthrough-bridge.ts +4 -1
  241. package/src/web-search/sidecar-providers.ts +4 -3
  242. package/gui/dist/assets/App-BOUa-ek3.js +0 -54
  243. package/gui/dist/assets/Tray-DUtzB4zF.js +0 -1
  244. package/gui/dist/assets/index-DwHNDF4s.js +0 -94
  245. package/gui/dist/assets/quota-summary-BuL4WIBo.js +0 -1
@@ -163,6 +163,7 @@ import { ambiguousResendAllowanceFor, selfContainedResponsesBody } from "./reset
163
163
  import { upstreamErrorMessageFromPayload, ENCRYPTED_FUNCTION_OUTPUT_REJECTION } from "../../lib/errors";
164
164
  import { isTransientConsoleGoUploadRejection } from "../../providers/opencode-zen-rate-limit";
165
165
  import { planReasoningEffortDowngrade } from "../../providers/reasoning-metadata";
166
+ import { unboundPoolSpendRefusalResponse } from "../workflow-refusal";
166
167
 
167
168
  /** Prepares and recovers one native Responses exchange before client commitment. */
168
169
  export async function preparePassthroughExchange(
@@ -214,6 +215,7 @@ export async function preparePassthroughExchange(
214
215
  ResponsesSendBudget,
215
216
  | "remainingTransientSendBudget"
216
217
  | "noteTransientSends"
218
+ | "transientSendReporter"
217
219
  | "recoverySendAllowance"
218
220
  | "recoveryClassFor"
219
221
  | "sendBudgetExhausted"
@@ -249,6 +251,7 @@ export async function preparePassthroughExchange(
249
251
  const {
250
252
  remainingTransientSendBudget,
251
253
  noteTransientSends,
254
+ transientSendReporter,
252
255
  recoverySendAllowance,
253
256
  recoveryClassFor,
254
257
  sendBudgetExhausted,
@@ -794,7 +797,10 @@ export async function preparePassthroughExchange(
794
797
  */
795
798
  const sendAmbiguousReplacement = (
796
799
  signal: AbortSignal = upstream.signal,
797
- ): Promise<Response> => fetchWithHeaderTimeout(
800
+ ): Promise<Response> => {
801
+ const report = transientSendReporter();
802
+ let started = false;
803
+ const run = () => fetchWithHeaderTimeout(
798
804
  request.url,
799
805
  applyUpstreamRecoveryInit({
800
806
  method: request.method,
@@ -829,11 +835,17 @@ export async function preparePassthroughExchange(
829
835
  // Charged to the SAME request counter every other send goes through. The
830
836
  // replacement is bought here rather than by a nested retry helper, so there is
831
837
  // one charge for one send and no per-layer counter to reconcile.
832
- noteTransientSends(1);
838
+ if (report.beforeSend?.() === false) throw new SendBudgetExhaustedError(safeHostLabel(request.url));
839
+ started = true;
833
840
  },
834
841
  }),
835
842
  route.provider.authMode === "forward",
836
843
  );
844
+ return (report.execute ? report.execute(run) : run()).finally(() => {
845
+ try { if (started) report(1); }
846
+ finally { report.close?.(); }
847
+ });
848
+ };
837
849
  /**
838
850
  * Refuse a built body that exceeds the operator's configured ceiling, before it is sent.
839
851
  *
@@ -891,7 +903,8 @@ export async function preparePassthroughExchange(
891
903
  releaseUpstreamHostAdmission(nativeHostState.lease);
892
904
  nativeHostState.lease = null;
893
905
  releaseCodexAuthContextProbeLease(admissionState.authCtx);
894
- return formatErrorResponse(429, "request_send_budget_exhausted", err.message);
906
+ return unboundPoolSpendRefusalResponse(logCtx)
907
+ ?? formatErrorResponse(429, "request_send_budget_exhausted", err.message);
895
908
  }
896
909
  const refusal = unwrapUpstreamRetryEvidenceError(err);
897
910
  // Pacing may outlive the selected account's admission. No fetch occurred, so do
@@ -991,7 +1004,7 @@ export async function preparePassthroughExchange(
991
1004
  .then(adoptObservedResponse);
992
1005
  },
993
1006
  { abortSignal: upstream.signal, label: safeHostLabel(request.url),
994
- attempts: remainingTransientSendBudget(transientSendAttempts()), onSendsConsumed: noteTransientSends,
1007
+ attempts: remainingTransientSendBudget(transientSendAttempts()), onSendsConsumed: transientSendReporter(),
995
1008
  claimAmbiguousResend: claimPreHeaderResend,
996
1009
  },
997
1010
  );
@@ -1094,7 +1107,7 @@ export async function preparePassthroughExchange(
1094
1107
  .then(adoptObservedResponse);
1095
1108
  },
1096
1109
  { abortSignal: upstream.signal, label: safeHostLabel(request.url), attempts: allowance.attempts,
1097
- onSendsConsumed: noteTransientSends, claimAmbiguousResend: claimPreHeaderResend },
1110
+ onSendsConsumed: transientSendReporter(allowance.permit), claimAmbiguousResend: claimPreHeaderResend },
1098
1111
  );
1099
1112
  } catch (err) {
1100
1113
  return { failed: transportFailureResponse(err) };
@@ -1226,7 +1239,7 @@ export async function preparePassthroughExchange(
1226
1239
  },
1227
1240
  { abortSignal: upstream.signal, label: safeHostLabel(request.url),
1228
1241
  attempts: remainingTransientSendBudget(transientSendAttempts()),
1229
- onSendsConsumed: noteTransientSends, claimAmbiguousResend: claimPreHeaderResend },
1242
+ onSendsConsumed: transientSendReporter(), claimAmbiguousResend: claimPreHeaderResend },
1230
1243
  );
1231
1244
  } catch (err) {
1232
1245
  return transportFailureResponse(err);
@@ -1354,7 +1367,7 @@ export async function preparePassthroughExchange(
1354
1367
  .then(adoptObservedResponse);
1355
1368
  },
1356
1369
  { abortSignal: upstream.signal, label: safeHostLabel(request.url), attempts: remainingTransientSendBudget(transientSendAttempts()),
1357
- onSendsConsumed: noteTransientSends, claimAmbiguousResend: claimPreHeaderResend },
1370
+ onSendsConsumed: transientSendReporter(), claimAmbiguousResend: claimPreHeaderResend },
1358
1371
  );
1359
1372
  } catch (err) {
1360
1373
  return transportFailureResponse(err);
@@ -1489,7 +1502,7 @@ export async function preparePassthroughExchange(
1489
1502
  .then(adoptObservedResponse);
1490
1503
  },
1491
1504
  { abortSignal: upstream.signal, label: safeHostLabel(request.url), attempts: remainingTransientSendBudget(transientSendAttempts()),
1492
- onSendsConsumed: noteTransientSends, claimAmbiguousResend: claimPreHeaderResend },
1505
+ onSendsConsumed: transientSendReporter(), claimAmbiguousResend: claimPreHeaderResend },
1493
1506
  );
1494
1507
  } catch (err) {
1495
1508
  return transportFailureResponse(err);
@@ -64,6 +64,7 @@ import type { OcxParsedRequest } from "../../types";
64
64
  import { buildToolBridgeMaps } from "./collaboration";
65
65
  import { parseRequest } from "../../responses/parser";
66
66
  import { anthropicSessionKeyFromParts } from "../../oauth/anthropic-routing";
67
+ import { configuredAnthropicInstance } from "../../providers/anthropic-instance";
67
68
  import { isTranslatorBudgetExceededError } from "../../lib/translator-budget";
68
69
  import { bindTurnTerminationScope, rememberDeliveredFinalAnswer } from "../../responses/turn-termination";
69
70
  import { observeCacheDiagnosticInbound, rebindCacheDiagnosticBody, requestLogSpeedLabel, readConfiguredCodexServiceTier } from "../request-log";
@@ -74,6 +75,7 @@ import {
74
75
  routeCompactionModel,
75
76
  routeModel,
76
77
  NoEligiblePolicyCandidateError,
78
+ AnthropicSecondaryInstanceUnavailableError,
77
79
  } from "../../router";
78
80
  import { evidenceFromBody } from "../../routing/request-evidence";
79
81
  import { OPENAI_CODEX_PROVIDER_ID, isCanonicalOpenAiForwardProvider } from "../../providers/openai-tiers";
@@ -522,6 +524,23 @@ export async function prepareResponsesRequest(
522
524
  // or a subagent fallback rewrites it, so a refusal names the client's own
523
525
  // request rather than a destination it never asked for.
524
526
  const inboundSelector = parsed.modelId;
527
+ const selectorTargetsSecondary = (selector: string): boolean => {
528
+ const slash = selector.indexOf("/");
529
+ if (slash <= 0) return false;
530
+ const qualifier = selector.slice(0, slash);
531
+ // Match router precedence: exact custom keys (including uppercase names) win over aliases.
532
+ if (Object.hasOwn(config.providers, qualifier)) return qualifier === "anthropic2";
533
+ return qualifier === "anthropic2"
534
+ || config.providers.anthropic2?.alias?.trim().toLowerCase() === qualifier.toLowerCase();
535
+ };
536
+ const explicitlyRequestsSecondary = selectorTargetsSecondary(inboundSelector);
537
+ const secondaryProvider = config.providers.anthropic2;
538
+ // Reserve an explicit B selector before an absent row can default-route it to A.
539
+ // Combo parents have already dispatched above; concrete children retain their own target.
540
+ if (explicitlyRequestsSecondary && (!secondaryProvider || secondaryProvider.disabled === true
541
+ || secondaryProvider.authMode === "oauth" && configuredAnthropicInstance(config, "anthropic2") !== "anthropic2")) {
542
+ return formatErrorResponse(401, "authentication_error", "Anthropic Pool 2 requires an enabled configured provider");
543
+ }
525
544
  const admissionScope = resolveAdmissionModelScope(config, options.admission);
526
545
  const captureInboundRoutePolicy = (candidate: RouteResult, captureRequestPolicy = true): RouteResult => {
527
546
  // Every route this request path produces passes through here: the direct
@@ -553,11 +572,21 @@ export async function prepareResponsesRequest(
553
572
  // no canonical OpenAI route for (#2901). Only the initial compaction route
554
573
  // may fall back to the configured default provider; combo attempts and the
555
574
  // later fallback/recovery re-routes keep the ordinary reservation.
556
- const routeInbound = (modelId: string) => concreteSelection
557
- ? routeConcreteModel(config, modelId)
558
- : parsed._compactionRequest === true
559
- ? routeCompactionModel(config, modelId, evidenceFromBody(parsed._rawBody))
560
- : routeModel(config, modelId, evidenceFromBody(parsed._rawBody));
575
+ const routeInbound = (modelId: string) => {
576
+ const resolved = concreteSelection
577
+ ? routeConcreteModel(config, modelId)
578
+ : parsed._compactionRequest === true
579
+ ? routeCompactionModel(config, modelId, evidenceFromBody(parsed._rawBody))
580
+ : routeModel(config, modelId, evidenceFromBody(parsed._rawBody));
581
+ // A Pool 2 selector may leave Pool 2 only through an operator-configured cross-provider
582
+ // blocked-model redirect, which the router marks as a credential-domain rewrite. Any other
583
+ // destination means the selector fell through to default resolution, which stays refused.
584
+ const operatorRedirect = resolved.routeReason === "blocked-model-redirect" && resolved.credentialDomainRewrite === true;
585
+ if (selectorTargetsSecondary(modelId) && resolved.providerName !== "anthropic2" && !operatorRedirect) {
586
+ throw new Error("Anthropic Pool 2 selector cannot use the default provider");
587
+ }
588
+ return resolved;
589
+ };
561
590
  const resolveRoute = (modelId: string) => captureInboundRoutePolicy(routeInbound(modelId));
562
591
  // The phase's destination. Resolved through the admission-scoped resolver every other route
563
592
  // uses, and it fails closed exactly like the shadow target: falling back to the native model
@@ -644,6 +673,9 @@ export async function prepareResponsesRequest(
644
673
  logCtx.routeDecision = route.routeDecision;
645
674
  logCtx.policyEligibility = route.policyEligibility;
646
675
  } catch (err) {
676
+ if (err instanceof AnthropicSecondaryInstanceUnavailableError) {
677
+ return formatErrorResponse(401, "authentication_error", err.message);
678
+ }
647
679
  const policyRefusal = policyCandidateRefusalResponse(err);
648
680
  if (policyRefusal) return policyRefusal;
649
681
  if (err instanceof AdmissionModelDeniedError) return admissionModelDeniedResponse(err);
@@ -1,11 +1,12 @@
1
+ import { createInferenceSendBudget } from "../inference/context";
1
2
  import type { ResponsesRequestContext } from "./core-options";
2
- import { createRequestExecutionBudget, isRequestExecutionBudget } from "../../lib/request-execution-budget";
3
+ import { claimDispatchSpendProof, createPhysicalSendReporter, createRequestExecutionBudget, isRequestExecutionBudget, reportDispatchSends } from "../../lib/request-execution-budget";
3
4
  import {
4
5
  chargeWorkflowSends,
5
6
  workflowSendCeilingReached,
6
7
  workflowSpendCeilingReached,
7
8
  } from "../../lib/workflow-budget";
8
- import { workflowRefusalResponse } from "../workflow-refusal";
9
+ import { poolContinuityRefusalReason, workflowRefusalResponse } from "../workflow-refusal";
9
10
  import type { AttemptRecoveryKind, AttemptRecoveryWithheld } from "../../usage/log";
10
11
  import { noteAttemptRecoveryWithheld, noteAttemptSend } from "../request-log";
11
12
  import { TRANSIENT_RETRY_MAX_ATTEMPTS } from "../../lib/upstream-retry";
@@ -52,17 +53,27 @@ export function createResponsesSendBudget(
52
53
  // fresh default of 3. It is now a holder carried on options, so a combo child inherits the
53
54
  // parent's spend instead of starting over per target -- both halves of the measured
54
55
  // amplification in #4546.
55
- const sendBudget = options.sendBudget ?? createRequestExecutionBudget();
56
+ const sendBudget = options.sendBudget ?? createInferenceSendBudget(req, logCtx);
56
57
  // The root workflow is the user-visible task. A per-request cap cannot bound a fan-out that
57
58
  // sends once per child seven hundred times, so every send charged to the request is charged
58
59
  // to the root as well (#4546).
59
60
  const workflowRootId = req.headers.get("x-codex-parent-thread-id")?.trim() || undefined;
60
- const noteTransientSends = (used: number): void => {
61
+ const inheritedPermit = options.compactionRecoveryPermit ?? options.comboDispatchPermit;
62
+ let pendingHopPermit: SingleUseDispatchPermit | undefined = options.compactionRecoveryPermit;
63
+ const recordTransientSends = (used: number, permit?: SingleUseDispatchPermit): void => {
61
64
  const charged = Math.max(0, used);
62
- sendBudget.used += charged;
65
+ reportDispatchSends(sendBudget, charged, permit);
63
66
  options.onCompactionRecoverySendsReported?.(charged);
64
67
  chargeWorkflowSends(workflowRootId, charged);
65
68
  };
69
+ const noteTransientSends = (used: number): void => recordTransientSends(used, pendingHopPermit ?? inheritedPermit);
70
+ // Capture at helper creation, before another leg can replace the pending handoff.
71
+ const transientSendReporter = (permit = pendingHopPermit ?? inheritedPermit) =>
72
+ createPhysicalSendReporter(sendBudget, () => ({ poolId: logCtx.spendPoolId ?? logCtx.provider,
73
+ identityId: logCtx.accountLogLabel }), permit, charged => {
74
+ options.onCompactionRecoverySendsReported?.(charged);
75
+ chargeWorkflowSends(workflowRootId, charged);
76
+ });
66
77
  // Refused before any dispatch, and deliberately not by evicting the root's ledger entry:
67
78
  // dropping the record to make room would hand the fan-out a fresh allowance, which is the
68
79
  // laundering this ceiling exists to stop. The client is told the task needs a new grant
@@ -78,7 +89,17 @@ export function createResponsesSendBudget(
78
89
  // returns -- a refusal an operator cannot tell from an ordinary budget exhaustion, on a
79
90
  // ceiling they configured themselves. Asked before dispatch, it names the scope and the
80
91
  // number instead. Returns undefined and touches no ledger when no ceiling is configured.
81
- const spentCeiling = workflowSpendCeilingReached(workflowRootId);
92
+ // Passthrough reports sends after they leave. Invalid or conflicting identity evidence
93
+ // still refuses here as well as in reserve(), including requests with no workflow root.
94
+ // Positive unbound balances alone are instead overlaid on each candidate pool below.
95
+ const continuityRefusal = poolContinuityRefusalReason();
96
+ if (continuityRefusal) {
97
+ return workflowRefusalResponse(continuityRefusal, logCtx, undefined, workflowRootId);
98
+ }
99
+ const prepaid = isRequestExecutionBudget(sendBudget)
100
+ ? claimDispatchSpendProof(sendBudget, options.compactionRecoveryPermit ?? options.comboDispatchPermit) : undefined;
101
+ const admissionPolicy = isRequestExecutionBudget(sendBudget) ? sendBudget.spendAdmissionPolicy : undefined;
102
+ const spentCeiling = workflowSpendCeilingReached(workflowRootId, undefined, logCtx.spendPoolId ?? logCtx.provider, prepaid, admissionPolicy);
82
103
  if (spentCeiling) {
83
104
  return workflowRefusalResponse(
84
105
  "workflow-spend-exhausted",
@@ -157,7 +178,6 @@ export function createResponsesSendBudget(
157
178
  * refused and the request would answer with a synthetic 502 in place of the real 429 the hop
158
179
  * was recovering from.
159
180
  */
160
- let pendingHopPermit: SingleUseDispatchPermit | undefined = options.compactionRecoveryPermit;
161
181
  /**
162
182
  * The budget an adapter's OWN dispatch ladder reserves against.
163
183
  *
@@ -171,19 +191,21 @@ export function createResponsesSendBudget(
171
191
  * The hop hands its reservation down through `pendingHopPermit`, the same seam the
172
192
  * passthrough ladder already uses, and this view spends it on the adapter's FIRST
173
193
  * reservation. Every later send in that ladder is a new physical send and is charged
174
- * normally. A permit the adapter takes but never sends under is released through the same
175
- * call it would have used for a reservation of its own, so an abandoned replay is refunded
176
- * rather than left charged.
194
+ * normally. The refundable view below keeps an unused hop open until executor invocation;
195
+ * its adapter release hands back an abandoned replay. The ordinary view retains early
196
+ * settlement for existing callers.
177
197
  */
198
+ const claimHopPermit = (): SingleUseDispatchPermit | undefined => {
199
+ const permit = pendingHopPermit;
200
+ pendingHopPermit = undefined;
201
+ return permit;
202
+ };
178
203
  const adapterDispatchBudget: RequestExecutionBudget | undefined = adapterSendBudget === undefined
179
- ? undefined
180
- : adapterDispatchBudgetView(adapterSendBudget, {
181
- claimHopPermit: () => {
182
- const permit = pendingHopPermit;
183
- pendingHopPermit = undefined;
184
- return permit;
185
- },
186
- });
204
+ ? undefined : adapterDispatchBudgetView(adapterSendBudget, { claimHopPermit });
205
+ // Opt in only where the adapter's executor is the confirmation boundary. Ordinary callers
206
+ // keep the existing early settlement contract, including already-settled permit fallback.
207
+ const refundableAdapterDispatchBudget: RequestExecutionBudget | undefined = adapterSendBudget === undefined
208
+ ? undefined : adapterDispatchBudgetView(adapterSendBudget, { claimHopPermit }, true);
187
209
  /**
188
210
  * How many sends a recovery leg may make, and the permit that authorises the last one.
189
211
  *
@@ -268,6 +290,7 @@ export function createResponsesSendBudget(
268
290
  return {
269
291
  workflowRootId,
270
292
  noteTransientSends,
293
+ transientSendReporter,
271
294
  remainingTransientSendBudget,
272
295
  /**
273
296
  * Physical sends this logical request has already made.
@@ -279,6 +302,7 @@ export function createResponsesSendBudget(
279
302
  get sendsUsed(): number { return sendBudget.used; },
280
303
  adapterSendBudget,
281
304
  adapterDispatchBudget,
305
+ refundableAdapterDispatchBudget,
282
306
  noteAdapterPhysicalSend,
283
307
  noteAdapterRecoveryWithheld,
284
308
  sendBudgetExhausted,
@@ -312,8 +336,17 @@ export type ResponsesSendBudget = Exclude<ReturnType<typeof createResponsesSendB
312
336
  function adapterDispatchBudgetView(
313
337
  budget: RequestExecutionBudget,
314
338
  hop: { claimHopPermit: () => SingleUseDispatchPermit | undefined },
339
+ refundableHop = false,
315
340
  ): RequestExecutionBudget {
316
341
  return {
342
+ get physicalStarted() { return budget.physicalStarted; },
343
+ get physicalLimit() { return budget.physicalLimit; },
344
+ get spendEnforced() { return budget.spendEnforced; },
345
+ get spendPolicyStarted() { return budget.spendPolicyStarted; },
346
+ get spendAdmissionPolicy() { return budget.spendAdmissionPolicy; },
347
+ startRequest: target => budget.startRequest?.(target),
348
+ claimPhysicalSend: () => budget.claimPhysicalSend?.(),
349
+ beginSpendProducer: () => budget.beginSpendProducer?.(),
317
350
  get used(): number { return budget.used; },
318
351
  set used(next: number) { budget.used = next; },
319
352
  logicalRequestId: budget.logicalRequestId,
@@ -327,6 +360,7 @@ function adapterDispatchBudgetView(
327
360
  claimAmbiguousResend: (limit: number): boolean => budget.claimAmbiguousResend?.(limit) === true,
328
361
  get ambiguousResendSpent(): boolean { return budget.ambiguousResendSpent === true; },
329
362
  reserveDispatch(intent: DispatchIntent): DispatchDecision {
363
+ const refundable = refundableHop || budget.spendEnforced === true;
330
364
  // A dispatch whose upstream state is unknown is refused on its own merits. A hop that
331
365
  // already paid does not make an unsafe replay safe, so that check stays with the budget.
332
366
  if (intent.replaySafe !== false) {
@@ -338,30 +372,33 @@ function adapterDispatchBudgetView(
338
372
  hopPermit.release();
339
373
  return budget.reserveDispatch({ ...intent, sendClass: hopPermit.sendClass });
340
374
  }
341
- // Confirmed here rather than in `use()`: the adapter reserves immediately before it
342
- // opens the transport, which is the same boundary the hop's own confirmation uses.
343
- // A permit some other leg already settled returns false, and this falls through to a
344
- // real reservation rather than handing the adapter a dead permit -- an adapter whose
345
- // `use()` fails treats the request as exhausted and stops sending entirely.
346
- if (hopPermit !== undefined && hopPermit.assumeCharge()) {
347
- let spent = false;
375
+ if (hopPermit !== undefined && (refundable || hopPermit.assumeCharge())) {
376
+ let settled = false;
377
+ let executionPermit = hopPermit;
378
+ const confirm = (): boolean => {
379
+ if (settled) return false;
380
+ settled = true;
381
+ // Admission can still reject this replay. Close its external booking only when
382
+ // the adapter invokes its executor, leaving release() able to refund it until then.
383
+ if (!refundable || hopPermit.assumeCharge()) return true;
384
+ // Preserve the old fallback for a permit another leg already settled.
385
+ const fresh = budget.reserveDispatch(intent);
386
+ if (!fresh.allowed) return false;
387
+ executionPermit = fresh.permit;
388
+ return fresh.permit.assumeCharge();
389
+ };
348
390
  return {
349
391
  allowed: true,
350
392
  permit: {
351
393
  sendClass: hopPermit.sendClass,
352
- use: (): boolean => {
353
- if (spent) return false;
354
- spent = true;
355
- return true;
356
- },
357
- assumeCharge: (): boolean => {
358
- if (spent) return false;
359
- spent = true;
360
- return true;
394
+ execute: run => executionPermit.execute ? executionPermit.execute(run) : run(),
395
+ use: confirm,
396
+ assumeCharge: confirm,
397
+ release: (): void => {
398
+ if (!refundable || settled) return;
399
+ settled = true;
400
+ hopPermit.release();
361
401
  },
362
- // The hop's charge is already settled and belongs to the leg that asked for it,
363
- // so there is nothing here to refund.
364
- release: (): void => {},
365
402
  },
366
403
  };
367
404
  }