diff --git a/extensions/anthropic/openclaw.plugin.json b/extensions/anthropic/openclaw.plugin.json index 27ba950e472..4bf8a288b05 100644 --- a/extensions/anthropic/openclaw.plugin.json +++ b/extensions/anthropic/openclaw.plugin.json @@ -2,6 +2,7 @@ "id": "anthropic", "enabledByDefault": true, "providers": ["anthropic"], + "providerDiscoveryEntry": "./provider-discovery.ts", "modelSupport": { "modelPrefixes": ["claude-"] }, diff --git a/extensions/anthropic/provider-discovery.ts b/extensions/anthropic/provider-discovery.ts new file mode 100644 index 00000000000..01279cc3bf6 --- /dev/null +++ b/extensions/anthropic/provider-discovery.ts @@ -0,0 +1,35 @@ +import type { ProviderPlugin } from "openclaw/plugin-sdk/provider-model-shared"; +import { readClaudeCliCredentialsForRuntime } from "./cli-auth-seam.js"; + +const CLAUDE_CLI_BACKEND_ID = "claude-cli"; + +function resolveClaudeCliSyntheticAuth() { + const credential = readClaudeCliCredentialsForRuntime(); + if (!credential) { + return undefined; + } + return credential.type === "oauth" + ? { + apiKey: credential.access, + source: "Claude CLI native auth", + mode: "oauth" as const, + expiresAt: credential.expires, + } + : { + apiKey: credential.token, + source: "Claude CLI native auth", + mode: "token" as const, + expiresAt: credential.expires, + }; +} + +export const anthropicProviderDiscovery: ProviderPlugin = { + id: CLAUDE_CLI_BACKEND_ID, + label: "Claude CLI", + docsPath: "/providers/models", + auth: [], + resolveSyntheticAuth: ({ provider }) => + provider === CLAUDE_CLI_BACKEND_ID ? resolveClaudeCliSyntheticAuth() : undefined, +}; + +export default anthropicProviderDiscovery; diff --git a/extensions/ollama/provider-discovery.ts b/extensions/ollama/provider-discovery.ts index 75c43248d96..d2372700b4c 100644 --- a/extensions/ollama/provider-discovery.ts +++ b/extensions/ollama/provider-discovery.ts @@ -1,6 +1,9 @@ import type { ProviderCatalogContext } from "openclaw/plugin-sdk/provider-catalog-shared"; +import type { ModelProviderConfig } from "openclaw/plugin-sdk/provider-model-shared"; import { + OLLAMA_DEFAULT_API_KEY, OLLAMA_PROVIDER_ID, + hasMeaningfulExplicitOllamaConfig, resolveOllamaDiscoveryResult, type OllamaPluginConfig, } from "./src/discovery-shared.js"; @@ -12,6 +15,13 @@ type OllamaProviderPlugin = { docsPath: string; envVars: string[]; auth: []; + resolveSyntheticAuth: (ctx: { providerConfig?: ModelProviderConfig }) => + | { + apiKey: string; + source: string; + mode: "api-key"; + } + | undefined; discovery: { order: "late"; run: (ctx: ProviderCatalogContext) => ReturnType; @@ -40,6 +50,16 @@ export const ollamaProviderDiscovery: OllamaProviderPlugin = { docsPath: "/providers/ollama", envVars: ["OLLAMA_API_KEY"], auth: [], + resolveSyntheticAuth: ({ providerConfig }) => { + if (!hasMeaningfulExplicitOllamaConfig(providerConfig)) { + return undefined; + } + return { + apiKey: OLLAMA_DEFAULT_API_KEY, + source: "models.providers.ollama (synthetic local key)", + mode: "api-key", + }; + }, discovery: { order: "late", run: runOllamaDiscovery, diff --git a/extensions/xai/openclaw.plugin.json b/extensions/xai/openclaw.plugin.json index 57dd1451b69..af9bcf43f63 100644 --- a/extensions/xai/openclaw.plugin.json +++ b/extensions/xai/openclaw.plugin.json @@ -2,6 +2,7 @@ "id": "xai", "enabledByDefault": true, "providers": ["xai"], + "providerDiscoveryEntry": "./provider-discovery.ts", "providerEndpoints": [ { "endpointClass": "xai-native", diff --git a/extensions/xai/provider-discovery.ts b/extensions/xai/provider-discovery.ts new file mode 100644 index 00000000000..8c898ff8f37 --- /dev/null +++ b/extensions/xai/provider-discovery.ts @@ -0,0 +1,27 @@ +import type { ProviderPlugin } from "openclaw/plugin-sdk/provider-model-shared"; +import { readProviderEnvValue } from "openclaw/plugin-sdk/provider-web-search"; +import { resolveFallbackXaiAuth } from "./src/tool-auth-shared.js"; + +const PROVIDER_ID = "xai"; + +function resolveXaiSyntheticAuth(config: unknown) { + const apiKey = + resolveFallbackXaiAuth(config as never)?.apiKey || readProviderEnvValue(["XAI_API_KEY"]); + return apiKey + ? { + apiKey, + source: "xAI plugin config", + mode: "api-key" as const, + } + : undefined; +} + +export const xaiProviderDiscovery: ProviderPlugin = { + id: PROVIDER_ID, + label: "xAI", + docsPath: "/providers/models", + auth: [], + resolveSyntheticAuth: ({ config }) => resolveXaiSyntheticAuth(config), +}; + +export default xaiProviderDiscovery; diff --git a/src/agents/models-config.providers.policy.runtime.ts b/src/agents/models-config.providers.policy.runtime.ts index 8f89bb92908..b45aadf6906 100644 --- a/src/agents/models-config.providers.policy.runtime.ts +++ b/src/agents/models-config.providers.policy.runtime.ts @@ -14,6 +14,7 @@ export function applyProviderNativeStreamingUsagePolicy( return ( applyProviderNativeStreamingUsageCompatWithPlugin({ provider: runtimeProviderKey, + allowRuntimePluginLoad: false, context: { provider: providerKey, providerConfig: provider, @@ -30,6 +31,7 @@ export function normalizeProviderConfigPolicy( return ( normalizeProviderConfigWithPlugin({ provider: runtimeProviderKey, + allowRuntimePluginLoad: false, context: { provider: providerKey, providerConfig: provider, @@ -46,6 +48,7 @@ export function resolveProviderConfigApiKeyPolicy( return (env) => resolveProviderConfigApiKeyWithPlugin({ provider: runtimeProviderKey, + allowRuntimePluginLoad: false, context: { provider: providerKey, env, diff --git a/src/agents/provider-auth-aliases.ts b/src/agents/provider-auth-aliases.ts index 8b8b45881ab..c71c1185f91 100644 --- a/src/agents/provider-auth-aliases.ts +++ b/src/agents/provider-auth-aliases.ts @@ -26,6 +26,22 @@ const PROVIDER_AUTH_ALIAS_ORIGIN_PRIORITY: Readonly global: 2, workspace: 3, }; +let providerAuthAliasMapCache = new WeakMap< + NodeJS.ProcessEnv, + Map> +>(); + +function buildProviderAuthAliasMapCacheKey(params?: ProviderAuthAliasLookupParams): string { + return JSON.stringify({ + workspaceDir: params?.workspaceDir ?? "", + includeUntrustedWorkspacePlugins: params?.includeUntrustedWorkspacePlugins === true, + plugins: params?.config?.plugins ?? null, + }); +} + +export function resetProviderAuthAliasMapCacheForTest(): void { + providerAuthAliasMapCache = new WeakMap>>(); +} function resolveProviderAuthAliasOriginPriority(origin: PluginOrigin | undefined): number { if (!origin) { @@ -83,10 +99,21 @@ function setPreferredAlias(params: { export function resolveProviderAuthAliasMap( params?: ProviderAuthAliasLookupParams, ): Record { + const env = params?.env ?? process.env; + const cacheKey = buildProviderAuthAliasMapCacheKey(params); + let envCache = providerAuthAliasMapCache.get(env); + if (!envCache) { + envCache = new Map>(); + providerAuthAliasMapCache.set(env, envCache); + } + const cached = envCache.get(cacheKey); + if (cached) { + return cached; + } const registry = loadPluginManifestRegistryForPluginRegistry({ config: params?.config, workspaceDir: params?.workspaceDir, - env: params?.env, + env, includeDisabled: true, }); const preferredAliases = new Map(); @@ -119,6 +146,7 @@ export function resolveProviderAuthAliasMap( for (const [alias, candidate] of preferredAliases) { aliases[alias] = candidate.target; } + envCache.set(cacheKey, aliases); return aliases; } diff --git a/src/entry.test.ts b/src/entry.test.ts index e6ff5adda88..7854a9b6d40 100644 --- a/src/entry.test.ts +++ b/src/entry.test.ts @@ -11,10 +11,9 @@ describe("entry root help fast path", () => { it("prefers precomputed root help text when available", async () => { outputPrecomputedRootHelpTextMock.mockReturnValueOnce(true); - const handled = tryHandleRootHelpFastPath(["node", "openclaw", "--help"], { + const handled = await tryHandleRootHelpFastPath(["node", "openclaw", "--help"], { env: {}, }); - await vi.dynamicImportSettled(); expect(handled).toBe(true); expect(outputPrecomputedRootHelpTextMock).toHaveBeenCalledTimes(1); @@ -23,20 +22,19 @@ describe("entry root help fast path", () => { it("renders root help without importing the full program", async () => { const outputRootHelpMock = vi.fn(); - const handled = tryHandleRootHelpFastPath(["node", "openclaw", "--help"], { + const handled = await tryHandleRootHelpFastPath(["node", "openclaw", "--help"], { outputRootHelp: outputRootHelpMock, env: {}, }); - await Promise.resolve(); expect(handled).toBe(true); expect(outputRootHelpMock).toHaveBeenCalledTimes(1); }); - it("ignores non-root help invocations", () => { + it("ignores non-root help invocations", async () => { const outputRootHelpMock = vi.fn(); - const handled = tryHandleRootHelpFastPath(["node", "openclaw", "status", "--help"], { + const handled = await tryHandleRootHelpFastPath(["node", "openclaw", "status", "--help"], { outputRootHelp: outputRootHelpMock, env: {}, }); @@ -45,10 +43,10 @@ describe("entry root help fast path", () => { expect(outputRootHelpMock).not.toHaveBeenCalled(); }); - it("skips the host help fast path when a container target is active", () => { + it("skips the host help fast path when a container target is active", async () => { const outputRootHelpMock = vi.fn(); - const handled = tryHandleRootHelpFastPath( + const handled = await tryHandleRootHelpFastPath( ["node", "openclaw", "--container", "demo", "--help"], { outputRootHelp: outputRootHelpMock, diff --git a/src/entry.ts b/src/entry.ts index 6d9ec51a623..7a34f8d0103 100644 --- a/src/entry.ts +++ b/src/entry.ts @@ -126,19 +126,19 @@ if ( } if (!tryHandleRootVersionFastPath(process.argv)) { - runMainOrRootHelp(process.argv); + await runMainOrRootHelp(process.argv); } } } -export function tryHandleRootHelpFastPath( +export async function tryHandleRootHelpFastPath( argv: string[], deps: { outputRootHelp?: () => void | Promise; onError?: (error: unknown) => void; env?: NodeJS.ProcessEnv; } = {}, -): boolean { +): Promise { if (resolveCliContainerTarget(argv, deps.env)) { return false; } @@ -154,35 +154,35 @@ export function tryHandleRootHelpFastPath( ); process.exitCode = 1; }); - if (deps.outputRootHelp) { - Promise.resolve() - .then(() => deps.outputRootHelp?.()) - .catch(handleError); - return true; - } - import("./cli/root-help-metadata.js") - .then(async ({ outputPrecomputedRootHelpText }) => { - if (outputPrecomputedRootHelpText()) { - return; - } + try { + if (deps.outputRootHelp) { + await deps.outputRootHelp(); + return true; + } + const { outputPrecomputedRootHelpText } = await import("./cli/root-help-metadata.js"); + if (!outputPrecomputedRootHelpText()) { const { outputRootHelp } = await import("./cli/program/root-help.js"); await outputRootHelp(); - }) - .catch(handleError); - return true; + } + return true; + } catch (error) { + handleError(error); + return true; + } } -function runMainOrRootHelp(argv: string[]): void { - if (tryHandleRootHelpFastPath(argv)) { +async function runMainOrRootHelp(argv: string[]): Promise { + if (await tryHandleRootHelpFastPath(argv)) { return; } - import("./cli/run-main.js") - .then(({ runCli }) => runCli(argv)) - .catch((error) => { - console.error( - "[openclaw] Failed to start CLI:", - error instanceof Error ? (error.stack ?? error.message) : error, - ); - process.exitCode = 1; - }); + try { + const { runCli } = await import("./cli/run-main.js"); + await runCli(argv); + } catch (error) { + console.error( + "[openclaw] Failed to start CLI:", + error instanceof Error ? (error.stack ?? error.message) : error, + ); + process.exitCode = 1; + } } diff --git a/src/plugins/provider-hook-runtime.ts b/src/plugins/provider-hook-runtime.ts index 6bfed15e245..ef9c2961939 100644 --- a/src/plugins/provider-hook-runtime.ts +++ b/src/plugins/provider-hook-runtime.ts @@ -102,6 +102,10 @@ export function resolveProviderPluginsForHooks(params: { env?: NodeJS.ProcessEnv; onlyPluginIds?: string[]; providerRefs?: string[]; + applyAutoEnable?: boolean; + bundledProviderAllowlistCompat?: boolean; + bundledProviderVitestCompat?: boolean; + installBundledRuntimeDeps?: boolean; }): ProviderPlugin[] { const env = params.env ?? process.env; const workspaceDir = params.workspaceDir ?? getActivePluginRegistryWorkspaceDirFromState(); @@ -127,8 +131,10 @@ export function resolveProviderPluginsForHooks(params: { env, activate: false, cache: false, - bundledProviderAllowlistCompat: true, - bundledProviderVitestCompat: true, + applyAutoEnable: params.applyAutoEnable, + bundledProviderAllowlistCompat: params.bundledProviderAllowlistCompat ?? true, + bundledProviderVitestCompat: params.bundledProviderVitestCompat ?? true, + installBundledRuntimeDeps: params.installBundledRuntimeDeps, }) ) { return []; @@ -139,8 +145,10 @@ export function resolveProviderPluginsForHooks(params: { env, activate: false, cache: false, - bundledProviderAllowlistCompat: true, - bundledProviderVitestCompat: true, + applyAutoEnable: params.applyAutoEnable, + bundledProviderAllowlistCompat: params.bundledProviderAllowlistCompat ?? true, + bundledProviderVitestCompat: params.bundledProviderVitestCompat ?? true, + installBundledRuntimeDeps: params.installBundledRuntimeDeps, }); cacheBucket.set(cacheKey, resolved); return resolved; @@ -151,12 +159,20 @@ export function resolveProviderRuntimePlugin(params: { config?: OpenClawConfig; workspaceDir?: string; env?: NodeJS.ProcessEnv; + applyAutoEnable?: boolean; + bundledProviderAllowlistCompat?: boolean; + bundledProviderVitestCompat?: boolean; + installBundledRuntimeDeps?: boolean; }): ProviderPlugin | undefined { return resolveProviderPluginsForHooks({ config: params.config, workspaceDir: params.workspaceDir ?? getActivePluginRegistryWorkspaceDirFromState(), env: params.env, providerRefs: [params.provider], + applyAutoEnable: params.applyAutoEnable, + bundledProviderAllowlistCompat: params.bundledProviderAllowlistCompat, + bundledProviderVitestCompat: params.bundledProviderVitestCompat, + installBundledRuntimeDeps: params.installBundledRuntimeDeps, }).find((plugin) => matchesProviderId(plugin, params.provider)); } diff --git a/src/plugins/provider-runtime.synthetic-auth-discovery.test.ts b/src/plugins/provider-runtime.synthetic-auth-discovery.test.ts index afc493cff90..7a0a0529e7b 100644 --- a/src/plugins/provider-runtime.synthetic-auth-discovery.test.ts +++ b/src/plugins/provider-runtime.synthetic-auth-discovery.test.ts @@ -35,6 +35,13 @@ vi.mock("./provider-discovery.runtime.js", () => ({ resolvePluginDiscoveryProvidersRuntime, })); +vi.mock("./providers.js", () => ({ + resolveCatalogHookProviderPluginIds: vi.fn(() => []), + resolveExternalAuthProfileCompatFallbackPluginIds: vi.fn(() => []), + resolveExternalAuthProfileProviderPluginIds: vi.fn(() => []), + resolveOwningPluginIdsForProvider: vi.fn(() => ["anthropic-vertex"]), +})); + import { resolveProviderSyntheticAuthWithPlugin } from "./provider-runtime.js"; describe("resolveProviderSyntheticAuthWithPlugin", () => { @@ -53,7 +60,7 @@ describe("resolveProviderSyntheticAuthWithPlugin", () => { source: "gcp-vertex-credentials (ADC)", mode: "api-key", }); - expect(resolveProviderRuntimePlugin).toHaveBeenCalled(); + expect(resolveProviderRuntimePlugin).not.toHaveBeenCalled(); expect(resolvePluginDiscoveryProvidersRuntime).toHaveBeenCalled(); }); }); diff --git a/src/plugins/provider-runtime.test.ts b/src/plugins/provider-runtime.test.ts index d515e2e8656..9f996af4c54 100644 --- a/src/plugins/provider-runtime.test.ts +++ b/src/plugins/provider-runtime.test.ts @@ -26,6 +26,10 @@ type ResolveExternalAuthProfileCompatFallbackPluginIds = typeof import("./providers.js").resolveExternalAuthProfileCompatFallbackPluginIds; type ResolveExternalAuthProfileProviderPluginIds = typeof import("./providers.js").resolveExternalAuthProfileProviderPluginIds; +type ResolveOwningPluginIdsForProvider = + typeof import("./providers.js").resolveOwningPluginIdsForProvider; +type ResolveBundledProviderPolicySurface = + typeof import("./provider-public-artifacts.js").resolveBundledProviderPolicySurface; const resolvePluginProvidersMock = vi.fn((_) => [] as ProviderPlugin[]); const isPluginProvidersLoadInFlightMock = vi.fn((_) => false); @@ -36,6 +40,12 @@ const resolveExternalAuthProfileCompatFallbackPluginIdsMock = vi.fn((_) => [] as string[]); const resolveExternalAuthProfileProviderPluginIdsMock = vi.fn((_) => [] as string[]); +const resolveOwningPluginIdsForProviderMock = vi.fn( + (_) => undefined, +); +const resolveBundledProviderPolicySurfaceMock = vi.fn( + (_) => null, +); const providerRuntimeWarnMock = vi.fn(); let augmentModelCatalogWithProviderPlugins: typeof import("./provider-runtime.js").augmentModelCatalogWithProviderPlugins; @@ -244,7 +254,8 @@ describe("provider-runtime", () => { beforeAll(async () => { vi.resetModules(); vi.doMock("./provider-public-artifacts.js", () => ({ - resolveBundledProviderPolicySurface: () => null, + resolveBundledProviderPolicySurface: (provider: string) => + resolveBundledProviderPolicySurfaceMock(provider), })); vi.doMock("./providers.js", () => ({ resolveCatalogHookProviderPluginIds: (params: unknown) => @@ -253,6 +264,8 @@ describe("provider-runtime", () => { resolveExternalAuthProfileCompatFallbackPluginIdsMock(params as never), resolveExternalAuthProfileProviderPluginIds: (params: unknown) => resolveExternalAuthProfileProviderPluginIdsMock(params as never), + resolveOwningPluginIdsForProvider: (params: unknown) => + resolveOwningPluginIdsForProviderMock(params as never), })); vi.doMock("./providers.runtime.js", () => ({ resolvePluginProviders: (params: unknown) => resolvePluginProvidersMock(params as never), @@ -322,6 +335,7 @@ describe("provider-runtime", () => { beforeEach(() => { resetProviderRuntimeHookCacheForTest(); providerRuntimeTesting.resetExternalAuthFallbackWarningCacheForTest(); + providerRuntimeTesting.resetCatalogHookProvidersCacheForTest(); resolvePluginProvidersMock.mockReset(); resolvePluginProvidersMock.mockReturnValue([]); isPluginProvidersLoadInFlightMock.mockReset(); @@ -332,6 +346,10 @@ describe("provider-runtime", () => { resolveExternalAuthProfileCompatFallbackPluginIdsMock.mockReturnValue([]); resolveExternalAuthProfileProviderPluginIdsMock.mockReset(); resolveExternalAuthProfileProviderPluginIdsMock.mockReturnValue([]); + resolveOwningPluginIdsForProviderMock.mockReset(); + resolveOwningPluginIdsForProviderMock.mockReturnValue(undefined); + resolveBundledProviderPolicySurfaceMock.mockReset(); + resolveBundledProviderPolicySurfaceMock.mockReturnValue(null); providerRuntimeWarnMock.mockReset(); }); @@ -822,6 +840,31 @@ describe("provider-runtime", () => { }); }); + it("does not scan provider plugins after bundled policy surface handles config", () => { + const providerConfig: ModelProviderConfig = { + baseUrl: "https://api.openai.com/v1", + api: "openai-completions", + models: [], + }; + const normalizeConfig = vi.fn(() => providerConfig); + resolveBundledProviderPolicySurfaceMock.mockReturnValue({ + normalizeConfig, + }); + + expect( + normalizeProviderConfigWithPlugin({ + provider: "openai", + context: { + provider: "openai", + providerConfig, + }, + }), + ).toBeUndefined(); + + expect(normalizeConfig).toHaveBeenCalledTimes(1); + expect(resolvePluginProvidersMock).not.toHaveBeenCalled(); + }); + it("resolves provider config defaults through owner plugins", () => { resolvePluginProvidersMock.mockReturnValue([ { @@ -1758,7 +1801,7 @@ describe("provider-runtime", () => { }); expect(result).toBeUndefined(); - expect(resolvePluginProvidersMock).toHaveBeenCalledTimes(2); + expect(resolvePluginProvidersMock).toHaveBeenCalledTimes(1); }); it("keeps cached provider hook results available during a nested provider load", () => { @@ -1825,6 +1868,6 @@ describe("provider-runtime", () => { }), ).toBeUndefined(); - expect(resolvePluginProvidersMock).toHaveBeenCalledTimes(3); + expect(resolvePluginProvidersMock).toHaveBeenCalledTimes(2); }); }); diff --git a/src/plugins/provider-runtime.ts b/src/plugins/provider-runtime.ts index 1f601905d23..87180cf72c3 100644 --- a/src/plugins/provider-runtime.ts +++ b/src/plugins/provider-runtime.ts @@ -4,6 +4,7 @@ import { applyPluginTextReplacements, mergePluginTextTransforms, } from "../agents/plugin-text-transforms.js"; +import { normalizeProviderId } from "../agents/provider-id.js"; import type { ProviderSystemPromptContribution } from "../agents/system-prompt-contribution.js"; import type { ModelProviderConfig } from "../config/types.js"; import type { OpenClawConfig } from "../config/types.openclaw.js"; @@ -31,6 +32,7 @@ import { resolveCatalogHookProviderPluginIds, resolveExternalAuthProfileCompatFallbackPluginIds, resolveExternalAuthProfileProviderPluginIds, + resolveOwningPluginIdsForProvider, } from "./providers.js"; import { getActivePluginRegistryWorkspaceDirFromState } from "./runtime-state.js"; import { resolveRuntimeTextTransforms } from "./text-transforms.runtime.js"; @@ -83,11 +85,53 @@ import type { const log = createSubsystemLogger("plugins/provider-runtime"); const warnedExternalAuthFallbackPluginIds = new Set(); +let catalogHookProvidersCache = new WeakMap>(); + +function matchesProviderPluginRef(provider: ProviderPlugin, providerId: string): boolean { + const normalized = normalizeProviderId(providerId); + if (!normalized) { + return false; + } + if (normalizeProviderId(provider.id) === normalized) { + return true; + } + return [...(provider.aliases ?? []), ...(provider.hookAliases ?? [])].some( + (alias) => normalizeProviderId(alias) === normalized, + ); +} + +function hasExplicitProviderRuntimePluginActivation(params: { + provider: string; + config?: OpenClawConfig; + workspaceDir?: string; + env?: NodeJS.ProcessEnv; +}): boolean { + if (!params.config) { + return true; + } + const ownerPluginIds = + resolveOwningPluginIdsForProvider({ + provider: params.provider, + config: params.config, + workspaceDir: params.workspaceDir, + env: params.env, + }) ?? []; + if (ownerPluginIds.length === 0) { + return false; + } + const allow = new Set(params.config.plugins?.allow ?? []); + const entries = params.config.plugins?.entries ?? {}; + return ownerPluginIds.some((pluginId) => allow.has(pluginId) || entries[pluginId] !== undefined); +} function resetExternalAuthFallbackWarningCacheForTest(): void { warnedExternalAuthFallbackPluginIds.clear(); } +function resetCatalogHookProvidersCacheForTest(): void { + catalogHookProvidersCache = new WeakMap>(); +} + export { clearProviderRuntimeHookCache, prepareProviderExtraParams, @@ -102,6 +146,7 @@ export { export const __testing = { ...providerHookRuntimeTesting, resetExternalAuthFallbackWarningCacheForTest, + resetCatalogHookProvidersCacheForTest, } as const; function resolveProviderPluginsForCatalogHooks(params: { @@ -110,19 +155,37 @@ function resolveProviderPluginsForCatalogHooks(params: { env?: NodeJS.ProcessEnv; }): ProviderPlugin[] { const workspaceDir = params.workspaceDir ?? getActivePluginRegistryWorkspaceDirFromState(); + const env = params.env ?? process.env; + let envCache = catalogHookProvidersCache.get(env); + if (!envCache) { + envCache = new Map(); + catalogHookProvidersCache.set(env, envCache); + } + const cacheKey = JSON.stringify({ + workspaceDir: workspaceDir ?? "", + plugins: params.config?.plugins ?? null, + }); + const cached = envCache.get(cacheKey); + if (cached) { + return cached; + } const onlyPluginIds = resolveCatalogHookProviderPluginIds({ config: params.config, workspaceDir, - env: params.env, + env, }); if (onlyPluginIds.length === 0) { + envCache.set(cacheKey, []); return []; } - return resolveProviderPluginsForHooks({ + const providers = resolveProviderPluginsForHooks({ ...params, workspaceDir, + env, onlyPluginIds, }); + envCache.set(cacheKey, providers); + return providers; } export function runProviderDynamicModel(params: { @@ -410,6 +473,7 @@ export function normalizeProviderConfigWithPlugin(params: { workspaceDir?: string; env?: NodeJS.ProcessEnv; context: ProviderNormalizeConfigContext; + allowRuntimePluginLoad?: boolean; }): ModelProviderConfig | undefined { const hasConfigChange = (normalized: ModelProviderConfig) => normalized !== params.context.providerConfig; @@ -418,23 +482,15 @@ export function normalizeProviderConfigWithPlugin(params: { const normalized = bundledSurface.normalizeConfig(params.context); return normalized && hasConfigChange(normalized) ? normalized : undefined; } - const matchedPlugin = resolveProviderHookPlugin(params); + if (!hasExplicitProviderRuntimePluginActivation(params)) { + return undefined; + } + if (params.allowRuntimePluginLoad === false) { + return undefined; + } + const matchedPlugin = resolveProviderRuntimePlugin(params); const normalizedMatched = matchedPlugin?.normalizeConfig?.(params.context); - if (normalizedMatched && hasConfigChange(normalizedMatched)) { - return normalizedMatched; - } - - for (const candidate of resolveProviderPluginsForHooks(params)) { - if (!candidate.normalizeConfig || candidate === matchedPlugin) { - continue; - } - const normalized = candidate.normalizeConfig(params.context); - if (normalized && hasConfigChange(normalized)) { - return normalized; - } - } - - return undefined; + return normalizedMatched && hasConfigChange(normalizedMatched) ? normalizedMatched : undefined; } export function applyProviderNativeStreamingUsageCompatWithPlugin(params: { @@ -443,9 +499,13 @@ export function applyProviderNativeStreamingUsageCompatWithPlugin(params: { workspaceDir?: string; env?: NodeJS.ProcessEnv; context: ProviderNormalizeConfigContext; + allowRuntimePluginLoad?: boolean; }): ModelProviderConfig | undefined { + if (params.allowRuntimePluginLoad === false) { + return undefined; + } return ( - resolveProviderHookPlugin(params)?.applyNativeStreamingUsageCompat?.(params.context) ?? + resolveProviderRuntimePlugin(params)?.applyNativeStreamingUsageCompat?.(params.context) ?? undefined ); } @@ -456,13 +516,17 @@ export function resolveProviderConfigApiKeyWithPlugin(params: { workspaceDir?: string; env?: NodeJS.ProcessEnv; context: ProviderResolveConfigApiKeyContext; + allowRuntimePluginLoad?: boolean; }): string | undefined { const bundledSurface = resolveBundledProviderPolicySurface(params.provider); if (bundledSurface?.resolveConfigApiKey) { return normalizeOptionalString(bundledSurface.resolveConfigApiKey(params.context)); } + if (params.allowRuntimePluginLoad === false) { + return undefined; + } return normalizeOptionalString( - resolveProviderHookPlugin(params)?.resolveConfigApiKey?.(params.context), + resolveProviderRuntimePlugin(params)?.resolveConfigApiKey?.(params.context), ); } @@ -775,9 +839,34 @@ export function resolveProviderSyntheticAuthWithPlugin(params: { env?: NodeJS.ProcessEnv; context: ProviderResolveSyntheticAuthContext; }) { - const runtimeResolved = resolveProviderRuntimePlugin(params)?.resolveSyntheticAuth?.( - params.context, - ); + const discoveryPluginIds = + resolveOwningPluginIdsForProvider({ + provider: params.provider, + config: params.config, + workspaceDir: params.workspaceDir, + env: params.env, + }) ?? []; + const discoveryProvider = ( + discoveryPluginIds.length > 0 + ? resolvePluginDiscoveryProvidersRuntime({ + config: params.config, + workspaceDir: params.workspaceDir, + env: params.env, + onlyPluginIds: discoveryPluginIds, + discoveryEntriesOnly: true, + }) + : [] + ).find((provider) => matchesProviderPluginRef(provider, params.provider)); + if (typeof discoveryProvider?.resolveSyntheticAuth === "function") { + return discoveryProvider.resolveSyntheticAuth(params.context) ?? undefined; + } + const runtimeResolved = resolveProviderRuntimePlugin({ + ...params, + applyAutoEnable: false, + bundledProviderAllowlistCompat: false, + bundledProviderVitestCompat: false, + installBundledRuntimeDeps: false, + })?.resolveSyntheticAuth?.(params.context); if (runtimeResolved) { return runtimeResolved; } diff --git a/src/plugins/providers.runtime.ts b/src/plugins/providers.runtime.ts index 346761cf46e..7b3cfbbb087 100644 --- a/src/plugins/providers.runtime.ts +++ b/src/plugins/providers.runtime.ts @@ -195,7 +195,7 @@ function resolveRuntimeProviderPluginLoadState( env: base.env, workspaceDir: base.workspaceDir, onlyPluginIds: runtimeRequestedPluginIds, - applyAutoEnable: true, + applyAutoEnable: params.applyAutoEnable ?? true, compatMode: { allowlist: params.bundledProviderAllowlistCompat, enablement: "allowlist", @@ -233,6 +233,7 @@ function resolveRuntimeProviderPluginLoadState( pluginSdkResolution: params.pluginSdkResolution, cache: params.cache ?? true, activate: params.activate ?? false, + installBundledRuntimeDeps: params.installBundledRuntimeDeps, }, ); return { loadOptions }; @@ -264,6 +265,8 @@ export function resolvePluginProviders(params: { modelRefs?: readonly string[]; activate?: boolean; cache?: boolean; + applyAutoEnable?: boolean; + installBundledRuntimeDeps?: boolean; pluginSdkResolution?: PluginLoadOptions["pluginSdkResolution"]; mode?: "runtime" | "setup"; includeUntrustedWorkspacePlugins?: boolean; diff --git a/src/plugins/providers.ts b/src/plugins/providers.ts index 22f932ea344..c972c3619a5 100644 --- a/src/plugins/providers.ts +++ b/src/plugins/providers.ts @@ -426,6 +426,30 @@ function dedupeSortedPluginIds(values: Iterable): string[] { return [...new Set(values)].toSorted((left, right) => left.localeCompare(right)); } +let owningProviderPluginIdsCache = new WeakMap< + NodeJS.ProcessEnv, + Map +>(); + +function buildOwningProviderPluginIdsCacheKey(params: { + provider: string; + config?: PluginLoadOptions["config"]; + workspaceDir?: string; +}): string { + return JSON.stringify({ + provider: normalizeProviderId(params.provider), + workspaceDir: params.workspaceDir ?? "", + plugins: params.config?.plugins ?? null, + }); +} + +export function resetProviderOwnerPluginIdsCacheForTest(): void { + owningProviderPluginIdsCache = new WeakMap< + NodeJS.ProcessEnv, + Map + >(); +} + function resolvePreferredManifestPluginIds( registry: PluginManifestRegistry, matchedPluginIds: readonly string[], @@ -478,18 +502,33 @@ export function resolveOwningPluginIdsForProvider(params: { return pluginIds.length > 0 ? pluginIds : undefined; } + const env = params.env ?? process.env; + let envCache = owningProviderPluginIdsCache.get(env); + if (!envCache) { + envCache = new Map(); + owningProviderPluginIdsCache.set(env, envCache); + } + const cacheKey = buildOwningProviderPluginIdsCacheKey({ + provider: normalizedProvider, + config: params.config, + workspaceDir: params.workspaceDir, + }); + if (envCache.has(cacheKey)) { + return envCache.get(cacheKey); + } + const pluginIds = [ ...resolveProviderOwners({ config: params.config, workspaceDir: params.workspaceDir, - env: params.env, + env, providerId: normalizedProvider, includeDisabled: true, }), ...resolvePluginContributionOwners({ config: params.config, workspaceDir: params.workspaceDir, - env: params.env, + env, contribution: "cliBackends", matches: (backendId) => normalizeProviderId(backendId) === normalizedProvider, includeDisabled: true, @@ -497,7 +536,9 @@ export function resolveOwningPluginIdsForProvider(params: { ]; const deduped = dedupeSortedPluginIds(pluginIds); - return deduped.length > 0 ? deduped : undefined; + const resolved = deduped.length > 0 ? deduped : undefined; + envCache.set(cacheKey, resolved); + return resolved; } export function resolveOwningPluginIdsForModelRef(params: {