feat(plugins): split cold provider contributions

This commit is contained in:
Vincent Koc
2026-04-24 23:10:08 -07:00
parent fb4eec54a7
commit ea3e390346
2 changed files with 138 additions and 3 deletions

View File

@@ -1,13 +1,66 @@
import { describe, expect, it } from "vitest";
import fs from "node:fs";
import path from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import type { ModelProviderConfig } from "../config/types.js";
import type { PluginCandidate } from "./discovery.js";
import {
groupPluginDiscoveryProvidersByOrder,
normalizePluginDiscoveryResult,
resolveInstalledPluginProviderContributionIds,
runProviderCatalog,
runProviderStaticCatalog,
} from "./provider-discovery.js";
import { cleanupTrackedTempDirs, makeTrackedTempDir } from "./test-helpers/fs-fixtures.js";
import type { ProviderCatalogResult, ProviderDiscoveryOrder, ProviderPlugin } from "./types.js";
const tempDirs: string[] = [];
afterEach(() => {
cleanupTrackedTempDirs(tempDirs);
});
function makeTempDir() {
return makeTrackedTempDir("openclaw-provider-discovery", tempDirs);
}
function hermeticEnv(overrides: NodeJS.ProcessEnv = {}): NodeJS.ProcessEnv {
return {
OPENCLAW_BUNDLED_PLUGINS_DIR: undefined,
OPENCLAW_DISABLE_PLUGIN_DISCOVERY_CACHE: "1",
OPENCLAW_DISABLE_PLUGIN_MANIFEST_CACHE: "1",
OPENCLAW_VERSION: "2026.4.25",
VITEST: "true",
...overrides,
};
}
function createProviderContributionCandidate(params: {
pluginId?: string;
providerIds?: readonly string[];
}): PluginCandidate {
const rootDir = makeTempDir();
fs.writeFileSync(
path.join(rootDir, "index.ts"),
"throw new Error('runtime provider entry should not load for cold contribution ids');\n",
"utf-8",
);
fs.writeFileSync(
path.join(rootDir, "openclaw.plugin.json"),
JSON.stringify({
id: params.pluginId ?? "demo",
configSchema: { type: "object" },
providers: params.providerIds ?? ["demo"],
}),
"utf-8",
);
return {
idHint: params.pluginId ?? "demo",
source: path.join(rootDir, "index.ts"),
rootDir,
origin: "global",
};
}
function makeProvider(params: {
id: string;
label?: string;
@@ -112,6 +165,50 @@ async function expectProviderCatalogResult(params: {
).resolves.toEqual(params.expected);
}
describe("resolveInstalledPluginProviderContributionIds", () => {
it("reads provider ids from the installed plugin index without importing runtime entries", () => {
const candidate = createProviderContributionCandidate({
pluginId: "demo",
providerIds: ["demo", "demo-alias"],
});
expect(
resolveInstalledPluginProviderContributionIds({
candidates: [candidate],
env: hermeticEnv(),
}),
).toEqual(["demo", "demo-alias"]);
});
it("omits disabled plugin provider ids unless explicitly requested", () => {
const candidate = createProviderContributionCandidate({
pluginId: "demo",
providerIds: ["demo"],
});
const params = {
candidates: [candidate],
config: {
plugins: {
entries: {
demo: {
enabled: false,
},
},
},
},
env: hermeticEnv(),
};
expect(resolveInstalledPluginProviderContributionIds(params)).toEqual([]);
expect(
resolveInstalledPluginProviderContributionIds({
...params,
includeDisabled: true,
}),
).toEqual(["demo"]);
});
});
describe("groupPluginDiscoveryProvidersByOrder", () => {
it.each([
{

View File

@@ -1,6 +1,11 @@
import { normalizeProviderId } from "../agents/model-selection.js";
import type { ModelProviderConfig } from "../config/types.js";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import {
loadInstalledPluginIndex,
type InstalledPluginIndex,
type LoadInstalledPluginIndexParams,
} from "./installed-plugin-index.js";
import type { ProviderDiscoveryOrder, ProviderPlugin } from "./types.js";
const DISCOVERY_ORDER: readonly ProviderDiscoveryOrder[] = ["simple", "profile", "paired", "late"];
@@ -28,7 +33,7 @@ function isSafeProviderConfigKey(value: string): boolean {
return value !== "" && !DANGEROUS_PROVIDER_KEYS.has(value);
}
export async function resolvePluginDiscoveryProviders(params: {
export type ResolveRuntimePluginDiscoveryProvidersParams = {
config?: OpenClawConfig;
workspaceDir?: string;
env?: NodeJS.ProcessEnv;
@@ -36,12 +41,45 @@ export async function resolvePluginDiscoveryProviders(params: {
includeUntrustedWorkspacePlugins?: boolean;
requireCompleteDiscoveryEntryCoverage?: boolean;
discoveryEntriesOnly?: boolean;
}): Promise<ProviderPlugin[]> {
};
export type ResolveInstalledPluginProviderContributionIdsParams = LoadInstalledPluginIndexParams & {
index?: InstalledPluginIndex;
includeDisabled?: boolean;
};
function sortedValues(values: Iterable<string>): string[] {
return [...new Set(values)].toSorted((left, right) => left.localeCompare(right));
}
export function resolveInstalledPluginProviderContributionIds(
params: ResolveInstalledPluginProviderContributionIdsParams = {},
): string[] {
const index = params.index ?? loadInstalledPluginIndex(params);
const providerIds: string[] = [];
for (const plugin of index.plugins) {
if (!params.includeDisabled && !plugin.enabled) {
continue;
}
providerIds.push(...plugin.contributions.providers);
}
return sortedValues(providerIds);
}
export async function resolveRuntimePluginDiscoveryProviders(
params: ResolveRuntimePluginDiscoveryProvidersParams,
): Promise<ProviderPlugin[]> {
return (await loadProviderRuntime())
.resolvePluginDiscoveryProvidersRuntime(params)
.filter((provider) => resolveProviderCatalogOrderHook(provider));
}
export async function resolvePluginDiscoveryProviders(
params: ResolveRuntimePluginDiscoveryProvidersParams,
): Promise<ProviderPlugin[]> {
return resolveRuntimePluginDiscoveryProviders(params);
}
export function groupPluginDiscoveryProvidersByOrder(
providers: ProviderPlugin[],
): Record<ProviderDiscoveryOrder, ProviderPlugin[]> {