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

@@ -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: {