fix(agents): avoid provider startup scans

This commit is contained in:
Peter Steinberger
2026-04-26 11:11:29 +01:00
parent 8bc4d4bcd4
commit 8ba9c9098a
15 changed files with 384 additions and 72 deletions

View File

@@ -2,6 +2,7 @@
"id": "anthropic",
"enabledByDefault": true,
"providers": ["anthropic"],
"providerDiscoveryEntry": "./provider-discovery.ts",
"modelSupport": {
"modelPrefixes": ["claude-"]
},

View File

@@ -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;

View File

@@ -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<typeof runOllamaDiscovery>;
@@ -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,

View File

@@ -2,6 +2,7 @@
"id": "xai",
"enabledByDefault": true,
"providers": ["xai"],
"providerDiscoveryEntry": "./provider-discovery.ts",
"providerEndpoints": [
{
"endpointClass": "xai-native",

View File

@@ -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;

View File

@@ -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,

View File

@@ -26,6 +26,22 @@ const PROVIDER_AUTH_ALIAS_ORIGIN_PRIORITY: Readonly<Record<PluginOrigin, number>
global: 2,
workspace: 3,
};
let providerAuthAliasMapCache = new WeakMap<
NodeJS.ProcessEnv,
Map<string, Record<string, string>>
>();
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<NodeJS.ProcessEnv, Map<string, Record<string, string>>>();
}
function resolveProviderAuthAliasOriginPriority(origin: PluginOrigin | undefined): number {
if (!origin) {
@@ -83,10 +99,21 @@ function setPreferredAlias(params: {
export function resolveProviderAuthAliasMap(
params?: ProviderAuthAliasLookupParams,
): Record<string, string> {
const env = params?.env ?? process.env;
const cacheKey = buildProviderAuthAliasMapCacheKey(params);
let envCache = providerAuthAliasMapCache.get(env);
if (!envCache) {
envCache = new Map<string, Record<string, string>>();
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<string, ProviderAuthAliasCandidate>();
@@ -119,6 +146,7 @@ export function resolveProviderAuthAliasMap(
for (const [alias, candidate] of preferredAliases) {
aliases[alias] = candidate.target;
}
envCache.set(cacheKey, aliases);
return aliases;
}

View File

@@ -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,

View File

@@ -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<void>;
onError?: (error: unknown) => void;
env?: NodeJS.ProcessEnv;
} = {},
): boolean {
): Promise<boolean> {
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<void> {
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;
}
}

View File

@@ -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));
}

View File

@@ -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();
});
});

View File

@@ -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<ResolvePluginProviders>((_) => [] as ProviderPlugin[]);
const isPluginProvidersLoadInFlightMock = vi.fn<IsPluginProvidersLoadInFlight>((_) => false);
@@ -36,6 +40,12 @@ const resolveExternalAuthProfileCompatFallbackPluginIdsMock =
vi.fn<ResolveExternalAuthProfileCompatFallbackPluginIds>((_) => [] as string[]);
const resolveExternalAuthProfileProviderPluginIdsMock =
vi.fn<ResolveExternalAuthProfileProviderPluginIds>((_) => [] as string[]);
const resolveOwningPluginIdsForProviderMock = vi.fn<ResolveOwningPluginIdsForProvider>(
(_) => undefined,
);
const resolveBundledProviderPolicySurfaceMock = vi.fn<ResolveBundledProviderPolicySurface>(
(_) => 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);
});
});

View File

@@ -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<string>();
let catalogHookProvidersCache = new WeakMap<NodeJS.ProcessEnv, Map<string, ProviderPlugin[]>>();
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<NodeJS.ProcessEnv, Map<string, ProviderPlugin[]>>();
}
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<string, ProviderPlugin[]>();
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;
}

View File

@@ -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;

View File

@@ -426,6 +426,30 @@ function dedupeSortedPluginIds(values: Iterable<string>): string[] {
return [...new Set(values)].toSorted((left, right) => left.localeCompare(right));
}
let owningProviderPluginIdsCache = new WeakMap<
NodeJS.ProcessEnv,
Map<string, string[] | undefined>
>();
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<string, string[] | undefined>
>();
}
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<string, string[] | undefined>();
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: {