@bitkyc08/opencodex 2.79.0 → 2.81.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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/cursor/live-transport.ts +5 -3
- package/src/adapters/cursor/native-exec-fs.ts +12 -9
- package/src/adapters/cursor/native-exec.ts +13 -5
- package/src/adapters/cursor/protobuf-request.ts +13 -7
- package/src/adapters/cursor/request-builder.ts +23 -10
- package/src/adapters/cursor/tool-definitions.ts +9 -6
- package/src/adapters/cursor/tool-guidance.ts +29 -8
- package/src/adapters/cursor/tool-naming.ts +40 -7
- package/src/adapters/cursor/tool-wording.ts +46 -0
- package/src/adapters/cursor.ts +3 -0
- package/src/adapters/devin/live-models.ts +2 -0
- package/src/adapters/devin.ts +25 -11
- package/src/adapters/kiro-retry.ts +9 -4
- package/src/adapters/openai-chat/messages.ts +7 -4
- 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 +10 -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/autostart-health.ts +3 -3
- 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/codex/journal.ts +36 -5
- package/src/codex/shim-overlay.ts +9 -1
- package/src/combos/jev-decision-contract.ts +25 -2
- package/src/combos/jev.ts +6 -3
- 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 +347 -199
- 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 +16 -16
- 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-scope.ts +4 -1
- 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/combo-requested-effort.ts +14 -0
- 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 +21 -18
- 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/desktop-startup.ts +100 -3
- 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/header-deadline.ts +67 -0
- package/src/web-search/index.ts +7 -3
- package/src/web-search/loop.ts +32 -10
- package/src/web-search/passthrough-bridge.ts +4 -1
- package/src/web-search/sidecar-providers.ts +4 -3
- package/gui/dist/assets/App-BOUa-ek3.js +0 -54
- package/gui/dist/assets/Tray-DUtzB4zF.js +0 -1
- package/gui/dist/assets/index-DwHNDF4s.js +0 -94
- package/gui/dist/assets/quota-summary-BuL4WIBo.js +0 -1
|
@@ -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
|
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { isDeepStrictEqual } from "node:util";
|
|
2
|
+
import type { OcxProviderConfig } from "../../types";
|
|
3
|
+
import type { ProviderEditorProviderDTO } from "../auth-cors";
|
|
4
|
+
import { anthropicInstanceRowShapeMatches } from "../../providers/anthropic-instance-id";
|
|
5
|
+
import { assertAnthropicInstanceLoginConfig } from "../../oauth/store-anthropic-instance";
|
|
6
|
+
|
|
7
|
+
/** Preset publication may create B, but may never take over an existing custom row. */
|
|
8
|
+
export function anthropicInstancePublicationError(name: string, next: OcxProviderConfig, existing: OcxProviderConfig | undefined): string | undefined {
|
|
9
|
+
if (name === "anthropic2" && anthropicInstanceRowShapeMatches(name, next)) {
|
|
10
|
+
try { assertAnthropicInstanceLoginConfig({ providers: existing ? { [name]: existing } : {} }, name); }
|
|
11
|
+
catch { return "cannot add Pool 2 over an existing custom provider or unreadable configuration; resolve it first"; }
|
|
12
|
+
}
|
|
13
|
+
return undefined;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Full form edits omit provenance; preserve only an already owned compatible row. */
|
|
17
|
+
export function preserveAnthropicInstanceMarker(name: string, next: OcxProviderConfig, existing: OcxProviderConfig | undefined): void {
|
|
18
|
+
if (name !== "anthropic2" || Object.hasOwn(next, "anthropicOAuthInstance")) return;
|
|
19
|
+
if (anthropicInstanceRowShapeMatches(name, existing) && next.adapter === "anthropic" && next.authMode === "oauth") {
|
|
20
|
+
next.anthropicOAuthInstance = "anthropic2";
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function mergeProviderEditorRow(
|
|
25
|
+
persisted: OcxProviderConfig | undefined,
|
|
26
|
+
baseline: ProviderEditorProviderDTO | undefined,
|
|
27
|
+
next: ProviderEditorProviderDTO,
|
|
28
|
+
): OcxProviderConfig {
|
|
29
|
+
const merged = structuredClone(persisted ?? {}) as Record<string, unknown>;
|
|
30
|
+
const fields = new Set([...Object.keys(baseline ?? {}), ...Object.keys(next)]);
|
|
31
|
+
for (const field of fields) {
|
|
32
|
+
const baselineHasField = baseline !== undefined && Object.hasOwn(baseline, field);
|
|
33
|
+
const nextHasField = Object.hasOwn(next, field);
|
|
34
|
+
if (
|
|
35
|
+
baselineHasField === nextHasField
|
|
36
|
+
&& (!baselineHasField || isDeepStrictEqual(baseline[field], next[field]))
|
|
37
|
+
) {
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
if (nextHasField) merged[field] = structuredClone(next[field]);
|
|
41
|
+
else delete merged[field];
|
|
42
|
+
}
|
|
43
|
+
return merged as unknown as OcxProviderConfig;
|
|
44
|
+
}
|
|
@@ -40,6 +40,7 @@ import { commitProviderPatch } from "./provider-patch-transaction";
|
|
|
40
40
|
import { captureConfigTopLevelRollback } from "../../config/rebase-provenance";
|
|
41
41
|
import { canonicalAutoReviewModelKey, mergeModelPinnedEfforts, modelPinnedEffortsConfigError, pinnedReasoningEffortConfigError } from "../../config/provider-validation";
|
|
42
42
|
import { replaceProviderAccountSet } from "../../oauth/store";
|
|
43
|
+
import { captureModelsOAuthTarget, mayResolveModelsOAuth, modelsOAuthTargetMatches } from "../../oauth/model-discovery-auth";
|
|
43
44
|
import { providerDestinationResolvedError } from "../../lib/destination-policy";
|
|
44
45
|
import { reconcileLiveStateStores } from "../../lib/state-store-registrations";
|
|
45
46
|
import { ProviderOutboundPolicyError, providerOutboundGet, providerOutboundPost, providerRedirectError } from "../../lib/provider-outbound";
|
|
@@ -115,7 +116,6 @@ import {
|
|
|
115
116
|
publicProviderBaseUrl,
|
|
116
117
|
safeConfigDTO,
|
|
117
118
|
type ProviderEditorConfigDTO,
|
|
118
|
-
type ProviderEditorProviderDTO,
|
|
119
119
|
} from "../auth-cors";
|
|
120
120
|
import { providerCatalogCapabilityConfigError } from "./provider-capability-config";
|
|
121
121
|
import { providerEmptyToolOutputConfigError } from "../../config/provider-validation";
|
|
@@ -138,6 +138,7 @@ import { isPlainRecord, parseDebugLogQuery, tokPerSecondResult, unavailableCostR
|
|
|
138
138
|
import type { MetricUnavailableReason, TokPerSecondResult, CostEstimateReason, CostResult, MetricSource } from "./shared";
|
|
139
139
|
import type { ManagementContext } from "./context";
|
|
140
140
|
import { readManagementJsonBody, rethrowManagementBodyTooLarge } from "./body";
|
|
141
|
+
import { anthropicInstancePublicationError, mergeProviderEditorRow, preserveAnthropicInstanceMarker } from "./provider-instance-ownership";
|
|
141
142
|
import { providerTlsProfileDiagnostic } from "../../lib/provider-tls-profile";
|
|
142
143
|
|
|
143
144
|
type ProviderPatchApplication =
|
|
@@ -213,28 +214,6 @@ type ProviderEditorCandidateResult =
|
|
|
213
214
|
|
|
214
215
|
type ProviderEditorMutationValue = ProviderEditorCandidateResult;
|
|
215
216
|
|
|
216
|
-
function mergeProviderEditorRow(
|
|
217
|
-
persisted: OcxProviderConfig | undefined,
|
|
218
|
-
baseline: ProviderEditorProviderDTO | undefined,
|
|
219
|
-
next: ProviderEditorProviderDTO,
|
|
220
|
-
): OcxProviderConfig {
|
|
221
|
-
const merged = structuredClone(persisted ?? {}) as Record<string, unknown>;
|
|
222
|
-
const fields = new Set([...Object.keys(baseline ?? {}), ...Object.keys(next)]);
|
|
223
|
-
for (const field of fields) {
|
|
224
|
-
const baselineHasField = baseline !== undefined && Object.hasOwn(baseline, field);
|
|
225
|
-
const nextHasField = Object.hasOwn(next, field);
|
|
226
|
-
if (
|
|
227
|
-
baselineHasField === nextHasField
|
|
228
|
-
&& (!baselineHasField || isDeepStrictEqual(baseline[field], next[field]))
|
|
229
|
-
) {
|
|
230
|
-
continue;
|
|
231
|
-
}
|
|
232
|
-
if (nextHasField) merged[field] = structuredClone(next[field]);
|
|
233
|
-
else delete merged[field];
|
|
234
|
-
}
|
|
235
|
-
return merged as unknown as OcxProviderConfig;
|
|
236
|
-
}
|
|
237
|
-
|
|
238
217
|
/** Build and validate a complete candidate without mutating the caller's snapshot. */
|
|
239
218
|
function providerEditorCandidate(
|
|
240
219
|
persisted: OcxConfig,
|
|
@@ -273,6 +252,8 @@ function providerEditorCandidate(
|
|
|
273
252
|
const namespaceCollision = codexAccountNamespaceProviderCollisionError(candidate.codexAccountNamespaces, name);
|
|
274
253
|
if (namespaceCollision) return { ok: false, status: 409, error: namespaceCollision, code: "provider_namespace_conflict" };
|
|
275
254
|
const merged = mergeProviderEditorRow(persisted.providers[name], baseline.providers[name], publicProvider);
|
|
255
|
+
const markerCollision = anthropicInstancePublicationError(name, merged, persisted.providers[name]);
|
|
256
|
+
if (markerCollision) return { ok: false, status: 409, error: markerCollision, code: "anthropic_instance_collision" };
|
|
276
257
|
const transportCandidate = providerTransportValidationCandidate(merged as unknown as Record<string, unknown>);
|
|
277
258
|
// The editor merges onto the persisted row, so stored operator overlays (selectedModels,
|
|
278
259
|
// disabled, …) ride along in the candidate. They are owned by their own write boundaries;
|
|
@@ -1172,6 +1153,9 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
|
|
|
1172
1153
|
const aliasOwnershipError = providerAliasOverlayOwnershipError(body.provider, existing);
|
|
1173
1154
|
if (aliasOwnershipError) return jsonResponse({ error: aliasOwnershipError }, 400);
|
|
1174
1155
|
const transportCandidate = providerTransportValidationCandidate(body.provider);
|
|
1156
|
+
preserveAnthropicInstanceMarker(name, transportCandidate as unknown as OcxProviderConfig, existing);
|
|
1157
|
+
const markerCollision = anthropicInstancePublicationError(name, transportCandidate as unknown as OcxProviderConfig, existing);
|
|
1158
|
+
if (markerCollision) return jsonResponse({ error: markerCollision }, 409);
|
|
1175
1159
|
const pinError = applyProviderPinFields(transportCandidate as unknown as OcxProviderConfig, body.provider, existing);
|
|
1176
1160
|
if (pinError) return jsonResponse({ error: pinError }, 400);
|
|
1177
1161
|
const providerError = providerManagementConfigError(name, transportCandidate)
|
|
@@ -1328,6 +1312,9 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
|
|
|
1328
1312
|
// DNS validation above awaits. Re-read the live row so a dedicated alias write that
|
|
1329
1313
|
// completed during that wait remains authoritative instead of being overwritten by the
|
|
1330
1314
|
// older ownership snapshot used to admit this POST.
|
|
1315
|
+
const lateMarkerCollision = anthropicInstancePublicationError(name, prov, config.providers[name]);
|
|
1316
|
+
if (lateMarkerCollision) return jsonResponse({ error: lateMarkerCollision }, 409);
|
|
1317
|
+
preserveAnthropicInstanceMarker(name, prov, config.providers[name]);
|
|
1331
1318
|
restorePersistedAliasOverlays(prov, config.providers[name]);
|
|
1332
1319
|
const capabilities = Object.hasOwn(body.provider, "modelCapabilities")
|
|
1333
1320
|
? mergeModelCapabilities(undefined, prov.modelCapabilities)
|
|
@@ -1371,6 +1358,8 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
|
|
|
1371
1358
|
const rollback = pinsOwned ? captureConfigTopLevelRollback(config, ["defaultProvider", "modelDiscovery", "disabledModels"]) : undefined;
|
|
1372
1359
|
let validationError: string | undefined;
|
|
1373
1360
|
withConfigMutationLockSync(() => {
|
|
1361
|
+
validationError = anthropicInstancePublicationError(name, candidate, config.providers[name]);
|
|
1362
|
+
if (validationError) return;
|
|
1374
1363
|
const liveTiers = config.providers[name]?.modelContextTiers;
|
|
1375
1364
|
if (submittedModelContextTiers && liveTiers) candidate.modelContextTiers = Object.assign(Object.create(null), liveTiers, candidate.modelContextTiers ?? {});
|
|
1376
1365
|
else if (!submittedModelContextTiers && liveTiers) candidate.modelContextTiers = { ...liveTiers };
|
|
@@ -1621,7 +1610,8 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
|
|
|
1621
1610
|
if (!name || !isValidProviderName(name) || !hasOwnProvider(config.providers, name)) {
|
|
1622
1611
|
return jsonResponse({ error: "unknown provider" }, 404);
|
|
1623
1612
|
}
|
|
1624
|
-
const prov = config.providers[name]
|
|
1613
|
+
const prov = { ...config.providers[name]! };
|
|
1614
|
+
const authorizedTarget = captureModelsOAuthTarget(name, prov);
|
|
1625
1615
|
if (prov.disabled) {
|
|
1626
1616
|
return jsonResponse({ ok: false, error: "Provider is disabled", latencyMs: 0 });
|
|
1627
1617
|
}
|
|
@@ -1646,10 +1636,10 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
|
|
|
1646
1636
|
// credential resolution/network access for providers such as Antigravity.
|
|
1647
1637
|
return jsonResponse({ applicable: false, reason: "static_catalog", latencyMs: 0 });
|
|
1648
1638
|
}
|
|
1649
|
-
const { buildModelsRequest,
|
|
1639
|
+
const { buildModelsRequest, getModelsOAuthAccessSnapshot, resolveModelsAuthToken } = await import("../../oauth");
|
|
1650
1640
|
const antigravity = effectiveGoogleMode(name, prov) === "cloud-code-assist";
|
|
1651
|
-
const snapshot = prov.authMode === "oauth"
|
|
1652
|
-
? await
|
|
1641
|
+
const snapshot = prov.authMode === "oauth" && mayResolveModelsOAuth(name, config.providers[name])
|
|
1642
|
+
? await getModelsOAuthAccessSnapshot(name, prov).catch(() => undefined)
|
|
1653
1643
|
: undefined;
|
|
1654
1644
|
const apiKey = prov.authMode === "oauth" ? snapshot?.accessToken : await resolveModelsAuthToken(name, prov);
|
|
1655
1645
|
if (prov.authMode === "oauth" && !apiKey) {
|
|
@@ -1735,7 +1725,10 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
|
|
|
1735
1725
|
// Same canonical-URL TUN transparency as catalog discovery: the registry's
|
|
1736
1726
|
// own fixed discovery URL survives purely-benchmark (Clash/Surge/Mihomo
|
|
1737
1727
|
// fake-IP) DNS without proxy env.
|
|
1738
|
-
const outboundDependencies = { isCanonicalUrl: isRegistryModelDiscoveryUrl
|
|
1728
|
+
const outboundDependencies = { isCanonicalUrl: isRegistryModelDiscoveryUrl,
|
|
1729
|
+
beforeSend: () => prov.authMode !== "oauth" || (mayResolveModelsOAuth(name, config.providers[name])
|
|
1730
|
+
&& modelsOAuthTargetMatches(name, config.providers[name], authorizedTarget)
|
|
1731
|
+
&& (name !== "anthropic2" || new URL(modelsUrl).href === authorizedTarget)) };
|
|
1739
1732
|
const res = method === "POST"
|
|
1740
1733
|
? await providerOutboundPost(name, prov, modelsUrl, {
|
|
1741
1734
|
headers,
|
|
@@ -231,6 +231,8 @@ export const MANAGEMENT_ROUTES: readonly ManagementRoute[] = [
|
|
|
231
231
|
{ method: "POST", path: "/api/client-integrations/restore", module: "server/management/integration-routes", mutates: true },
|
|
232
232
|
{ method: "POST", path: "/api/client-integrations/preview", module: "server/management/integration-routes", mutates: false },
|
|
233
233
|
{ method: "POST", path: "/api/client-integrations/restore/preview", module: "server/management/integration-routes", mutates: false },
|
|
234
|
+
{ method: "POST", path: "/api/client-integrations/commandcode/restore", module: "server/management/integration-routes", mutates: true },
|
|
235
|
+
{ method: "POST", path: "/api/client-integrations/commandcode/restore/preview", module: "server/management/integration-routes", mutates: false },
|
|
234
236
|
// server/management/lab-automation-routes
|
|
235
237
|
{ method: "GET", path: "/api/lab/automation", module: "server/management/lab-automation-routes", mutates: false, exempt: { reason: "local-transport", why: "ocx lab reads the same rows from the local SQLite projection; src/cli/lab.ts imports ../lab/query directly and never fetches /api/lab." } },
|
|
236
238
|
{ method: "GET", path: "/api/lab/automation/runs", module: "server/management/lab-automation-routes", mutates: false, exempt: { reason: "local-transport", why: "ocx lab reads the same rows from the local SQLite projection; src/cli/lab.ts imports ../lab/query directly and never fetches /api/lab." } },
|
|
@@ -324,7 +326,7 @@ export const MANAGEMENT_ROUTES: readonly ManagementRoute[] = [
|
|
|
324
326
|
{ method: "PATCH", path: "/api/keys", module: "server/management/oauth-account-routes", mutates: true },
|
|
325
327
|
{ method: "PATCH", path: "/api/oauth/accounts/pool", module: "server/management/oauth-account-routes", mutates: true },
|
|
326
328
|
{ method: "POST", path: "/api/keys", module: "server/management/oauth-account-routes", mutates: true },
|
|
327
|
-
{ method: "POST", path: "/api/keys/reveal", module: "server/management/oauth-account-routes", mutates: false, exempt: { reason: "session-only", why: "
|
|
329
|
+
{ method: "POST", path: "/api/keys/reveal", module: "server/management/oauth-account-routes", mutates: false, exempt: { reason: "session-only", why: "Stored-key reads require a current pairing or trusted Tailscale-identity session; automatic loopback sessions and admin tokens are refused." } },
|
|
328
330
|
{ method: "POST", path: "/api/keys/rotate", module: "server/management/oauth-account-routes", mutates: true },
|
|
329
331
|
{ method: "POST", path: "/api/keys/rotate/commit", module: "server/management/oauth-account-routes", mutates: true },
|
|
330
332
|
{ method: "POST", path: "/api/oauth/accounts/clear-cooldown", module: "server/management/oauth-account-routes", mutates: true },
|
|
@@ -63,7 +63,7 @@ function parseEvidence(raw: unknown): { evidence: PolicyRequestEvidence; ok: boo
|
|
|
63
63
|
return { evidence, ok: true };
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
-
function parseCandidateEvidence(raw: unknown): PolicyCandidateEvidence[] | null {
|
|
66
|
+
function parseCandidateEvidence(raw: unknown, config: OcxConfig): PolicyCandidateEvidence[] | null {
|
|
67
67
|
if (!Array.isArray(raw)) return null;
|
|
68
68
|
const out: PolicyCandidateEvidence[] = [];
|
|
69
69
|
for (const item of raw) {
|
|
@@ -87,10 +87,10 @@ function parseCandidateEvidence(raw: unknown): PolicyCandidateEvidence[] | null
|
|
|
87
87
|
// caller does not supply an explicit quota object, so a dry-run following
|
|
88
88
|
// the documented shape reports the same cached account quota as routing.
|
|
89
89
|
...(item.quota === undefined && typeof item.codexAccountId === "string"
|
|
90
|
-
? { quota: quotaEvidenceForCandidate({ provider, model, codexAccountId: item.codexAccountId }) }
|
|
90
|
+
? { quota: quotaEvidenceForCandidate({ provider, model, codexAccountId: item.codexAccountId }, config) }
|
|
91
91
|
: {}),
|
|
92
92
|
...(item.quota === undefined && typeof item.accountRef === "string" && typeof item.codexAccountId !== "string"
|
|
93
|
-
? { quota: quotaEvidenceForCandidate({ provider, model, accountRef: item.accountRef }) }
|
|
93
|
+
? { quota: quotaEvidenceForCandidate({ provider, model, accountRef: item.accountRef }, config) }
|
|
94
94
|
: {}),
|
|
95
95
|
});
|
|
96
96
|
}
|
|
@@ -392,7 +392,7 @@ export async function handleRoutingProfileRoutes(ctx: ManagementContext): Promis
|
|
|
392
392
|
if (labActivationRequired(config, getConfigDir())) activateLab(config, getConfigDir());
|
|
393
393
|
const candidateEvidence = body.candidates === undefined
|
|
394
394
|
? assembleCandidateEvidence(config, resolvedProfile, now)
|
|
395
|
-
: parseCandidateEvidence(body.candidates);
|
|
395
|
+
: parseCandidateEvidence(body.candidates, config);
|
|
396
396
|
if (candidateEvidence === null) {
|
|
397
397
|
return jsonResponse({ error: { code: "invalid_candidates", message: "candidates must be an array of evidence objects" } }, 400, req, config);
|
|
398
398
|
}
|