@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.
- package/README.md +3 -3
- package/gui/dist/assets/App-7yhoys8a.js +54 -0
- package/gui/dist/assets/Tray-EwAOsP0S.js +1 -0
- package/gui/dist/assets/index-RsoaAxXY.js +94 -0
- package/gui/dist/assets/quota-summary-D2lN8tDm.js +1 -0
- package/gui/dist/index.html +1 -1
- package/gui/dist/provider-icons/claude-green.svg +1 -0
- package/package.json +3 -2
- package/src/adapters/anthropic-model-contract.ts +8 -2
- package/src/adapters/anthropic.ts +7 -2
- package/src/adapters/codebuddy/mcp-server.ts +2 -25
- package/src/adapters/codebuddy/tool-bridge.ts +1 -7
- package/src/adapters/coding-agent/mcp-server.ts +33 -0
- package/src/adapters/coding-agent/protocol.ts +122 -17
- package/src/adapters/coding-agent/tool-catalog.ts +75 -0
- package/src/adapters/coding-agent/turn.ts +101 -44
- package/src/adapters/cursor/catalog.ts +8 -0
- package/src/adapters/cursor/effort-map.ts +2 -0
- package/src/adapters/devin/live-models.ts +2 -0
- package/src/adapters/kiro-retry.ts +9 -4
- package/src/adapters/openai-chat/messages.ts +7 -6
- package/src/adapters/physical-send.ts +8 -5
- package/src/adapters/qoder/adapter.ts +41 -6
- package/src/adapters/qoder/scaffold-guard.ts +2 -3
- package/src/adapters/zed.ts +16 -1
- package/src/claude/inbound-cache-stabilize.ts +4 -9
- package/src/claude/inbound.ts +1 -0
- package/src/claude/intercept/model-bindings.ts +1 -0
- package/src/claude/intercept/picker-listener.ts +71 -4
- package/src/cli/account-anthropic-threshold.ts +14 -5
- package/src/cli/account-api.ts +3 -1
- package/src/cli/account-extended.ts +13 -9
- package/src/cli/account-policy-dto.ts +1 -1
- package/src/cli/account-policy.ts +13 -4
- package/src/cli/account.ts +3 -3
- package/src/cli/capabilities-accounts.ts +2 -2
- package/src/cli/capabilities-agents-routing.ts +24 -0
- package/src/cli/capabilities-integrations.ts +16 -0
- package/src/cli/codex-shim-autorestore.ts +3 -0
- package/src/cli/dispatch.ts +10 -1
- package/src/cli/help.ts +4 -1
- package/src/cli/index.ts +11 -5
- package/src/cli/integration-plan-dto.ts +1 -0
- package/src/cli/integration-preview.ts +8 -1
- package/src/cli/integrations.ts +32 -3
- package/src/cli/message-args.ts +34 -0
- package/src/cli/message-command.ts +43 -0
- package/src/cli/message-runtime.ts +6 -0
- package/src/cli/observe.ts +3 -1
- package/src/cli/provider.ts +29 -6
- package/src/cli/registry.ts +24 -2
- package/src/clients/config-export/commandcode.ts +131 -0
- package/src/clients/config-export/contracts.ts +12 -0
- package/src/clients/config-export.ts +64 -1
- package/src/codex/auth-api/account-list.ts +11 -3
- package/src/codex/auth-api/main-account-probe.ts +25 -8
- package/src/codex/auth-api/pool-mode-gate.ts +33 -9
- package/src/codex/auth-api/pool-quota-probe.ts +15 -1
- package/src/codex/auth-api/routes.ts +1 -0
- package/src/codex/catalog/auto-review.ts +13 -6
- package/src/codex/catalog/build-entries.ts +15 -7
- package/src/codex/catalog/control-plane.ts +45 -0
- package/src/codex/catalog/effort.ts +11 -1
- package/src/codex/catalog/gather-capture.ts +5 -3
- package/src/codex/catalog/parsing.ts +2 -1
- package/src/codex/catalog/provider-models.ts +15 -9
- package/src/codex/control-plane-models.ts +6 -0
- package/src/combos/jev-decision-contract.ts +25 -2
- package/src/combos/jev.ts +3 -2
- package/src/combos/types.ts +1 -1
- package/src/config/diagnostics.ts +27 -16
- package/src/config/load-degrade.ts +34 -0
- package/src/config/schema/anthropic-account-pool.ts +143 -0
- package/src/config/schema/config-schema.ts +7 -1
- package/src/config/schema/leaf-validators.ts +4 -0
- package/src/config.ts +2 -0
- package/src/generated/compatibility-version.json +320 -184
- package/src/generated/model-metadata.ts +5 -4
- package/src/images/loop.ts +2 -1
- package/src/integrations/catalog-refresh.ts +1 -1
- package/src/integrations/mutation-plan.ts +7 -0
- package/src/integrations/registry.ts +8 -0
- package/src/integrations/state.ts +9 -2
- package/src/integrations/writer.ts +8 -2
- package/src/lib/lab-live-route-production.ts +9 -1
- package/src/lib/request-execution-budget.ts +236 -58
- package/src/lib/spend-pool-alias-validation.ts +79 -0
- package/src/lib/spend-pool-continuity.ts +34 -0
- package/src/lib/spend-reservation-ledger.ts +412 -50
- package/src/lib/state-store-registrations.ts +20 -3
- package/src/lib/upstream-retry.ts +17 -5
- package/src/lib/workflow-budget.ts +53 -11
- package/src/messaging/budget.ts +45 -0
- package/src/messaging/discovery.ts +57 -0
- package/src/messaging/envelope.ts +49 -0
- package/src/messaging/input.ts +37 -0
- package/src/messaging/rpc.ts +196 -0
- package/src/messaging/send.ts +58 -0
- package/src/messaging/socket.ts +72 -0
- package/src/messaging/types.ts +29 -0
- package/src/oauth/anthropic-account-refusal.ts +51 -17
- package/src/oauth/anthropic-account-threshold.ts +8 -2
- package/src/oauth/anthropic-continuity.ts +15 -1
- package/src/oauth/anthropic-model-quota.ts +120 -48
- package/src/oauth/anthropic-model-routes.ts +11 -3
- package/src/oauth/anthropic-oauth-definitions.ts +98 -0
- package/src/oauth/anthropic-pool-config.ts +30 -0
- package/src/oauth/anthropic-rate-limit-policy.ts +64 -9
- package/src/oauth/anthropic-routing.ts +1202 -866
- package/src/oauth/anthropic-send-ownership.ts +41 -0
- package/src/oauth/anthropic.ts +4 -1
- package/src/oauth/generic-account-failover.ts +3 -1
- package/src/oauth/health.ts +8 -6
- package/src/oauth/index.ts +90 -173
- package/src/oauth/kiro.ts +22 -9
- package/src/oauth/login-cli.ts +2 -0
- package/src/oauth/model-discovery-auth.ts +43 -0
- package/src/oauth/pool-kernel.ts +11 -5
- package/src/oauth/pool-settings-capability.ts +5 -3
- package/src/oauth/store-anthropic-instance.ts +87 -0
- package/src/oauth/store.ts +67 -9
- package/src/oauth/token-guardian.ts +2 -0
- package/src/oauth/types.ts +2 -0
- package/src/protocols/plan-snapshot.ts +5 -0
- package/src/protocols/settings.ts +20 -5
- package/src/providers/anthropic-instance-id.ts +47 -0
- package/src/providers/anthropic-instance.ts +48 -0
- package/src/providers/anthropic-reset-grant-ledger.ts +11 -1
- package/src/providers/derive.ts +2 -0
- package/src/providers/kiro-models.ts +3 -0
- package/src/providers/label.ts +1 -1
- package/src/providers/model-rename-fields.ts +2 -0
- package/src/providers/quota/account-cache.ts +37 -11
- package/src/providers/quota/anthropic-account-quota.ts +93 -0
- package/src/providers/quota/anthropic-cooldown-recovery.ts +222 -111
- package/src/providers/quota/report-cache.ts +24 -1
- package/src/providers/quota/vendor-probes-oauth.ts +46 -14
- package/src/providers/quota.ts +22 -32
- package/src/providers/registry/entries-core.ts +35 -30
- package/src/providers/registry/model-ids.ts +1 -0
- package/src/providers/registry/model-seeds.ts +7 -4
- package/src/providers/registry/types.ts +3 -0
- package/src/providers/registry.ts +3 -3
- package/src/responses/spill-store.ts +5 -1
- package/src/responses/state/metrics.ts +87 -0
- package/src/responses/state/snapshot-codec.ts +6 -0
- package/src/responses/state/spill-queue.ts +1 -0
- package/src/responses/state/unforced-store-false.ts +55 -0
- package/src/responses/state.ts +50 -66
- package/src/router.ts +28 -4
- package/src/routing/compatibility/assemble.ts +1 -1
- package/src/routing/quota.ts +7 -3
- package/src/server/auth-cors.ts +5 -0
- package/src/server/chat-completions.ts +6 -1
- package/src/server/chat-native.ts +45 -25
- package/src/server/claude-messages.ts +16 -2
- package/src/server/gui-static.ts +38 -4
- package/src/server/index/spend-ledger-lifecycle.ts +10 -4
- package/src/server/index.ts +4 -2
- package/src/server/inference/context.ts +1 -1
- package/src/server/lifecycle.ts +1 -1
- package/src/server/management/agent-settings-routes.ts +30 -6
- package/src/server/management/anthropic-account-threshold.ts +12 -9
- package/src/server/management/anthropic-pool-settings.ts +72 -0
- package/src/server/management/anthropic-reset-grant-routes.ts +98 -39
- package/src/server/management/config-routes.ts +27 -7
- package/src/server/management/decision-routes.ts +2 -1
- package/src/server/management/integration-routes.ts +25 -2
- package/src/server/management/oauth-account-routes.ts +79 -98
- package/src/server/management/provider-instance-ownership.ts +44 -0
- package/src/server/management/provider-routes.ts +21 -28
- package/src/server/management/route-registry.ts +3 -1
- package/src/server/management/routing-profile-routes.ts +4 -4
- package/src/server/management/system-restart.ts +44 -14
- package/src/server/management/system-routes.ts +24 -2
- package/src/server/management/vision-sidecar-options.ts +9 -6
- package/src/server/management/web-search-sidecar-options.ts +51 -2
- package/src/server/management-auth.ts +6 -0
- package/src/server/messages-native-eligibility.ts +3 -2
- package/src/server/messages-native-oauth.ts +86 -41
- package/src/server/messages-native-selector.ts +39 -0
- package/src/server/messages-native.ts +84 -25
- package/src/server/request-log.ts +5 -0
- package/src/server/responses/adapter-continuation.ts +29 -17
- package/src/server/responses/adapter-dispatch.ts +47 -17
- package/src/server/responses/antigravity-validation-refusal.ts +20 -6
- package/src/server/responses/compact.ts +25 -8
- package/src/server/responses/compaction-recovery.ts +1 -1
- package/src/server/responses/core-combo-native.ts +4 -1
- package/src/server/responses/core-combo.ts +16 -11
- package/src/server/responses/core-normalize.ts +1 -0
- package/src/server/responses/core-options.ts +3 -1
- package/src/server/responses/core-replay.ts +5 -0
- package/src/server/responses/passthrough-delivery.ts +7 -3
- package/src/server/responses/passthrough-dispatch.ts +21 -8
- package/src/server/responses/request-prepare.ts +37 -5
- package/src/server/responses/request-send-budget.ts +74 -37
- package/src/server/responses/request-spend.ts +325 -33
- package/src/server/responses/request-transport.ts +132 -52
- package/src/server/responses/run-turn-execution.ts +11 -3
- package/src/server/responses/sidecar-execution.ts +96 -14
- package/src/server/responses-undeclared-tool-guard.ts +5 -1
- package/src/server/workflow-refusal.ts +30 -0
- package/src/service/ownership-mutation-lease.d.mts +1 -1
- package/src/service/ownership-mutation-lease.mjs +5 -7
- package/src/sidecar/anthropic-binding.ts +79 -0
- package/src/sidecar/auth.ts +76 -2
- package/src/types/anthropic-account-pool.ts +33 -0
- package/src/types/config.ts +12 -28
- package/src/types/provider.ts +5 -0
- package/src/types/tools.ts +7 -0
- package/src/types.ts +4 -0
- package/src/usage/cost.ts +3 -2
- package/src/usage/expected-prices.ts +66 -14
- package/src/vision/anthropic-describe.ts +16 -3
- package/src/vision/index.ts +3 -0
- package/src/vision/plan.ts +19 -6
- package/src/web-search/alpha-search.ts +7 -1
- package/src/web-search/anthropic-executor.ts +16 -4
- package/src/web-search/index.ts +7 -3
- package/src/web-search/loop.ts +27 -5
- package/src/web-search/passthrough-bridge.ts +4 -1
- package/src/web-search/sidecar-providers.ts +4 -3
- package/gui/dist/assets/App-MbDVfUeC.js +0 -54
- package/gui/dist/assets/Tray-CzkxK82A.js +0 -1
- package/gui/dist/assets/index-UJt6npXk.js +0 -94
- 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
|
|
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
|
|
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 {
|
|
2
|
-
import {
|
|
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 {
|
|
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
|
-
|
|
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
|
|
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
|
|
471
|
-
effectiveAutoSwitchThreshold:
|
|
472
|
-
autoSwitchThreshold:
|
|
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
|
|
536
|
-
const {
|
|
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
|
-
|
|
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
|
|
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
|
|
572
|
-
const {
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
880
|
-
autoSwitchThreshold: config
|
|
881
|
-
strategy: normalizeAccountPoolStrategy(config
|
|
882
|
-
stickyLimit: normalizeAccountPoolStickyLimit(config
|
|
883
|
-
quotaWindow: normalizeAccountPoolQuotaWindow(config
|
|
884
|
-
routes: config
|
|
885
|
-
nativeMessages: !Object.hasOwn(config
|
|
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
|
|
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 {
|
|
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
|
-
|
|
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
|
|
973
|
-
const {
|
|
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
|
-
|
|
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
|
}
|