@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
@@ -4,6 +4,8 @@ import { getMainAccountExternalUsageWarning } from "../../codex/main-account-ext
4
4
  import { getObservedMainQuotaIdentityKey } from "../../codex/main-account-cache";
5
5
  import { compactionRoutingSchema, memoryModelsSchema } from "../../config/schema/leaf-validators";
6
6
  import { compactionRecoverySchema } from "../../config/schema/compaction-recovery";
7
+ import { anthropicSidecarPatchError } from "../../config/schema/anthropic-account-pool";
8
+ import { isAnthropicInstanceId } from "../../providers/anthropic-instance-id";
7
9
  import { captureConfigTopLevelRollback } from "../../config/rebase-provenance";
8
10
  import type { IntegrationClientId } from "../../integrations/registry";
9
11
  import { randomUUID } from "node:crypto";
@@ -85,7 +87,6 @@ import { getProviderRegistryEntry } from "../../providers/registry";
85
87
  import { VISION_REASONING_EFFORTS, isVisionReasoningEffort } from "../../reasoning-effort";
86
88
  import { normalizeVisionReasoningForModel } from "../../vision/reasoning";
87
89
  import {
88
- findAnthropicVisionProvider,
89
90
  isValidVisionTimeoutMs,
90
91
  MAX_VISION_TIMEOUT_MS,
91
92
  MIN_VISION_TIMEOUT_MS,
@@ -105,6 +106,7 @@ import {
105
106
  webSearchModelIsRejected,
106
107
  webSearchModelOptionsFrom,
107
108
  webSearchModelRejection,
109
+ sidecarAnthropicPoolOptions, sidecarOptionsAuth, sidecarSettingsAfterPatch,
108
110
  } from "./web-search-sidecar-options";
109
111
  import { validateXaiSearchOptions } from "../../web-search/xai-executor";
110
112
  import { getDebugLogEntries } from "../../lib/debug-log-buffer";
@@ -150,7 +152,9 @@ async function sidecarVisionResponseSettings(config: OcxConfig): Promise<{
150
152
  const vs = config.visionSidecar ?? {};
151
153
  // Match the runtime's one selected Anthropic executor for both backend fallback
152
154
  // and catalog reachability; resolving it once prevents the two projections drifting.
153
- const anthropicSidecar = findAnthropicVisionProvider(config);
155
+ const auth = sidecarOptionsAuth(config, vs.anthropicInstance);
156
+ const anthropicSidecar = auth.isAnthropicAuth && auth.anthropicProviderName && auth.anthropicProvider
157
+ ? { providerName: auth.anthropicProviderName, provider: auth.anthropicProvider, config } : undefined;
154
158
  // The routed backend reports its own namespaced model verbatim: it is the
155
159
  // dispatched value, and collapsing it through the legacy resolver would
156
160
  // display a describer the runtime is not using (roadmap 190).
@@ -276,7 +280,7 @@ export async function syncEnabledClientIntegrations(
276
280
  },
277
281
  config,
278
282
  port,
279
- }, ["mcode", "pi", "aside", "raycast", "omo", "cline", "droid", "opencode", "kilo"]));
283
+ }, ["mcode", "pi", "aside", "raycast", "omo", "cline", "commandcode", "droid", "opencode", "kilo"]));
280
284
 
281
285
  return out;
282
286
  }
@@ -290,6 +294,8 @@ function publicVisionSidecarSettings(
290
294
  enabled: vs.enabled !== false,
291
295
  model: vision.model,
292
296
  backend: vs.backend,
297
+ ...(vs.anthropicInstance ? { anthropicInstance: vs.anthropicInstance } : {}),
298
+ anthropicPool: sidecarAnthropicPoolOptions(config, { ...vs, backend: vs.backend ?? (vision.models.find(row => row.value === vision.model)?.backend) }),
293
299
  reasoning: vision.reasoning,
294
300
  maxDescriptionsPerTurn: resolveMaxDescriptionsPerTurn(vs.maxDescriptionsPerTurn),
295
301
  timeoutMs: resolveVisionTimeoutMs(vs.timeoutMs),
@@ -882,6 +888,8 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
882
888
  enabled: ws.enabled !== false,
883
889
  model: ws.model ?? "gpt-5.6-luna",
884
890
  backend: ws.backend,
891
+ ...(ws.anthropicInstance ? { anthropicInstance: ws.anthropicInstance } : {}),
892
+ anthropicPool: sidecarAnthropicPoolOptions(config, ws),
885
893
  streamRoutedModelOutput: ws.streamRoutedModelOutput === true,
886
894
  ...(ws.xSearch ? { xSearch: ws.xSearch } : {}),
887
895
  },
@@ -902,10 +910,11 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
902
910
  if (raw.webSearch !== undefined && !isPlainRecord(raw.webSearch)) return jsonResponse({ error: "webSearch must be an object" }, 400);
903
911
  if (raw.vision !== undefined && !isPlainRecord(raw.vision)) return jsonResponse({ error: "vision must be an object" }, 400);
904
912
  const body = raw as {
905
- webSearch?: { enabled?: unknown; model?: unknown; backend?: unknown; reasoning?: unknown; streamRoutedModelOutput?: unknown; exaApiKey?: unknown; xSearch?: unknown };
913
+ webSearch?: { enabled?: unknown; model?: unknown; backend?: unknown; anthropicInstance?: unknown; reasoning?: unknown; streamRoutedModelOutput?: unknown; exaApiKey?: unknown; xSearch?: unknown };
906
914
  vision?: {
907
915
  model?: unknown;
908
916
  backend?: unknown;
917
+ anthropicInstance?: unknown;
909
918
  reasoning?: unknown;
910
919
  maxDescriptionsPerTurn?: unknown;
911
920
  enabled?: unknown;
@@ -913,6 +922,11 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
913
922
  };
914
923
  };
915
924
  const WEB_SEARCH_BACKENDS_UNION = ["openai", "anthropic", "xai", "gemini", "exa"] as const;
925
+ const instanceError = anthropicSidecarPatchError(config, {
926
+ ...(body.webSearch ? { webSearchSidecar: body.webSearch } : {}),
927
+ ...(body.vision ? { visionSidecar: body.vision } : {}),
928
+ });
929
+ if (instanceError) return jsonResponse({ error: instanceError }, 400);
916
930
  if (body.webSearch && body.webSearch.backend !== undefined && body.webSearch.backend !== null
917
931
  && !WEB_SEARCH_BACKENDS_UNION.includes(body.webSearch.backend as never)) {
918
932
  return jsonResponse({ error: "webSearch.backend must be openai, anthropic, xai, gemini, exa, or null" }, 400);
@@ -956,7 +970,7 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
956
970
  // rejection body, so a 400 cannot cost two provider fetches.
957
971
  if (body.vision && typeof body.vision.model === "string" && body.vision.model !== "") {
958
972
  const requested = body.vision.model;
959
- const candidates = await visionCandidateRows(config);
973
+ const candidates = await visionCandidateRows({ ...config, visionSidecar: sidecarSettingsAfterPatch(config.visionSidecar, body.vision) });
960
974
  const hint = body.vision.backend === "anthropic" || body.vision.backend === "openai"
961
975
  || body.vision.backend === "routed"
962
976
  ? body.vision.backend
@@ -994,7 +1008,7 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
994
1008
  // only case that owes a config.toml rewrite.
995
1009
  const webSearchEnabledBefore = config.webSearchSidecar?.enabled !== false;
996
1010
  if (body.webSearch) {
997
- const pairTouched = body.webSearch.model !== undefined || body.webSearch.backend !== undefined;
1011
+ const pairTouched = body.webSearch.model !== undefined || body.webSearch.backend !== undefined || body.webSearch.anthropicInstance !== undefined;
998
1012
  // Validate against the backend the caller SUBMITTED, across the whole
999
1013
  // union — not just openai/anthropic (#2457). The union check above has
1000
1014
  // already refused unknown literals, so a surviving string is a member;
@@ -1013,12 +1027,14 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
1013
1027
  ? body.webSearch.model || undefined
1014
1028
  : config.webSearchSidecar?.model;
1015
1029
  if (pairTouched && effectiveModel) {
1016
- const candidates = await webSearchCandidateRows(config);
1030
+ const candidates = await webSearchCandidateRows({ ...config, webSearchSidecar: sidecarSettingsAfterPatch(config.webSearchSidecar, body.webSearch) });
1017
1031
  if (webSearchModelIsRejected(effectiveBackend, effectiveModel, candidates)) {
1018
1032
  return jsonResponse(webSearchModelRejection("webSearch.model", effectiveBackend, effectiveModel, candidates), 400);
1019
1033
  }
1020
1034
  }
1021
1035
  const webSearchCandidate = { ...config.webSearchSidecar };
1036
+ if (body.webSearch.anthropicInstance === null) delete webSearchCandidate.anthropicInstance;
1037
+ else if (isAnthropicInstanceId(body.webSearch.anthropicInstance)) webSearchCandidate.anthropicInstance = body.webSearch.anthropicInstance;
1022
1038
  if (typeof body.webSearch.model === "string") {
1023
1039
  if (body.webSearch.model === "") delete webSearchCandidate.model;
1024
1040
  else webSearchCandidate.model = body.webSearch.model;
@@ -1098,6 +1114,8 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
1098
1114
  }
1099
1115
  if (body.vision) {
1100
1116
  config.visionSidecar = { ...config.visionSidecar };
1117
+ if (body.vision.anthropicInstance === null) delete config.visionSidecar.anthropicInstance;
1118
+ else if (isAnthropicInstanceId(body.vision.anthropicInstance)) config.visionSidecar.anthropicInstance = body.vision.anthropicInstance;
1101
1119
  if (typeof body.vision.model === "string") {
1102
1120
  if (body.vision.model === "") delete config.visionSidecar.model;
1103
1121
  else config.visionSidecar.model = body.vision.model;
@@ -1143,6 +1161,8 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
1143
1161
  enabled: ws.enabled !== false,
1144
1162
  model: ws.model ?? "gpt-5.6-luna",
1145
1163
  backend: ws.backend,
1164
+ ...(ws.anthropicInstance ? { anthropicInstance: ws.anthropicInstance } : {}),
1165
+ anthropicPool: sidecarAnthropicPoolOptions(config, ws),
1146
1166
  streamRoutedModelOutput: ws.streamRoutedModelOutput === true,
1147
1167
  ...(ws.xSearch ? { xSearch: ws.xSearch } : {}),
1148
1168
  },
@@ -1,4 +1,5 @@
1
1
  import { jsonResponse } from "../auth-cors";
2
+ import { jevDecisionEndpointUrl } from "../../combos/jev-decision-contract";
2
3
  import type { ManagementContext } from "./context";
3
4
  import { readManagementJsonBody, rethrowManagementBodyTooLarge } from "./body";
4
5
  import { isPlainRecord } from "./shared";
@@ -29,7 +30,7 @@ function decisionServiceIssue(
29
30
  provider: DecisionRow,
30
31
  isSystemOneEndpoint: (url: string) => boolean,
31
32
  ): { url: string; model: string; issue?: "disabled" | "endpoint" | "model" } {
32
- const url = typeof provider.baseUrl === "string" ? provider.baseUrl.trim().replace(/\/+$/, "") : "";
33
+ const url = typeof provider.baseUrl === "string" ? jevDecisionEndpointUrl(provider.baseUrl) : "";
33
34
  const model = provider.defaultModel?.trim() || provider.models?.[0]?.trim() || "";
34
35
  const issue = provider.disabled === true
35
36
  ? "disabled"
@@ -318,6 +318,25 @@ function invalidClientResponse(ctx: ManagementContext): Response {
318
318
  }, 400, ctx.req, ctx.config);
319
319
  }
320
320
 
321
+ /** Bind an explicitly scoped restore before profile delegation or mutation. */
322
+ function restoreClientScopeResponse(ctx: ManagementContext, opId: string, expectedClientId: unknown, pathClientId?: IntegrationClientId): Response | null {
323
+ if (pathClientId !== undefined) {
324
+ if (expectedClientId !== undefined && (typeof expectedClientId !== "string" || !isIntegrationClientId(expectedClientId))) return invalidClientResponse(ctx);
325
+ if (expectedClientId !== undefined && expectedClientId !== pathClientId) return jsonResponse({ error: "restore identity does not match the client path", code: "integration_client_mismatch" }, 409, ctx.req, ctx.config);
326
+ expectedClientId = pathClientId;
327
+ }
328
+ if (expectedClientId === undefined) return null;
329
+ if (typeof expectedClientId !== "string" || !isIntegrationClientId(expectedClientId)) return invalidClientResponse(ctx);
330
+ try {
331
+ const operation = integrationStore().findOperation(opId);
332
+ if (!operation) return jsonResponse({ error: "integration operation not found", code: "integration_operation_not_found", opId }, 404, ctx.req, ctx.config);
333
+ if (operation.clientId !== expectedClientId) return jsonResponse({ error: "integration operation belongs to a different client", code: "integration_client_mismatch" }, 409, ctx.req, ctx.config);
334
+ return null;
335
+ } catch (error) {
336
+ return internalErrorResponse(error, ctx);
337
+ }
338
+ }
339
+
321
340
  function isStringRecord(value: unknown): value is Record<string, string> {
322
341
  return value !== null && typeof value === "object" && !Array.isArray(value)
323
342
  && Object.values(value).every(item => typeof item === "string");
@@ -775,7 +794,7 @@ export async function handleIntegrationRoutes(ctx: ManagementContext): Promise<R
775
794
  }
776
795
  }
777
796
 
778
- if (url.pathname === "/api/client-integrations/restore/preview") {
797
+ if (url.pathname === "/api/client-integrations/restore/preview" || url.pathname === "/api/client-integrations/commandcode/restore/preview") {
779
798
  if (req.method !== "POST") return null;
780
799
  const parsed = await readJsonBody(ctx);
781
800
  if (parsed instanceof Response) return parsed;
@@ -786,6 +805,8 @@ export async function handleIntegrationRoutes(ctx: ManagementContext): Promise<R
786
805
  return jsonResponse({ error: "confirmDrift must be a boolean", code: "invalid_confirm_drift" }, 400, req, ctx.config);
787
806
  }
788
807
  const opId = parsed.opId.trim();
808
+ const scopeResponse = restoreClientScopeResponse(ctx, opId, parsed.expectedClientId, url.pathname === "/api/client-integrations/commandcode/restore/preview" ? "commandcode" : undefined);
809
+ if (scopeResponse) return scopeResponse;
789
810
  try {
790
811
  const store = integrationStore();
791
812
  const operation = store.findOperation(opId);
@@ -819,7 +840,7 @@ export async function handleIntegrationRoutes(ctx: ManagementContext): Promise<R
819
840
  }
820
841
  }
821
842
 
822
- if (url.pathname === "/api/client-integrations/restore") {
843
+ if (url.pathname === "/api/client-integrations/restore" || url.pathname === "/api/client-integrations/commandcode/restore") {
823
844
  if (req.method !== "POST") return null;
824
845
  const parsed = await readJsonBody(ctx);
825
846
  if (parsed instanceof Response) return parsed;
@@ -837,6 +858,8 @@ export async function handleIntegrationRoutes(ctx: ManagementContext): Promise<R
837
858
  }
838
859
 
839
860
  const opId = parsed.opId.trim();
861
+ const scopeResponse = restoreClientScopeResponse(ctx, opId, parsed.expectedClientId, url.pathname === "/api/client-integrations/commandcode/restore" ? "commandcode" : undefined);
862
+ if (scopeResponse) return scopeResponse;
840
863
  const confirmDrift = parsed.confirmDrift ?? false;
841
864
  const restoreBinding = planBindingOf(parsed);
842
865
  if (restoreBinding === "half") return halfBoundResponse(ctx);
@@ -1,7 +1,8 @@
1
- import { readConfigFileSnapshot, validateConfigCandidate } from "../../config/diagnostics";
2
- import { ConfigWritePublishedError } from "../../config/persist-unlocked";
1
+ import { isAnthropicOAuthInstance, configuredAnthropicInstance, type AnthropicInstanceId } from "../../providers/anthropic-instance";
2
+ import { resolveAnthropicAccountPoolConfig } from "../../oauth/anthropic-pool-config";
3
+ import { persistAnthropicPoolPatch, writeAnthropicPoolSettings } from "./anthropic-pool-settings";
3
4
  import { parseAnthropicModelRoutes, readAnthropicModelRoutes } from "../../oauth/anthropic-model-routes";
4
- import { effectiveAnthropicAccountThreshold } from "../../oauth/anthropic-account-threshold";
5
+ import { effectiveAnthropicAccountThresholdForInstance } from "../../oauth/anthropic-account-threshold";
5
6
  import { handleAnthropicAccountThreshold } from "./anthropic-account-threshold";
6
7
  import { randomBytes, randomUUID } from "node:crypto";
7
8
  import { readFileSync } from "node:fs";
@@ -18,7 +19,6 @@ import {
18
19
  readConfigDiagnostics,
19
20
  reconcileLiveConfigFromDisk,
20
21
  saveConfigPreservingClaudeCode,
21
- mutatePersistedConfig,
22
22
  } from "../../config";
23
23
  import {
24
24
  clearLoginState,
@@ -39,7 +39,7 @@ import { enrichProviderFromCatalog, listKeyLoginProviders } from "../../oauth/ke
39
39
  import { deriveProviderPresets } from "../../providers/derive";
40
40
  import { providerCodexAccountMode } from "../../providers/registry";
41
41
  import { routedSlug, slugEquals } from "../../providers/slug-codec";
42
- import { clearAccountQuotaCache, clearProviderQuotaCache, fetchProviderAccountQuotas, fetchProviderApiKeyQuotas, fetchProviderQuotaReports, providerOAuthAccountQuotaMode, providerApiKeyQuotaMode, readPassiveProviderAccountQuotas } from "../../providers/quota";
42
+ import { clearAccountQuotaCache, clearProviderQuotaCache, clearProviderQuotaCacheFor, fetchProviderAccountQuotas, fetchProviderApiKeyQuotas, fetchProviderQuotaReports, providerOAuthAccountQuotaMode, providerApiKeyQuotaMode, readPassiveProviderAccountQuotas } from "../../providers/quota";
43
43
  import { isCanonicalOpenAiForwardProvider } from "../../providers/openai-tiers";
44
44
  import { clearThreadAccountMap } from "../../codex/routing";
45
45
  import {
@@ -101,6 +101,21 @@ function isDevinCloudDirectProvider(provider: string): boolean {
101
101
  return provider === "devin" || provider === "devin-cli";
102
102
  }
103
103
 
104
+ /**
105
+ * Quota invalidation after an account mutation. A B mutation retires only B's provider-level
106
+ * report, routing row and per-account rows; A's cache, epochs and in-flight probes survive.
107
+ * Other providers keep the global clear; `accountRows` also drops that provider's account rows.
108
+ */
109
+ function clearQuotaCachesAfterAccountMutation(provider: string, accountRows: boolean): void {
110
+ if (provider === "anthropic2") {
111
+ clearProviderQuotaCacheFor(provider);
112
+ clearAccountQuotaCache(provider);
113
+ return;
114
+ }
115
+ clearProviderQuotaCache();
116
+ if (accountRows) clearAccountQuotaCache(provider);
117
+ }
118
+
104
119
  async function clearDevinCloudDirectCaches(): Promise<void> {
105
120
  const { clearCachedUserJwt, clearCachedCatalog } = await import("../../adapters/devin/cloud-direct");
106
121
  clearCachedUserJwt();
@@ -208,49 +223,12 @@ function metaMuseConsentRequired(provider: string, principal: ManagementContext[
208
223
 
209
224
  function genericOAuthProviderConfig(provider: string, config: ManagementContext["config"]) {
210
225
  const configured = config.providers[provider];
226
+ if (provider === "anthropic2" && !configuredAnthropicInstance(config, provider)) return undefined;
211
227
  if (configured) return configured;
212
228
  const definition = OAUTH_PROVIDERS[provider];
213
229
  return definition?.resolveProviderConfig?.(config) ?? definition?.providerConfig;
214
230
  }
215
231
 
216
- /** Both Anthropic writers share publication-aware recovery from the atomic mutation owner. */
217
- function persistAnthropicPoolPatch(
218
- config: OcxConfig,
219
- patch: (target: OcxConfig) => { changed: boolean; value: NonNullable<OcxConfig["anthropicAccountPool"]> },
220
- ): { status: "saved"; warning?: "config_bookkeeping_failed" } | { status: "failed"; response: Response } {
221
- const before = readConfigFileSnapshot();
222
- const unknown = () => ({ status: "failed" as const, response: jsonResponse({
223
- error: "Pool settings save state is unknown; reload settings before editing again",
224
- code: "config_save_state_unknown",
225
- }, 409) });
226
- try {
227
- const saved = mutatePersistedConfig(patch);
228
- if (saved.status === "unavailable") return unknown();
229
- config.anthropicAccountPool = saved.value;
230
- reconcileLiveStateStores();
231
- return { status: "saved" };
232
- } catch (error) {
233
- const persisted = readConfigFileSnapshot();
234
- if (persisted.diagnostics.source !== "file" || persisted.raw === undefined) return unknown();
235
- // File reads may salvage hand edits; recovery success requires a strict, authoritative document.
236
- let validated: ReturnType<typeof validateConfigCandidate>;
237
- try { validated = validateConfigCandidate(JSON.parse(persisted.raw.replace(/^\uFEFF/, ""))); }
238
- catch { return unknown(); }
239
- if (!validated.ok) return unknown();
240
- const current = validated.config;
241
- const expected = patch(structuredClone(current)).value;
242
- const matches = JSON.stringify(current.anthropicAccountPool) === JSON.stringify(expected);
243
- if (matches) {
244
- config.anthropicAccountPool = current.anthropicAccountPool;
245
- // A failed reconcile must not turn confirmed publication back into a rollback.
246
- try { reconcileLiveStateStores(); } catch { /* Fixed warning covers bookkeeping failure. */ }
247
- return { status: "saved", warning: "config_bookkeeping_failed" };
248
- }
249
- if (error instanceof ConfigWritePublishedError || persisted.raw !== before.raw) return unknown();
250
- return { status: "failed", response: jsonResponse({ error: "Pool settings could not be saved" }, 500) };
251
- }
252
- }
253
-
254
232
  export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<Response | null> {
255
233
  const { req, url, config, deps, principal, syncClaudeAgentDefsBestEffort } = ctx;
256
234
 
@@ -422,9 +400,7 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
422
400
  clearModelCache(provider);
423
401
  clearGatherRoutedModelsInflight();
424
402
  // Drop cached/last-good quota rows tied to the removed credential.
425
- const { clearProviderQuotaCache, clearAccountQuotaCache } = await import("../../providers/quota");
426
- clearProviderQuotaCache();
427
- clearAccountQuotaCache(provider);
403
+ clearQuotaCachesAfterAccountMutation(provider, true);
428
404
  // The cached user_jwt's payload contains the api_key, and the catalog is
429
405
  // keyed by that key. Without this they outlive the credential in process
430
406
  // memory until the JWT's own ~24 minute expiry. `devin-cli` is a deprecated
@@ -445,7 +421,7 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
445
421
  const { isGenericFailoverProvider, kiroAutoSelection } = await import("../../oauth/generic-account-failover");
446
422
  const effectiveProvider = genericOAuthProviderConfig(provider, config);
447
423
  const supportsPause = effectiveProvider !== undefined
448
- && (provider === "anthropic" && effectiveProvider.authMode === "oauth"
424
+ && (isAnthropicOAuthInstance(provider) && effectiveProvider.authMode === "oauth"
449
425
  || isGenericFailoverProvider(provider, effectiveProvider));
450
426
  const {
451
427
  oauthAccountHealthFields,
@@ -456,7 +432,7 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
456
432
  const set = getAccountSet(provider);
457
433
  const current = getLoginStatus(provider, emailMaskingEnabled(config));
458
434
  return {
459
- activeAccountId: current.activeAccountId ?? null,
435
+ ...(provider === "anthropic2" ? { provider } : {}), activeAccountId: current.activeAccountId ?? null,
460
436
  accounts: (current.accounts ?? []).map(summary => {
461
437
  const full = set?.accounts.find(account => account.id === summary.id);
462
438
  const health = full
@@ -467,9 +443,9 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
467
443
  });
468
444
  return { ...summary, ...oauthAccountHealthFields(provider, summary.id, health), quotaMode,
469
445
  ...(supportsPause ? { paused: full?.paused === true } : {}),
470
- ...(provider === "anthropic" && supportsPause ? { autoSwitchThresholdOverride: full?.autoSwitchThresholdOverride ?? null,
471
- effectiveAutoSwitchThreshold: effectiveAnthropicAccountThreshold(config, full),
472
- autoSwitchThreshold: effectiveAnthropicAccountThreshold(config) } : {}),
446
+ ...(isAnthropicOAuthInstance(provider) && supportsPause ? { autoSwitchThresholdOverride: full?.autoSwitchThresholdOverride ?? null,
447
+ effectiveAutoSwitchThreshold: effectiveAnthropicAccountThresholdForInstance(provider as AnthropicInstanceId, config, full),
448
+ autoSwitchThreshold: effectiveAnthropicAccountThresholdForInstance(provider as AnthropicInstanceId, config) } : {}),
473
449
  ...(provider === "kiro" && full ? kiroAutoSelection(full) : {}) };
474
450
  }),
475
451
  };
@@ -489,11 +465,11 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
489
465
  // from the post-probe store so the response is not stale.
490
466
  const rows = passiveQuota
491
467
  ? readPassiveProviderAccountQuotas(provider)
492
- : await fetchProviderAccountQuotas(provider, forceRefresh, quotaProvider);
468
+ : await fetchProviderAccountQuotas(provider, forceRefresh, quotaProvider, config);
493
469
  const byId = new Map(rows.map(row => [row.accountId, row]));
494
470
  const projected = projectAccounts();
495
471
  return jsonResponse({
496
- activeAccountId: projected.activeAccountId,
472
+ ...(provider === "anthropic2" ? { provider } : {}), activeAccountId: projected.activeAccountId,
497
473
  accounts: projected.accounts.map(account => {
498
474
  const row = byId.get(account.id);
499
475
  if (!row) return account;
@@ -514,6 +490,7 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
514
490
  const body = await readManagementJsonBodyOr(req, {}) as { provider?: string; accountId?: string };
515
491
  const provider = (body.provider ?? "").trim().toLowerCase();
516
492
  if (!isPublicOAuthProvider(provider)) return jsonResponse({ error: "unknown oauth provider" }, 400);
493
+ if (provider === "anthropic2" && !configuredAnthropicInstance(config, provider)) return jsonResponse({ error: "Anthropic pool instance is not configured" }, 409);
517
494
  if (!body.accountId) return jsonResponse({ error: "missing accountId" }, 400);
518
495
  const { getAccountCredentialWithStatus, setActiveAccount } = await import("../../oauth/store");
519
496
  const current = getAccountCredentialWithStatus(provider, body.accountId);
@@ -532,16 +509,15 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
532
509
  // side carries resetCodexRoutingForManualSelection for.
533
510
  const { genericPoolKey, seedPoolRotationAccount } = await import("../../oauth/pool-kernel");
534
511
  seedPoolRotationAccount(genericPoolKey(provider), body.accountId);
535
- if (provider === "anthropic") {
536
- const { resetAnthropicRoutingForManualSelection } = await import("../../oauth/anthropic-routing");
537
- resetAnthropicRoutingForManualSelection(body.accountId);
512
+ if (isAnthropicOAuthInstance(provider)) {
513
+ const { anthropicRoutingFor } = await import("../../oauth/anthropic-routing");
514
+ anthropicRoutingFor(provider).resetAnthropicRoutingForManualSelection(body.accountId);
538
515
  }
539
516
  const { clearModelCache } = await import("../../codex/model-cache");
540
517
  const { clearGatherRoutedModelsInflight } = await import("../../codex/catalog");
541
518
  clearModelCache(provider);
542
519
  clearGatherRoutedModelsInflight();
543
- const { clearProviderQuotaCache } = await import("../../providers/quota");
544
- clearProviderQuotaCache();
520
+ clearQuotaCachesAfterAccountMutation(provider, false);
545
521
  return jsonResponse({ ok: true, provider, activeAccountId: body.accountId });
546
522
  }
547
523
 
@@ -558,7 +534,7 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
558
534
 
559
535
  const { isGenericFailoverProvider } = await import("../../oauth/generic-account-failover");
560
536
  const effectiveProvider = genericOAuthProviderConfig(provider, config);
561
- if (!effectiveProvider || !(provider === "anthropic" && effectiveProvider.authMode === "oauth"
537
+ if (!effectiveProvider || !(isAnthropicOAuthInstance(provider) && effectiveProvider.authMode === "oauth"
562
538
  || isGenericFailoverProvider(provider, effectiveProvider))) {
563
539
  return jsonResponse({ error: "account pause is not supported for this OAuth provider" }, 400);
564
540
  }
@@ -568,9 +544,9 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
568
544
  if (result.status === "not-found") return jsonResponse({ error: "account not found" }, 404);
569
545
 
570
546
  if (result.activeAccountChanged) {
571
- if (provider === "anthropic") {
572
- const { resetAnthropicRoutingForManualSelection } = await import("../../oauth/anthropic-routing");
573
- resetAnthropicRoutingForManualSelection(result.activeAccountId);
547
+ if (isAnthropicOAuthInstance(provider)) {
548
+ const { anthropicRoutingFor } = await import("../../oauth/anthropic-routing");
549
+ anthropicRoutingFor(provider).resetAnthropicRoutingForManualSelection(result.activeAccountId);
574
550
  } else {
575
551
  const { genericPoolKey, seedPoolRotationAccount } = await import("../../oauth/pool-kernel");
576
552
  seedPoolRotationAccount(genericPoolKey(provider), result.activeAccountId);
@@ -579,8 +555,7 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
579
555
  const { clearGatherRoutedModelsInflight } = await import("../../codex/catalog");
580
556
  clearModelCache(provider);
581
557
  clearGatherRoutedModelsInflight();
582
- const { clearProviderQuotaCache } = await import("../../providers/quota");
583
- clearProviderQuotaCache();
558
+ clearQuotaCachesAfterAccountMutation(provider, false);
584
559
  }
585
560
 
586
561
  return jsonResponse({
@@ -671,7 +646,7 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
671
646
  if (autoSwitchThreshold !== undefined) config.autoSwitchThreshold = autoSwitchThreshold;
672
647
  } else if (kind === "anthropic") {
673
648
  const patchPool = (target: OcxConfig) => {
674
- const pool = { ...(target.anthropicAccountPool ?? {}) };
649
+ const pool = { ...resolveAnthropicAccountPoolConfig(target, provider as AnthropicInstanceId) };
675
650
  if (fields.nativeMessages !== undefined) pool.nativeMessages = fields.nativeMessages as boolean;
676
651
  if (fields.enabled !== undefined) pool.enabled = fields.enabled as boolean;
677
652
  if (strategy !== undefined) pool.strategy = strategy as never;
@@ -682,10 +657,10 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
682
657
  if (fields.routes === null) delete pool.routes;
683
658
  else if (parsedRoutes?.ok) pool.routes = parsedRoutes.routes;
684
659
  }
685
- target.anthropicAccountPool = pool;
660
+ writeAnthropicPoolSettings(target, provider as AnthropicInstanceId, pool);
686
661
  return { changed: true, value: pool };
687
662
  };
688
- const saved = persistAnthropicPoolPatch(config, patchPool);
663
+ const saved = persistAnthropicPoolPatch(config, provider as AnthropicInstanceId, patchPool);
689
664
  if (saved.status === "failed") return saved.response;
690
665
  return jsonResponse({ ...unifiedPoolSettingsDto(config, provider, kind), ...(saved.warning ? { warning: saved.warning } : {}) });
691
666
  } else {
@@ -710,7 +685,7 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
710
685
  // Opt-in Anthropic OAuth account pool (#294): enable/threshold/strategy + clear cooldown.
711
686
  if (url.pathname === "/api/oauth/accounts/pool" && req.method === "GET") {
712
687
  const provider = (url.searchParams.get("provider") ?? "").trim().toLowerCase();
713
- if (provider !== "anthropic") {
688
+ if (!isAnthropicOAuthInstance(provider)) {
714
689
  // Generic OAuth pool-settings contract (#695 slice 1): persisted per provider. `strategy`
715
690
  // and `autoSwitchThreshold` stay inert until the selector consumes them; `enabled` already
716
691
  // governs the pre-dispatch account preference. Codex keeps /api/codex-auth; api-key
@@ -722,7 +697,8 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
722
697
  }
723
698
  return jsonResponse(genericPoolSettingsDto(provider, prov, config.pool?.kernel === true));
724
699
  }
725
- const pool = config.anthropicAccountPool ?? {};
700
+ if (provider === "anthropic2" && !configuredAnthropicInstance(config, provider)) return jsonResponse({ error: "Anthropic pool instance is not configured" }, 409);
701
+ const pool = resolveAnthropicAccountPoolConfig(config, provider);
726
702
  return jsonResponse({
727
703
  provider,
728
704
  enabled: pool.enabled === true,
@@ -753,10 +729,10 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
753
729
  };
754
730
  const provider = typeof body.provider === "string" ? body.provider.trim().toLowerCase() : "";
755
731
  if (Object.hasOwn(body, "nativeMessages")) {
756
- if (provider !== "anthropic") return jsonResponse({ error: "nativeMessages is only part of the anthropic pool contract" }, 400);
732
+ if (!isAnthropicOAuthInstance(provider)) return jsonResponse({ error: "nativeMessages is only part of the anthropic pool contract" }, 400);
757
733
  if (typeof body.nativeMessages !== "boolean") return jsonResponse({ error: "nativeMessages must be a boolean" }, 400);
758
734
  }
759
- if (provider !== "anthropic") {
735
+ if (!isAnthropicOAuthInstance(provider)) {
760
736
  const {
761
737
  poolSettingsCapability, genericPoolSettingsDto, parseGenericPoolStrategy, parseGenericAutoSwitchThreshold,
762
738
  parseGenericStickyLimit, parseKiroAccountCap,
@@ -810,12 +786,13 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
810
786
  saveConfigPreservingClaudeCode(config);
811
787
  return jsonResponse({ ok: true, ...genericPoolSettingsDto(provider, prov, config.pool?.kernel === true) });
812
788
  }
813
- let enabled = config.anthropicAccountPool?.enabled === true;
789
+ if (provider === "anthropic2" && !configuredAnthropicInstance(config, provider)) return jsonResponse({ error: "Anthropic pool instance is not configured" }, 409);
790
+ let enabled = resolveAnthropicAccountPoolConfig(config, provider)?.enabled === true;
814
791
  if (body.enabled !== undefined) {
815
792
  if (typeof body.enabled !== "boolean") return jsonResponse({ error: "enabled must be a boolean" }, 400);
816
793
  enabled = body.enabled;
817
794
  }
818
- let threshold = config.anthropicAccountPool?.autoSwitchThreshold ?? 80;
795
+ let threshold = resolveAnthropicAccountPoolConfig(config, provider)?.autoSwitchThreshold ?? 80;
819
796
  if (body.autoSwitchThreshold !== undefined) {
820
797
  if (
821
798
  typeof body.autoSwitchThreshold !== "number"
@@ -827,7 +804,7 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
827
804
  }
828
805
  threshold = body.autoSwitchThreshold;
829
806
  }
830
- let strategy = config.anthropicAccountPool?.strategy;
807
+ let strategy = resolveAnthropicAccountPoolConfig(config, provider)?.strategy;
831
808
  if (body.strategy !== undefined) {
832
809
  const parsed = parseAccountPoolStrategy(body.strategy);
833
810
  if (parsed === null) {
@@ -835,7 +812,7 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
835
812
  }
836
813
  strategy = parsed;
837
814
  }
838
- let stickyLimit = config.anthropicAccountPool?.stickyLimit;
815
+ let stickyLimit = resolveAnthropicAccountPoolConfig(config, provider)?.stickyLimit;
839
816
  if (body.stickyLimit !== undefined) {
840
817
  const parsed = parseAccountPoolStickyLimit(body.stickyLimit);
841
818
  if (parsed === null) {
@@ -843,7 +820,7 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
843
820
  }
844
821
  stickyLimit = parsed;
845
822
  }
846
- let quotaWindow = config.anthropicAccountPool?.quotaWindow;
823
+ let quotaWindow = resolveAnthropicAccountPoolConfig(config, provider)?.quotaWindow;
847
824
  if (body.quotaWindow !== undefined) {
848
825
  const parsed = parseAccountPoolQuotaWindow(body.quotaWindow);
849
826
  if (parsed === null) {
@@ -855,7 +832,7 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
855
832
  ? parseAnthropicModelRoutes(body.routes) : null;
856
833
  if (parsedLegacyRoutes && !parsedLegacyRoutes.ok) return jsonResponse({ error: parsedLegacyRoutes.error }, 400);
857
834
  const legacyPatch = (target: OcxConfig) => {
858
- const pool = { ...(target.anthropicAccountPool ?? {}) };
835
+ const pool = { ...resolveAnthropicAccountPoolConfig(target, provider as AnthropicInstanceId) };
859
836
  if (body.enabled !== undefined) pool.enabled = enabled;
860
837
  else if (pool.enabled === undefined) pool.enabled = false;
861
838
  if (body.nativeMessages !== undefined) pool.nativeMessages = body.nativeMessages as boolean;
@@ -868,21 +845,21 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
868
845
  if (body.routes === null) delete pool.routes;
869
846
  else if (parsedLegacyRoutes?.ok) pool.routes = parsedLegacyRoutes.routes;
870
847
  }
871
- target.anthropicAccountPool = pool;
848
+ writeAnthropicPoolSettings(target, provider as AnthropicInstanceId, pool);
872
849
  return { changed: true, value: pool };
873
850
  };
874
- const saved = persistAnthropicPoolPatch(config, legacyPatch);
851
+ const saved = persistAnthropicPoolPatch(config, provider as AnthropicInstanceId, legacyPatch);
875
852
  if (saved.status === "failed") return saved.response;
876
853
  return jsonResponse({
877
854
  ok: true,
878
855
  provider,
879
- enabled: config.anthropicAccountPool?.enabled === true,
880
- autoSwitchThreshold: config.anthropicAccountPool?.autoSwitchThreshold ?? 80,
881
- strategy: normalizeAccountPoolStrategy(config.anthropicAccountPool?.strategy),
882
- stickyLimit: normalizeAccountPoolStickyLimit(config.anthropicAccountPool?.stickyLimit),
883
- quotaWindow: normalizeAccountPoolQuotaWindow(config.anthropicAccountPool?.quotaWindow),
884
- routes: config.anthropicAccountPool?.routes ?? null,
885
- nativeMessages: !Object.hasOwn(config.anthropicAccountPool ?? {}, "nativeMessages") || config.anthropicAccountPool?.nativeMessages === true,
856
+ enabled: resolveAnthropicAccountPoolConfig(config, provider)?.enabled === true,
857
+ autoSwitchThreshold: resolveAnthropicAccountPoolConfig(config, provider)?.autoSwitchThreshold ?? 80,
858
+ strategy: normalizeAccountPoolStrategy(resolveAnthropicAccountPoolConfig(config, provider)?.strategy),
859
+ stickyLimit: normalizeAccountPoolStickyLimit(resolveAnthropicAccountPoolConfig(config, provider)?.stickyLimit),
860
+ quotaWindow: normalizeAccountPoolQuotaWindow(resolveAnthropicAccountPoolConfig(config, provider)?.quotaWindow),
861
+ routes: resolveAnthropicAccountPoolConfig(config, provider)?.routes ?? null,
862
+ nativeMessages: !Object.hasOwn(resolveAnthropicAccountPoolConfig(config, provider), "nativeMessages") || resolveAnthropicAccountPoolConfig(config, provider)?.nativeMessages === true,
886
863
  ...(saved.warning ? { warning: saved.warning } : {}),
887
864
  experimental: true,
888
865
  });
@@ -891,10 +868,11 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
891
868
  const body = await readManagementJsonBodyOr(req, {}) as { provider?: unknown; accountId?: unknown };
892
869
  const provider = typeof body.provider === "string" ? body.provider.trim().toLowerCase() : "";
893
870
  const accountId = typeof body.accountId === "string" ? body.accountId.trim() : "";
894
- if (provider !== "anthropic") return jsonResponse({ error: "clear-cooldown is only supported for anthropic" }, 400);
871
+ if (!isAnthropicOAuthInstance(provider)) return jsonResponse({ error: "clear-cooldown is only supported for anthropic" }, 400);
872
+ if (provider === "anthropic2" && !configuredAnthropicInstance(config, provider)) return jsonResponse({ error: "Anthropic pool instance is not configured" }, 409);
895
873
  if (!accountId) return jsonResponse({ error: "missing accountId" }, 400);
896
- const { clearAnthropicAccountCooldown } = await import("../../oauth/anthropic-routing");
897
- const cleared = clearAnthropicAccountCooldown(accountId);
874
+ const { anthropicRoutingFor } = await import("../../oauth/anthropic-routing");
875
+ const cleared = anthropicRoutingFor(provider).clearAnthropicAccountCooldown(accountId);
898
876
  return jsonResponse({ ok: true, cleared });
899
877
  }
900
878
 
@@ -936,8 +914,7 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
936
914
  const { clearGatherRoutedModelsInflight } = await import("../../codex/catalog");
937
915
  clearModelCache(provider);
938
916
  clearGatherRoutedModelsInflight();
939
- clearProviderQuotaCache();
940
- clearAccountQuotaCache(provider);
917
+ clearQuotaCachesAfterAccountMutation(provider, true);
941
918
  }
942
919
  if (!imported.ok) return jsonResponse({ code: imported.code }, imported.status);
943
920
  return jsonResponse(imported.result);
@@ -969,19 +946,17 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
969
946
  const { removeAccount, getAccountSet } = await import("../../oauth/store");
970
947
  if (!(await removeAccount(provider, id))) return jsonResponse({ error: "account not found" }, 404);
971
948
  reconcileLiveStateStores();
972
- if (provider === "anthropic") {
973
- const { clearAnthropicAccountCooldown, clearAnthropicSessionAffinityForAccount } = await import("../../oauth/anthropic-routing");
974
- clearAnthropicAccountCooldown(id);
975
- clearAnthropicSessionAffinityForAccount(id);
949
+ if (isAnthropicOAuthInstance(provider)) {
950
+ const { anthropicRoutingFor } = await import("../../oauth/anthropic-routing");
951
+ anthropicRoutingFor(provider).clearAnthropicAccountCooldown(id);
952
+ anthropicRoutingFor(provider).clearAnthropicSessionAffinityForAccount(id);
976
953
  }
977
954
  if (!getAccountSet(provider)) clearLoginState(provider);
978
955
  const { clearModelCache } = await import("../../codex/model-cache");
979
956
  const { clearGatherRoutedModelsInflight } = await import("../../codex/catalog");
980
957
  clearModelCache(provider);
981
958
  clearGatherRoutedModelsInflight();
982
- const { clearProviderQuotaCache, clearAccountQuotaCache } = await import("../../providers/quota");
983
- clearProviderQuotaCache();
984
- clearAccountQuotaCache(provider);
959
+ clearQuotaCachesAfterAccountMutation(provider, true);
985
960
  // Same reasoning as logout. Removing the last account for a provider used to
986
961
  // leave the JWT and catalog in memory, because only the logout route cleared
987
962
  // them.
@@ -1168,7 +1143,13 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
1168
1143
  if (principal !== "gui-session") {
1169
1144
  return jsonResponse({ error: "dashboard session required" }, 403, req, config);
1170
1145
  }
1146
+ // Local page bootstrap is sufficient for the dashboard, not stored-secret disclosure.
1147
+ const authorized = () => ctx.sessionControl?.canRevealDataKeys?.(req, config) === true;
1148
+ const denied = () => jsonResponse({ error: "operator-authorized dashboard session required" }, 403, req, config);
1149
+ if (!authorized()) return denied();
1171
1150
  const body = await readJsonBody(req);
1151
+ // Body reception can outlive the session or its revocation; do not trust cached admission.
1152
+ if (!authorized()) return denied();
1172
1153
  if (!body || Object.keys(body).length !== 1 || typeof body.id !== "string" || !body.id) {
1173
1154
  return jsonResponse({ error: "invalid body" }, 400, req, config);
1174
1155
  }