@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
@@ -21,6 +21,9 @@ import {
21
21
  import { collectReferencedSpillFileNames, snapshotReferencedSpillFileNames } from "./state/spill-inspect";
22
22
  import { selectSnapshotEntries, snapshotPayload } from "./state/snapshot-select";
23
23
  import { clientCarriedPrefixLength, providerIssuedIdentity } from "./state/replay-fingerprint";
24
+ import { hasPendingClientToolCall, allowsUnforcedStoreFalseReplay } from "./state/unforced-store-false";
25
+ import { computeResponseStateMetrics, type ResponseStateMetrics } from "./state/metrics";
26
+ export type { ResponseStateMetrics } from "./state/metrics";
24
27
  export type { ResponseStateTempRecoveryResult, ResponseStateTempRecoveryOptions } from "./state/temp-recovery";
25
28
  export type { ResponseSpillDirInspection } from "./spill-store";
26
29
  export { recoverStaleResponseStateTemps, reclaimAbandonedResponseStateTemps, inspectAbandonedResponseStateTemps, sweepAbandonedResponseStateTemps } from "./state/temp-recovery";
@@ -118,6 +121,7 @@ export interface ResidentResponseState {
118
121
  /** Index in `items` where provider output begins; see clientCarriedPrefixLength. */
119
122
  providerOutputStart?: number;
120
123
  providers?: OcxProviderContinuationState;
124
+ unforcedStoreFalse?: boolean;
121
125
  sizeBytes: number;
122
126
  }
123
127
 
@@ -128,6 +132,7 @@ export interface SpilledResponseState {
128
132
  /** Mirrors the spilled payload boundary so a spilled entry keeps its anchor. */
129
133
  providerOutputStart?: number;
130
134
  providers?: OcxProviderContinuationState;
135
+ unforcedStoreFalse?: boolean;
131
136
  spill: ResponseSpillRef;
132
137
  sizeBytes: number;
133
138
  }
@@ -315,6 +320,7 @@ function measureResidentEntry(id: string, entry: ResidentInput): ResidentRespons
315
320
  items: entry.items,
316
321
  ...(entry.providerOutputStart !== undefined ? { providerOutputStart: entry.providerOutputStart } : {}),
317
322
  ...(entry.providers ? { providers: entry.providers } : {}),
323
+ ...(entry.unforcedStoreFalse ? { unforcedStoreFalse: entry.unforcedStoreFalse } : {}),
318
324
  });
319
325
  return sizeBytes === null ? null : { kind: "resident", ...entry, sizeBytes };
320
326
  }
@@ -417,7 +423,9 @@ function swapResidentForSpill(id: string, expected: ResidentResponseState, ref:
417
423
  kind: "spill",
418
424
  createdAt: expected.createdAt,
419
425
  ...(expected.clientThreadId ? { clientThreadId: expected.clientThreadId } : {}),
426
+ ...(expected.providerOutputStart !== undefined ? { providerOutputStart: expected.providerOutputStart } : {}),
420
427
  ...(expected.providers ? { providers: expected.providers } : {}),
428
+ ...(expected.unforcedStoreFalse ? { unforcedStoreFalse: expected.unforcedStoreFalse } : {}),
421
429
  spill: ref,
422
430
  };
423
431
  const next: SpilledResponseState = { ...base, sizeBytes: stubSize(id, base) };
@@ -441,6 +449,7 @@ function replaceSpillEntryAtomically(
441
449
  items: candidate.items,
442
450
  ...(candidate.providerOutputStart !== undefined ? { providerOutputStart: candidate.providerOutputStart } : {}),
443
451
  ...(candidate.providers ? { providers: candidate.providers } : {}),
452
+ ...(candidate.unforcedStoreFalse ? { unforcedStoreFalse: candidate.unforcedStoreFalse } : {}),
444
453
  });
445
454
  const base: Omit<SpilledResponseState, "sizeBytes"> = {
446
455
  kind: "spill",
@@ -448,6 +457,7 @@ function replaceSpillEntryAtomically(
448
457
  ...(candidate.clientThreadId ? { clientThreadId: candidate.clientThreadId } : {}),
449
458
  ...(candidate.providerOutputStart !== undefined ? { providerOutputStart: candidate.providerOutputStart } : {}),
450
459
  ...(candidate.providers ? { providers: candidate.providers } : {}),
460
+ ...(candidate.unforcedStoreFalse ? { unforcedStoreFalse: candidate.unforcedStoreFalse } : {}),
451
461
  spill: ref,
452
462
  };
453
463
  const next: SpilledResponseState = { ...base, sizeBytes: stubSize(id, base) };
@@ -533,6 +543,7 @@ function admitOversizedCandidate(
533
543
  items: candidate.items,
534
544
  ...(candidate.providerOutputStart !== undefined ? { providerOutputStart: candidate.providerOutputStart } : {}),
535
545
  ...(candidate.providers ? { providers: candidate.providers } : {}),
546
+ ...(candidate.unforcedStoreFalse ? { unforcedStoreFalse: candidate.unforcedStoreFalse } : {}),
536
547
  });
537
548
  // Enforce the ceiling against the REAL envelope: the spill payload adds
538
549
  // the {version, responseId, ...} wrapper, so a candidate within the
@@ -547,7 +558,9 @@ function admitOversizedCandidate(
547
558
  kind: "spill",
548
559
  createdAt: candidate.createdAt,
549
560
  ...(candidate.clientThreadId ? { clientThreadId: candidate.clientThreadId } : {}),
561
+ ...(candidate.providerOutputStart !== undefined ? { providerOutputStart: candidate.providerOutputStart } : {}),
550
562
  ...(candidate.providers ? { providers: candidate.providers } : {}),
563
+ ...(candidate.unforcedStoreFalse ? { unforcedStoreFalse: candidate.unforcedStoreFalse } : {}),
551
564
  spill: ref,
552
565
  };
553
566
  const next: SpilledResponseState = { ...base, sizeBytes: stubSize(id, base) };
@@ -935,6 +948,7 @@ function pruneResponses(at = now()): void {
935
948
  items: entry.items,
936
949
  ...(entry.providerOutputStart !== undefined ? { providerOutputStart: entry.providerOutputStart } : {}),
937
950
  ...(entry.providers ? { providers: entry.providers } : {}),
951
+ ...(entry.unforcedStoreFalse ? { unforcedStoreFalse: entry.unforcedStoreFalse } : {}),
938
952
  });
939
953
  if (swapResidentForSpill(oldestId, entry, ref)) noteSpillWriteSuccess();
940
954
  } catch (error) {
@@ -994,6 +1008,7 @@ export function evictOldestResponseContinuationForBudget(): number {
994
1008
  items: entry.items,
995
1009
  ...(entry.providerOutputStart !== undefined ? { providerOutputStart: entry.providerOutputStart } : {}),
996
1010
  ...(entry.providers ? { providers: entry.providers } : {}),
1011
+ ...(entry.unforcedStoreFalse ? { unforcedStoreFalse: entry.unforcedStoreFalse } : {}),
997
1012
  });
998
1013
  if (swapResidentForSpill(id, entry, ref)) noteSpillWriteSuccess();
999
1014
  } catch (error) {
@@ -1030,6 +1045,15 @@ function materializeEntry(
1030
1045
  schedulePersist();
1031
1046
  return { ok: false, failure };
1032
1047
  }
1048
+ // A restricted snapshot stub must agree with the validated spill's provenance.
1049
+ if (entry.unforcedStoreFalse && (result.payload.unforcedStoreFalse !== true
1050
+ || entry.providerOutputStart === undefined || entry.providerOutputStart > result.payload.items.length
1051
+ || entry.providerOutputStart !== result.payload.providerOutputStart)) {
1052
+ spillCounters.readFailures += 1;
1053
+ replaceWithSpillFailure(id, entry);
1054
+ schedulePersist();
1055
+ return { ok: false, failure: { code: "previous_response_not_found", reason: "spill_corrupt" } };
1056
+ }
1033
1057
  const state = measureResidentEntry(id, {
1034
1058
  createdAt: result.payload.createdAt,
1035
1059
  ...(result.payload.clientThreadId ? { clientThreadId: result.payload.clientThreadId } : {}),
@@ -1038,6 +1062,7 @@ function materializeEntry(
1038
1062
  ? { providerOutputStart: result.payload.providerOutputStart }
1039
1063
  : {}),
1040
1064
  ...(result.payload.providers ? { providers: result.payload.providers } : {}),
1065
+ ...(result.payload.unforcedStoreFalse ? { unforcedStoreFalse: result.payload.unforcedStoreFalse } : {}),
1041
1066
  });
1042
1067
  if (!state) {
1043
1068
  spillCounters.readFailures += 1;
@@ -1075,6 +1100,15 @@ export function expandPreviousResponseInput(body: unknown, clientThreadId?: stri
1075
1100
  replayScopeMismatchDrops += 1;
1076
1101
  return body;
1077
1102
  }
1103
+ const clientInput = inputItems(request.input);
1104
+ const stored = materialized.state.items;
1105
+ const carried = clientCarriedPrefixLength(stored, clientInput);
1106
+ if (
1107
+ materialized.state.unforcedStoreFalse
1108
+ && !allowsUnforcedStoreFalseReplay(stored, materialized.state.providerOutputStart, clientInput, carried)
1109
+ ) {
1110
+ return body;
1111
+ }
1078
1112
  // The client already replayed this history verbatim. Prepending the stored copy would
1079
1113
  // double it, and the doubled turn is stored again, so the next turn triples (#1412 saw
1080
1114
  // 127k of real context reach 1.3M tokens this way).
@@ -1087,10 +1121,7 @@ export function expandPreviousResponseInput(body: unknown, clientThreadId?: stri
1087
1121
  // There is no invariant that provider output always carries ids, so an entry whose output
1088
1122
  // has none simply never skips.
1089
1123
  {
1090
- const clientInput = inputItems(request.input);
1091
- const stored = materialized.state.items;
1092
1124
  const anchor = materialized.state.providerOutputStart;
1093
- const carried = clientCarriedPrefixLength(stored, clientInput);
1094
1125
  if (
1095
1126
  carried === stored.length
1096
1127
  && anchor !== undefined
@@ -1110,9 +1141,9 @@ export function expandPreviousResponseInput(body: unknown, clientThreadId?: stri
1110
1141
  }
1111
1142
  const expanded = {
1112
1143
  ...request,
1113
- input: [...materialized.state.items, ...inputItems(request.input)],
1144
+ input: [...stored, ...clientInput],
1114
1145
  };
1115
- replayedInputPrefixLengths.set(expanded, materialized.state.items.length);
1146
+ replayedInputPrefixLengths.set(expanded, stored.length);
1116
1147
  return expanded;
1117
1148
  }
1118
1149
 
@@ -1156,29 +1187,6 @@ export function previousResponseProviderState(responseId: string | undefined): O
1156
1187
  return providers ? structuredClone(providers) : undefined;
1157
1188
  }
1158
1189
 
1159
- export interface ResponseStateMetrics {
1160
- count: number;
1161
- residentCount: number;
1162
- spillStubCount: number;
1163
- tombstoneCount: number;
1164
- totalBytes: number;
1165
- spillPayloadBytes: number;
1166
- largestBytes: number;
1167
- oldestAgeMs: number;
1168
- spillWrites: number;
1169
- spillWriteFailures: number;
1170
- spillWriteStatus: ResponseSpillWriteStatus;
1171
- spillWriteConsecutiveFailures: number;
1172
- spillLastWriteFailureCode: ResponseSpillWriteFailureCode | null;
1173
- spillLastWriteFailureOrigin: ResponseSpillWriteFailureOrigin | null;
1174
- spillAclRetryReturnedTimeouts: number;
1175
- spillAclTimeoutMemoRefusals: number;
1176
- spillLastWriteFailureAt: number | null;
1177
- spillLastWriteSuccessAt: number | null;
1178
- spillReadFailures: number;
1179
- replayScopeMismatchDrops: number;
1180
- }
1181
-
1182
1190
  /**
1183
1191
  * Observe-only snapshot of the in-RAM continuation store, surfaced via GET /api/system/memory.
1184
1192
  * Additive and side-effect free — it does NOT lazy-load the disk snapshot, prune, or evict — so a
@@ -1189,50 +1197,23 @@ export interface ResponseStateMetrics {
1189
1197
  * store (JS heap) or in the runtime allocator (native).
1190
1198
  */
1191
1199
  export function responseStateMetrics(): ResponseStateMetrics {
1192
- const at = now();
1193
- let largestBytes = 0;
1194
- let oldestCreatedAt = at;
1195
- let residentCount = 0;
1196
- let spillStubCount = 0;
1197
- let tombstoneCount = 0;
1198
- let spillPayloadBytes = 0;
1199
- for (const state of states.values()) {
1200
- const bytes = state.sizeBytes;
1201
- if (bytes > largestBytes) largestBytes = bytes;
1202
- if (state.createdAt < oldestCreatedAt) oldestCreatedAt = state.createdAt;
1203
- if (state.kind === "resident") {
1204
- residentCount += 1;
1205
- } else if (state.kind === "spill") {
1206
- spillStubCount += 1;
1207
- spillPayloadBytes += state.spill.payloadBytes;
1208
- } else tombstoneCount += 1;
1209
- }
1210
- return {
1211
- count: states.size,
1212
- residentCount,
1213
- spillStubCount,
1214
- tombstoneCount,
1200
+ return computeResponseStateMetrics({
1201
+ states: states.values(),
1202
+ stateCount: states.size,
1203
+ now: now(),
1215
1204
  totalBytes: responseContinuationRetainedStoreSnapshot().bytes,
1216
- spillPayloadBytes,
1217
- largestBytes,
1218
- oldestAgeMs: states.size > 0 ? at - oldestCreatedAt : 0,
1219
1205
  spillWrites: spillCounters.writes,
1220
1206
  spillWriteFailures: spillCounters.writeFailures,
1221
- spillWriteStatus: spillWriteHealth.consecutiveFailures > 0
1222
- ? "degraded"
1223
- : spillWriteHealth.lastSuccessAt !== null
1224
- ? "healthy"
1225
- : "initial",
1226
1207
  spillWriteConsecutiveFailures: spillWriteHealth.consecutiveFailures,
1227
- spillLastWriteFailureCode: spillWriteHealth.lastFailureCode,
1228
- spillLastWriteFailureOrigin: spillWriteHealth.lastFailureOrigin,
1208
+ spillLastFailureCode: spillWriteHealth.lastFailureCode,
1209
+ spillLastFailureOrigin: spillWriteHealth.lastFailureOrigin,
1210
+ spillLastSuccessAt: spillWriteHealth.lastSuccessAt,
1229
1211
  spillAclRetryReturnedTimeouts: spillCounters.aclRetryReturnedTimeouts,
1230
1212
  spillAclTimeoutMemoRefusals: spillCounters.aclTimeoutMemoRefusals,
1231
- spillLastWriteFailureAt: spillWriteHealth.lastFailureAt,
1232
- spillLastWriteSuccessAt: spillWriteHealth.lastSuccessAt,
1213
+ spillLastFailureAt: spillWriteHealth.lastFailureAt,
1233
1214
  spillReadFailures: spillCounters.readFailures,
1234
1215
  replayScopeMismatchDrops,
1235
- };
1216
+ });
1236
1217
  }
1237
1218
 
1238
1219
  /**
@@ -1256,7 +1237,7 @@ export function rememberResponseState(
1256
1237
  requestBody: unknown,
1257
1238
  response: { id?: unknown; output?: unknown; status?: unknown; incomplete_details?: unknown },
1258
1239
  providerState?: OcxProviderContinuationState | string,
1259
- opts?: { force?: boolean; clientThreadId?: string },
1240
+ opts?: { force?: boolean; clientThreadId?: string; retainForToolContinuation?: boolean },
1260
1241
  ): void {
1261
1242
  if (!requestBody || typeof requestBody !== "object" || Array.isArray(requestBody)) return;
1262
1243
  const request = requestBody as Record<string, unknown>;
@@ -1266,8 +1247,10 @@ export function rememberResponseState(
1266
1247
  // The passthrough branch records with force so those chains can be expanded locally; the
1267
1248
  // store stays in-memory under RESPONSE_TTL_MS, so this is a proxy-internal continuation cache, not
1268
1249
  // real server-side response storage.
1269
- if (request.store === false && !opts?.force) return;
1270
1250
  if (typeof response.id !== "string" || !Array.isArray(response.output)) return;
1251
+ // Only an explicitly opted-in adapter may retain pending client calls despite store:false.
1252
+ const unforcedStoreFalse = request.store === false && !opts?.force;
1253
+ if (unforcedStoreFalse && !(opts?.retainForToolContinuation === true && hasPendingClientToolCall(response.output))) return;
1271
1254
  if (response.status === "incomplete") {
1272
1255
  const details = response.incomplete_details;
1273
1256
  if (!details || typeof details !== "object" || Array.isArray(details)
@@ -1300,6 +1283,7 @@ export function rememberResponseState(
1300
1283
  // incomplete agent turn on the Cursor side (we suspended without a real mcpResult), so its
1301
1284
  // checkpoint must not be reused — but the conversation id string itself is still valid.
1302
1285
  ...(Object.keys(normalizedProviderState).length > 0 ? { providers: normalizedProviderState } : {}),
1286
+ ...(unforcedStoreFalse ? { unforcedStoreFalse: true } : {}),
1303
1287
  });
1304
1288
  enforceAppOwnedMemoryBudget();
1305
1289
  schedulePersist();
package/src/router.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { CODEX_INTERNAL_OPENAI_MODELS } from "./codex/control-plane-models";
1
2
  import type { CodexAccountMode, OcxConfig, OcxProviderConfig } from "./types";
2
3
  import { createHash } from "node:crypto";
3
4
  import { peekAuthStore } from "./oauth/store";
@@ -12,6 +13,7 @@ import {
12
13
  } from "./combos";
13
14
  import type { NormalizedComboConfig } from "./combos/types";
14
15
  import { hasOwnProvider } from "./config/provider-name";
16
+ import { anthropicInstanceRowShapeMatches } from "./providers/anthropic-instance-id";
15
17
  import { providerUsesKeyAuthOverride, resolveProviderApiKey } from "./providers/key-store";
16
18
  import { captureProviderApiKeySelection } from "./providers/api-key-selection-capture";
17
19
  import { assertProviderDestinationAllowed } from "./lib/destination-policy";
@@ -53,6 +55,14 @@ import { evaluatePolicyProfile, type PolicyRequestEvidence } from "./routing/eva
53
55
  import { assemblePolicyCandidateEvidence } from "./routing/compatibility/assemble";
54
56
  import { resolveModelPolicy, type ResolvedModelPolicy } from "./providers/resolved-model-policy";
55
57
 
58
+ /** Explicit Pool 2 admission refusal; ingress must not recover it through default routing. */
59
+ export class AnthropicSecondaryInstanceUnavailableError extends Error {
60
+ constructor() {
61
+ super("Anthropic Pool 2 requires an enabled configured provider with valid OAuth ownership");
62
+ this.name = "AnthropicSecondaryInstanceUnavailableError";
63
+ }
64
+ }
65
+
56
66
  export class UnknownRoutingPolicyError extends Error {
57
67
  constructor(readonly profileId: string) {
58
68
  super(`Unknown routing policy: ${profileId}`);
@@ -521,9 +531,18 @@ export function routedProviderConfig(providerName: string, provider: OcxProvider
521
531
  return resolved;
522
532
  }
523
533
 
534
+ /**
535
+ * Rows eligible for bare-model inference (configured default model, model list, model alias).
536
+ *
537
+ * The marked builtin Pool 2 row is excluded: it seeds the same catalog as `anthropic`, so letting it
538
+ * compete would move a bare `claude-*` request onto Pool 2 by insertion order, or make a shared alias
539
+ * ambiguous. Pool 2 is reached only through `anthropic2/<model>`, its provider alias, or an explicit
540
+ * `defaultProvider`. An unmarked custom row named `anthropic2` keeps its ordinary custom meaning.
541
+ */
524
542
  function activeProviderEntries(config: OcxConfig): [string, OcxProviderConfig][] {
525
543
  return Object.entries(config.providers)
526
- .filter(([name, provider]) => name !== LEGACY_CHATGPT_PROVIDER_ID && provider.disabled !== true);
544
+ .filter(([name, provider]) => name !== LEGACY_CHATGPT_PROVIDER_ID && provider.disabled !== true
545
+ && !(name === "anthropic2" && anthropicInstanceRowShapeMatches(name, provider)));
527
546
  }
528
547
 
529
548
  export class NoEnabledOpenAiProviderError extends Error {
@@ -564,9 +583,6 @@ export function comboRouteDecisionTrace(
564
583
  });
565
584
  }
566
585
 
567
- // Codex uses a small number of control-plane model ids that are not part of the public GPT/o
568
- // naming families. Keep this exact: a broad `codex-*` rule could capture a third-party model.
569
- const CODEX_INTERNAL_OPENAI_MODELS = new Set(["codex-auto-review"]);
570
586
  const MAX_BLOCKED_MODEL_REDIRECT_EDGES = 5;
571
587
 
572
588
  interface BlockedModelRedirectState {
@@ -620,6 +636,9 @@ function routeResult(
620
636
  routeReason: string,
621
637
  redirectState: BlockedModelRedirectState,
622
638
  ): RouteResult {
639
+ if (providerName === "anthropic2" && provider.authMode === "oauth" && !anthropicInstanceRowShapeMatches(providerName, provider)) {
640
+ throw new AnthropicSecondaryInstanceUnavailableError();
641
+ }
623
642
  const qualified = resolveBlockedModelRedirect(config, `${providerName}/${modelId}`);
624
643
  const bare = resolveBlockedModelRedirect(config, modelId);
625
644
  const crossTarget = config && (
@@ -813,6 +832,11 @@ function routeModelInternal(
813
832
  if (slash > 0) {
814
833
  const requestedProvider = modelId.slice(0, slash);
815
834
  const requestedLower = requestedProvider.toLowerCase();
835
+ // Literal B is a reserved instance selector even before its row exists. Keep other
836
+ // qualifiers, including exact uppercase custom keys, on the ordinary routing path.
837
+ if (requestedProvider === "anthropic2" && !hasOwnProvider(config.providers, requestedProvider)) {
838
+ throw new AnthropicSecondaryInstanceUnavailableError();
839
+ }
816
840
  let provName: string | undefined;
817
841
 
818
842
  if (hasOwnProvider(config.providers, requestedProvider)) {
@@ -76,7 +76,7 @@ export function assemblePolicyCandidateEvidence(
76
76
  quota: quotaEvidenceForCandidate({
77
77
  provider: candidate.provider,
78
78
  model: candidate.model,
79
- }),
79
+ }, config),
80
80
  cost: costEvidenceForCandidate({
81
81
  provider: candidate.provider,
82
82
  model: candidate.model,
@@ -1,3 +1,5 @@
1
+ import type { OcxConfig } from "../types";
2
+ import { configuredAnthropicInstance } from "../providers/anthropic-instance";
1
3
  /**
2
4
  * Quota-aware policy scoring (RI-07).
3
5
  *
@@ -98,7 +100,7 @@ export function codexPoolQuotaEvidence(accounts: readonly CodexPoolQuotaAccount[
98
100
  };
99
101
  }
100
102
 
101
- export function quotaEvidenceForCandidate(input: QuotaEvidenceInput): RouteQuotaEvidence {
103
+ export function quotaEvidenceForCandidate(input: QuotaEvidenceInput, config?: Pick<OcxConfig, "providers">): RouteQuotaEvidence {
102
104
  if (input.provider === "openai" && input.codexAccountId) {
103
105
  // listAccountQuotas() is the reconciled quota snapshot: config-generation
104
106
  // reconciliation prunes removed accounts. Other eligibility dimensions
@@ -113,8 +115,10 @@ export function quotaEvidenceForCandidate(input: QuotaEvidenceInput): RouteQuota
113
115
  return codexAccountQuotaEvidence(input.codexAccountId, input.codexAccountPlan);
114
116
  }
115
117
 
116
- if (input.provider === "anthropic" && input.accountRef) {
117
- const quota = getCachedProviderAccountQuota("anthropic", input.accountRef);
118
+ const instance = config ? configuredAnthropicInstance(config, input.provider)
119
+ : input.provider === "anthropic" ? "anthropic" : undefined;
120
+ if (instance && input.accountRef) {
121
+ const quota = getCachedProviderAccountQuota(instance, input.accountRef);
118
122
  if (quota) {
119
123
  const family = anthropicFamilyWindow(input.model, quota.customWindows ?? []);
120
124
  const percents = [quota.fiveHourPercent, quota.weeklyPercent, quota.monthlyPercent, family?.percent]
@@ -38,6 +38,7 @@ import { redactSecretString } from "../lib/redact";
38
38
  import { DECLARABLE_HOSTED_TOOL_TYPES } from "../responses/hosted-tool-policy";
39
39
  import { effectiveGoogleMode, getProviderRegistryEntry, providerCodexAccountMode, providerMatchesRegistryTransport, registryEntryForProviderDestination } from "../providers/registry";
40
40
  import { providerConfigSeed } from "../providers/derive";
41
+ import { anthropicOAuthInstanceConfigError } from "../config/schema/anthropic-account-pool";
41
42
  import type { OcxConfig, OcxProviderConfig } from "../types";
42
43
  import { openRouterRoutingConfigError } from "../providers/openrouter-routing";
43
44
  import { modelAutoCompactTokenLimitsConfigError } from "../providers/auto-compact-budget";
@@ -785,6 +786,8 @@ export function providerManagementConfigError(
785
786
  return `provider ${name} must not include codexAccountMode`;
786
787
  }
787
788
  const typed = provider as unknown as OcxProviderConfig;
789
+ const markerError = anthropicOAuthInstanceConfigError({ providers: { [name]: provider } });
790
+ if (markerError) return markerError;
788
791
  // Every write path (POST, PUT, reload, both PATCH passes) funnels through here, so a PATCH
789
792
  // that changes authMode/baseUrl/adapter under a retained tlsProfile is refused before it
790
793
  // persists a row the config schema would later reject as document-fatal.
@@ -1057,6 +1060,8 @@ const PROVIDER_CONFIG_FIELD_POLICY = {
1057
1060
  modelVercelGatewayRouting: "editor",
1058
1061
  authMode: "editor",
1059
1062
  oauthAccountFailover: "editor",
1063
+ anthropicAccountPool: "editor",
1064
+ anthropicOAuthInstance: "editor",
1060
1065
  keyOptional: "editor",
1061
1066
  freeTier: "editor",
1062
1067
  note: "editor",
@@ -32,7 +32,7 @@ import {
32
32
  UPSTREAM_RESET_REPLAY_REFUSED_CODE,
33
33
  } from "../lib/upstream-retry";
34
34
  import { estimateTokens } from "../lib/token-estimate";
35
- import { captureRouteStaticPolicy, NoEligiblePolicyCandidateError, UnknownRoutingPolicyError, routeModel } from "../router";
35
+ import { AnthropicSecondaryInstanceUnavailableError, captureRouteStaticPolicy, NoEligiblePolicyCandidateError, UnknownRoutingPolicyError, routeModel } from "../router";
36
36
  import { evidenceFromBody } from "../routing/request-evidence";
37
37
  import { resolveWireProtocolOverride } from "./adapter-resolve";
38
38
  import { resolveOpenCodeGoTransport } from "../providers/opencode-go-transport";
@@ -242,6 +242,11 @@ async function handleChatCompletionsWithBudget(
242
242
  }
243
243
  }
244
244
  } catch (err) {
245
+ if (err instanceof AnthropicSecondaryInstanceUnavailableError) {
246
+ logCtx.requestedModel = requestedModel;
247
+ if (logIds) addFinalRequestLog(logIds.requestId, logIds.start, logCtx, 401, { closeReason: "non_stream" });
248
+ return chatCompletionsErrorResponse(401, err.message, "authentication_error");
249
+ }
245
250
  if (err instanceof AdmissionModelDeniedError) {
246
251
  logCtx.requestedModel = requestedModel;
247
252
  if (logIds) addFinalRequestLog(logIds.requestId, logIds.start, logCtx, 403, { closeReason: "non_stream" });
@@ -1,3 +1,5 @@
1
+ import { createPhysicalSendReporter } from "../lib/request-execution-budget";
2
+ import { createInferenceSendBudget } from "./inference/context";
1
3
  import { buildOpenAIChatPassthroughRequest, createOpenAIChatAdapter } from "../adapters/openai-chat";
2
4
  import type { AdapterRequest, ProviderAdapter } from "../adapters/base";
3
5
  import { isNativeChatRouteEligible } from "./chat-native-eligibility";
@@ -17,7 +19,7 @@ import {
17
19
  isCyberPolicyMessage,
18
20
  SEND_BUDGET_EXHAUSTED_CODE,
19
21
  } from "../lib/errors";
20
- import type { RequestExecutionBudget } from "../lib/request-execution-budget";
22
+ import { reportDispatchSends, type RequestExecutionBudget, type SingleUseDispatchPermit } from "../lib/request-execution-budget";
21
23
  import type { AdmissionLease } from "../lib/admission";
22
24
  import { readBoundedResponseBody } from "../lib/bounded-body";
23
25
  import { redactSecretString } from "../lib/redact";
@@ -198,6 +200,7 @@ export interface NativeChatExecution extends HandleNativeChatOptions {
198
200
  * the parent row, replace the one the final log settles.
199
201
  */
200
202
  sendBudget?: RequestExecutionBudget;
203
+ comboDispatchPermit?: SingleUseDispatchPermit;
201
204
  /** Replaces the request-relative first-output mark; a combo child records its own. */
202
205
  onFirstOutput?: () => void;
203
206
  /** The lease a streamed body holds; defaults to `logIds.turnAdmissionLease`. */
@@ -254,6 +257,7 @@ export function createNativeChatComboSource(input: {
254
257
  translatorBudget: input.translatorBudget,
255
258
  finishLog: child.finishLog,
256
259
  sendBudget: child.sendBudget,
260
+ comboDispatchPermit: child.comboDispatchPermit,
257
261
  onFirstOutput: child.onFirstOutput,
258
262
  ...(child.turnAdmissionLease ? { turnAdmissionLease: child.turnAdmissionLease } : {}),
259
263
  }, child.attemptHandle),
@@ -314,6 +318,7 @@ export async function runNativeChatAttempt(
314
318
  let activeProvider: OcxProviderConfig = route.provider;
315
319
  stampApiKeyAccountLabel(logCtx, route.providerName, activeProvider);
316
320
  const spendTracker = sendBudget ? undefined : attachRequestSpendTracker(req, logCtx);
321
+ const physicalBudget = sendBudget ?? createInferenceSendBudget(req, logCtx);
317
322
  let activeAdapter: ProviderAdapter = createOpenAIChatAdapter(activeProvider);
318
323
  let activeRequest: AdapterRequest;
319
324
  let retainedRequestBytes = 0;
@@ -450,30 +455,45 @@ export async function runNativeChatAttempt(
450
455
  const encoding = new Headers(init.headers).get("accept-encoding");
451
456
  if (!headers.has("accept-encoding") && encoding) headers.set("accept-encoding", encoding);
452
457
  if (init.signal?.aborted) throw init.signal.reason;
453
- if (sendBudget) {
454
- // Backstop for sends the helper cannot see coming (a reset replay). The first
455
- // report settles the combo's booking; each later one is charged and booked.
456
- if (physicalSends > 0 && sendBudget.remainingBaseSends(sharedSendCap) <= 0) {
457
- throw new SendBudgetExhaustedError(safeHostLabel(request.url));
458
+ const spendReport = createPhysicalSendReporter(physicalBudget, () => ({
459
+ poolId: logCtx.spendPoolId ?? route.providerName, identityId: logCtx.accountLogLabel,
460
+ }), execution.comboDispatchPermit);
461
+ let dispatched: Response;
462
+ try {
463
+ // Capture this request's spend policy at its first physical boundary, including
464
+ // observe-only starts; later configuration changes belong to later requests.
465
+ if (spendReport.beforeSend && !spendReport.beforeSend()) {
466
+ if ((logCtx.spendTracker as { refusals?: number } | undefined)?.refusals) throw new NativeChatSpendRefusal();
467
+ throw new SendBudgetExhaustedError();
458
468
  }
459
- physicalSends += 1;
460
- sendBudget.used += 1;
461
- } else if (!spendTracker?.charge()) throw new NativeChatSpendRefusal();
462
- noteProviderAttemptSend(logCtx, route.providerName, activeProvider, logCtx.usageLogInputTokens, transportRecovery ?? recovery);
463
- // A reselected provider transport is still a physical send: the connection policy
464
- // and manual-redirect ownership wrap the selected implementation (#4992).
465
- const dispatched = await sendWithConnectionPolicy(
466
- (activeProvider as OcxProviderTransport).fetch ?? execute,
467
- request.url,
468
- applyUpstreamRecoveryInit({
469
- ...init, method: request.method, headers, body: request.body,
470
- }, transportRecovery),
471
- // Reselection can replace the provider transport and the wire shape, so the
472
- // egress route is bound to the provider this send actually uses. Omitting it
473
- // here would let a provider transport bypass its configured route entirely,
474
- // because that transport wins over the executor that carries the binding.
475
- { providerName: route.providerName, provider: activeProvider },
476
- );
469
+ if (!physicalBudget.spendEnforced && sendBudget) {
470
+ // Backstop for sends the helper cannot see coming (a reset replay). The first
471
+ // report settles the combo's booking; each later one is charged and booked.
472
+ if (physicalSends > 0 && sendBudget.remainingBaseSends(sharedSendCap) <= 0) {
473
+ throw new SendBudgetExhaustedError(safeHostLabel(request.url));
474
+ }
475
+ physicalSends += 1;
476
+ reportDispatchSends(sendBudget, 1, execution.comboDispatchPermit);
477
+ } else if (!physicalBudget.spendEnforced && !spendTracker?.charge()) throw new NativeChatSpendRefusal();
478
+ noteProviderAttemptSend(logCtx, route.providerName, activeProvider, logCtx.usageLogInputTokens, transportRecovery ?? recovery);
479
+ // A reselected provider transport is still a physical send: the connection policy
480
+ // and manual-redirect ownership wrap the selected implementation (#4992).
481
+ dispatched = await sendWithConnectionPolicy(
482
+ (activeProvider as OcxProviderTransport).fetch ?? execute,
483
+ request.url,
484
+ applyUpstreamRecoveryInit({
485
+ ...init, method: request.method, headers, body: request.body,
486
+ }, transportRecovery),
487
+ // Reselection can replace the provider transport and the wire shape, so the
488
+ // egress route is bound to the provider this send actually uses. Omitting it
489
+ // here would let a provider transport bypass its configured route entirely,
490
+ // because that transport wins over the executor that carries the binding.
491
+ { providerName: route.providerName, provider: activeProvider },
492
+ );
493
+ } finally {
494
+ try { if (physicalBudget.spendEnforced) spendReport(1); }
495
+ finally { spendReport.close?.(); }
496
+ }
477
497
  if (!dispatched.ok) await recordKeyAttemptFailure(logCtx, dispatched, init.signal ?? upstream.signal);
478
498
  return dispatched;
479
499
  },
@@ -558,7 +578,7 @@ export async function runNativeChatAttempt(
558
578
  upstream.abort();
559
579
  if (req.signal.aborted) return fail(499, "Client cancelled request", "client_cancelled");
560
580
  const sendError = error instanceof UpstreamRetryEvidenceError ? error.cause : error;
561
- if (sendBudget && sendError instanceof SendBudgetExhaustedError) {
581
+ if (sendError instanceof SendBudgetExhaustedError) {
562
582
  // A decision this process made, answered as the Responses path answers it: 429, not 502.
563
583
  return fail(429, sendError.message, SEND_BUDGET_EXHAUSTED_CODE, SEND_BUDGET_EXHAUSTED_CODE);
564
584
  }
@@ -80,7 +80,9 @@ import { upstreamWireForAdapter } from "../protocols/contract";
80
80
  import { createProtocolEnvelope, type ProtocolEnvelope } from "../protocols/envelope";
81
81
  import { featuresFromMessagesBody, type ProtocolFeature } from "../protocols/features";
82
82
  import { credentialDomainFor, messagesBodyHasOpaqueState } from "../protocols/opaque-state";
83
- import { hasAnthropicFailoverQuorum, anthropicSessionKeyFromParts } from "../oauth/anthropic-routing";
83
+ import { anthropicRoutingFor, anthropicSessionKeyFromParts } from "../oauth/anthropic-routing";
84
+ import { configuredAnthropicInstance } from "../providers/anthropic-instance";
85
+ import { messagesSelectorTargetsSecondaryInstance, messagesSecondaryInstanceUnavailable } from "./messages-native-selector";
84
86
  import { checkRepresentable, unrepresentableMessage } from "../protocols/guard";
85
87
  import { requestPathForLane } from "../protocols/path";
86
88
  import { resolveApiSurfaceSettings, resolveProtocolSettings } from "../protocols/settings";
@@ -244,6 +246,7 @@ function wantsNativePassthrough(
244
246
  ): model is string {
245
247
  if (cc?.nativePassthrough === false) return false;
246
248
  if (typeof model !== "string" || !/^(claude|anthropic)/i.test(model)) return false;
249
+ if (messagesSelectorTargetsSecondaryInstance(config, model, cc)) return false;
247
250
  // Authorization and x-api-key both belong to the upstream on this branch. An exposed listener
248
251
  // therefore requires the dedicated admission header even though the routed Messages surface
249
252
  // keeps accepting all three legacy admission forms.
@@ -961,6 +964,11 @@ async function handleClaudeMessagesWithBudget(
961
964
  recordProtocolShadowPlan(logCtx, config, { inbound: "messages", model: requestedModel });
962
965
  return await anthropicNativePassthrough(req, config, logCtx, logIds, anthropicBody, "/v1/messages");
963
966
  }
967
+ if (isRec(anthropicBody) && typeof anthropicBody.model === "string"
968
+ && messagesSecondaryInstanceUnavailable(config, anthropicBody.model, cc)) {
969
+ if (logIds) addFinalRequestLog(logIds.requestId, logIds.start, logCtx, 401, { closeReason: "non_stream" });
970
+ return anthropicErrorResponse(401, "Configured Anthropic OAuth instance is unavailable", "authentication_error");
971
+ }
964
972
  // Only Anthropic holds message-thread state; the error makes Claude Code resend the full turn.
965
973
  if (carriesMessageThread(anthropicBody)) {
966
974
  logCtx.errorCode = "claude_thread_unsupported";
@@ -1148,10 +1156,16 @@ async function handleClaudeMessagesWithBudget(
1148
1156
 
1149
1157
  // PF-08: a managed-key Anthropic route sends its Messages body natively. The caller-forward
1150
1158
  // passthrough above was decided on the caller's own credential and never reaches this point.
1159
+ const settledInstance = configuredAnthropicInstance(config, settledRoute?.providerName);
1160
+ if (settledRoute?.providerName === "anthropic2" && settledRoute.provider.authMode === "oauth" && !settledInstance) {
1161
+ if (logIds) addFinalRequestLog(logIds.requestId, logIds.start, logCtx, 401, { closeReason: "non_stream" });
1162
+ return anthropicErrorResponse(401, "Configured Anthropic OAuth instance is unavailable", "authentication_error");
1163
+ }
1151
1164
  const nativeSelector: NativeMessagesSelector = {
1152
1165
  effortRow: !!effortRow, fastRow: !!fastRow, routeSelector: String(internalBody.model ?? ""), claudeCode: cc,
1153
1166
  // PF-10: a stored second OAuth account turns on the bridge's rotation, so it stays there.
1154
- ...(settledRoute?.provider.authMode === "oauth" ? { oauthFailoverQuorum: hasAnthropicFailoverQuorum() } : {}),
1167
+ ...(settledRoute?.provider.authMode === "oauth" && settledInstance
1168
+ ? { oauthFailoverQuorum: anthropicRoutingFor(settledInstance).hasAnthropicFailoverQuorum() } : {}),
1155
1169
  };
1156
1170
  const nativeDecline = settledRoute && isRec(anthropicBody)
1157
1171
  ? nativeMessagesDeclineReason(settledRoute, anthropicBody, config, nativeSelector)