fix(plugins): select runtime deps by configured models

Co-authored-by: openclaw-clawsweeper[bot] <280122609+openclaw-clawsweeper[bot]@users.noreply.github.com>
This commit is contained in:
clawsweeper[bot]
2026-04-29 22:27:54 -07:00
committed by GitHub
parent 0459206c40
commit b876ecdb84
4 changed files with 221 additions and 25 deletions

View File

@@ -1,5 +1,6 @@
import fs from "node:fs";
import path from "node:path";
import { splitTrailingAuthProfile } from "../agents/model-ref-profile.js";
import { normalizeProviderId } from "../agents/provider-id.js";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import { normalizeOptionalLowercaseString } from "../shared/string-coerce.js";
@@ -29,11 +30,17 @@ export type BundledPluginRuntimeDepsManifest = {
enabledByDefault: boolean;
id?: string;
legacyPluginIds: string[];
modelSupport?: BundledPluginRuntimeDepsModelSupport;
providers: string[];
};
export type BundledPluginRuntimeDepsManifestCache = Map<string, BundledPluginRuntimeDepsManifest>;
type BundledPluginRuntimeDepsModelSupport = {
modelPatterns: string[];
modelPrefixes: string[];
};
function collectDeclaredMirroredRootRuntimeDepNames(packageJson: JsonObject): string[] {
const openclaw = packageJson.openclaw;
const bundle =
@@ -103,6 +110,7 @@ function readBundledPluginRuntimeDepsManifest(
const manifest = readRuntimeDepsJsonObject(path.join(pluginDir, "openclaw.plugin.json"));
const channels = manifest?.channels;
const legacyPluginIds = manifest?.legacyPluginIds;
const modelSupport = readBundledPluginRuntimeDepsModelSupport(manifest?.modelSupport);
const providers = manifest?.providers;
const runtimeDepsManifest = {
channels: Array.isArray(channels)
@@ -115,6 +123,7 @@ function readBundledPluginRuntimeDepsManifest(
(entry): entry is string => typeof entry === "string" && entry !== "",
)
: [],
...(modelSupport ? { modelSupport } : {}),
providers: Array.isArray(providers)
? providers.filter((entry): entry is string => typeof entry === "string" && entry !== "")
: [],
@@ -123,6 +132,27 @@ function readBundledPluginRuntimeDepsManifest(
return runtimeDepsManifest;
}
function readBundledPluginRuntimeDepsModelSupport(
value: unknown,
): BundledPluginRuntimeDepsModelSupport | undefined {
if (!isRecord(value)) {
return undefined;
}
const modelPatterns = readRuntimeDepsManifestStringList(value.modelPatterns);
const modelPrefixes = readRuntimeDepsManifestStringList(value.modelPrefixes);
if (modelPatterns.length === 0 && modelPrefixes.length === 0) {
return undefined;
}
return { modelPatterns, modelPrefixes };
}
function readRuntimeDepsManifestStringList(value: unknown): string[] {
if (!Array.isArray(value)) {
return [];
}
return value.filter((entry): entry is string => typeof entry === "string" && entry !== "");
}
const BUILT_IN_RUNTIME_DEPS_PLUGIN_ALIAS_FALLBACKS: ReadonlyArray<
readonly [alias: string, pluginId: string]
> = [
@@ -212,69 +242,176 @@ function isRecord(value: unknown): value is Record<string, unknown> {
return Boolean(value && typeof value === "object" && !Array.isArray(value));
}
function addConfiguredProviderId(providerIds: Set<string>, value: unknown): void {
type ConfiguredRuntimeDepsTargets = {
modelRefs: Set<string>;
providerIds: Set<string>;
};
function createConfiguredRuntimeDepsTargets(): ConfiguredRuntimeDepsTargets {
return {
modelRefs: new Set(),
providerIds: new Set(),
};
}
function addConfiguredProviderId(targets: ConfiguredRuntimeDepsTargets, value: unknown): void {
if (typeof value !== "string") {
return;
}
const normalized = normalizeProviderId(value);
if (normalized) {
providerIds.add(normalized);
targets.providerIds.add(normalized);
}
}
function addConfiguredProviderFromModelRef(providerIds: Set<string>, value: unknown): void {
function addConfiguredModelRef(targets: ConfiguredRuntimeDepsTargets, value: unknown): void {
if (typeof value !== "string") {
return;
}
const providerId = value.split("/", 1)[0]?.trim();
addConfiguredProviderId(providerIds, providerId);
const parsed = parseConfiguredModelRef(value);
if (!parsed) {
return;
}
if (parsed.providerId) {
targets.providerIds.add(parsed.providerId);
} else {
targets.modelRefs.add(parsed.modelId);
}
}
function addConfiguredProvidersFromModelConfig(providerIds: Set<string>, value: unknown): void {
function parseConfiguredModelRef(
value: string,
): { modelId: string; providerId?: string } | undefined {
const trimmed = value.trim();
if (!trimmed) {
return undefined;
}
const slash = trimmed.indexOf("/");
if (slash < 0) {
const modelId = splitTrailingAuthProfile(trimmed).model.trim();
return modelId ? { modelId } : undefined;
}
const providerId = normalizeProviderId(trimmed.slice(0, slash));
const modelId = splitTrailingAuthProfile(trimmed.slice(slash + 1)).model.trim();
if (!providerId || !modelId) {
return undefined;
}
return { providerId, modelId };
}
function addConfiguredModelsFromModelConfig(
targets: ConfiguredRuntimeDepsTargets,
value: unknown,
): void {
if (typeof value === "string") {
addConfiguredProviderFromModelRef(providerIds, value);
addConfiguredModelRef(targets, value);
return;
}
if (!isRecord(value)) {
return;
}
addConfiguredProviderFromModelRef(providerIds, value.primary);
addConfiguredModelRef(targets, value.primary);
if (Array.isArray(value.fallbacks)) {
for (const fallback of value.fallbacks) {
addConfiguredProviderFromModelRef(providerIds, fallback);
addConfiguredModelRef(targets, fallback);
}
}
}
function collectConfiguredProviderIds(config: OpenClawConfig): Set<string> {
const providerIds = new Set<string>();
function collectConfiguredRuntimeDepsTargets(config: OpenClawConfig): ConfiguredRuntimeDepsTargets {
const targets = createConfiguredRuntimeDepsTargets();
for (const providerId of Object.keys(config.models?.providers ?? {})) {
addConfiguredProviderId(providerIds, providerId);
addConfiguredProviderId(targets, providerId);
}
for (const profile of Object.values(config.auth?.profiles ?? {})) {
addConfiguredProviderId(providerIds, profile.provider);
addConfiguredProviderId(targets, profile.provider);
}
for (const providerId of Object.keys(config.auth?.order ?? {})) {
addConfiguredProviderId(providerIds, providerId);
addConfiguredProviderId(targets, providerId);
}
const defaults = config.agents?.defaults;
addConfiguredProvidersFromModelConfig(providerIds, defaults?.model);
addConfiguredProvidersFromModelConfig(providerIds, defaults?.imageModel);
addConfiguredProvidersFromModelConfig(providerIds, defaults?.imageGenerationModel);
addConfiguredProvidersFromModelConfig(providerIds, defaults?.videoGenerationModel);
addConfiguredProvidersFromModelConfig(providerIds, defaults?.musicGenerationModel);
addConfiguredProvidersFromModelConfig(providerIds, defaults?.pdfModel);
addConfiguredProvidersFromModelConfig(providerIds, defaults?.subagents?.model);
addConfiguredModelsFromModelConfig(targets, defaults?.model);
addConfiguredModelsFromModelConfig(targets, defaults?.imageModel);
addConfiguredModelsFromModelConfig(targets, defaults?.imageGenerationModel);
addConfiguredModelsFromModelConfig(targets, defaults?.videoGenerationModel);
addConfiguredModelsFromModelConfig(targets, defaults?.musicGenerationModel);
addConfiguredModelsFromModelConfig(targets, defaults?.pdfModel);
addConfiguredModelsFromModelConfig(targets, defaults?.subagents?.model);
for (const providerId of Object.keys(defaults?.models ?? {})) {
addConfiguredProviderFromModelRef(providerIds, providerId);
addConfiguredModelRef(targets, providerId);
}
for (const agent of config.agents?.list ?? []) {
addConfiguredProvidersFromModelConfig(providerIds, agent.model);
addConfiguredProvidersFromModelConfig(providerIds, agent.subagents?.model);
addConfiguredModelsFromModelConfig(targets, agent.model);
addConfiguredModelsFromModelConfig(targets, agent.subagents?.model);
}
return providerIds;
return targets;
}
function collectConfiguredProviderIds(config: OpenClawConfig): Set<string> {
return collectConfiguredRuntimeDepsTargets(config).providerIds;
}
function matchesBundledRuntimeDepsModelSupport(
manifest: BundledPluginRuntimeDepsManifest,
modelId: string,
kind: "pattern" | "prefix",
): boolean {
if (kind === "pattern") {
for (const patternSource of manifest.modelSupport?.modelPatterns ?? []) {
try {
if (new RegExp(patternSource, "u").test(modelId)) {
return true;
}
} catch {
continue;
}
}
return false;
}
return (manifest.modelSupport?.modelPrefixes ?? []).some((prefix) => modelId.startsWith(prefix));
}
export function resolveBundledRuntimeDepsConfiguredModelOwnerPluginIds(params: {
config: OpenClawConfig;
extensionsDir: string;
manifestCache?: BundledPluginRuntimeDepsManifestCache;
}): ReadonlySet<string> {
const targets = collectConfiguredRuntimeDepsTargets(params.config);
if (targets.modelRefs.size === 0 || !fs.existsSync(params.extensionsDir)) {
return new Set();
}
const plugins = fs
.readdirSync(params.extensionsDir, { withFileTypes: true })
.filter((entry) => entry.isDirectory())
.map((entry) => {
const pluginDir = path.join(params.extensionsDir, entry.name);
return {
pluginId: entry.name,
manifest: readBundledPluginRuntimeDepsManifest(pluginDir, params.manifestCache),
};
});
const pluginIds = new Set<string>();
for (const modelId of targets.modelRefs) {
const patternMatches = plugins.filter(({ manifest }) =>
matchesBundledRuntimeDepsModelSupport(manifest, modelId, "pattern"),
);
if (patternMatches.length === 1) {
pluginIds.add(patternMatches[0].pluginId);
continue;
}
if (patternMatches.length > 1) {
continue;
}
const prefixMatches = plugins.filter(({ manifest }) =>
matchesBundledRuntimeDepsModelSupport(manifest, modelId, "prefix"),
);
if (prefixMatches.length === 1) {
pluginIds.add(prefixMatches[0].pluginId);
}
}
return pluginIds;
}
function isBundledProviderConfiguredForRuntimeDeps(params: {
@@ -295,6 +432,7 @@ export function isBundledPluginConfiguredForRuntimeDeps(params: {
plugins: NormalizedPluginsConfig;
pluginId: string;
pluginDir: string;
configuredModelOwnerPluginIds?: ReadonlySet<string>;
includeConfiguredChannels?: boolean;
manifestCache?: BundledPluginRuntimeDepsManifestCache;
}): boolean {
@@ -363,6 +501,9 @@ export function isBundledPluginConfiguredForRuntimeDeps(params: {
if (hasConfiguredChannel) {
return true;
}
if (params.configuredModelOwnerPluginIds?.has(params.pluginId)) {
return true;
}
if (
isBundledProviderConfiguredForRuntimeDeps({
config: params.config,
@@ -409,6 +550,7 @@ function shouldIncludeBundledPluginRuntimeDeps(params: {
selectedPluginIds?: ReadonlySet<string>;
pluginId: string;
pluginDir: string;
configuredModelOwnerPluginIds?: ReadonlySet<string>;
includeConfiguredChannels?: boolean;
manifestCache?: BundledPluginRuntimeDepsManifestCache;
}): boolean {
@@ -458,6 +600,7 @@ function shouldIncludeBundledPluginRuntimeDeps(params: {
plugins: params.plugins,
pluginId: params.pluginId,
pluginDir: params.pluginDir,
configuredModelOwnerPluginIds: params.configuredModelOwnerPluginIds,
includeConfiguredChannels: params.includeConfiguredChannels,
manifestCache: params.manifestCache,
});
@@ -490,6 +633,14 @@ export function collectBundledPluginRuntimeDeps(params: {
const plugins = params.config
? normalizePluginsConfigWithResolver(params.config.plugins, normalizePluginId)
: undefined;
const configuredModelOwnerPluginIds =
params.config && plugins
? resolveBundledRuntimeDepsConfiguredModelOwnerPluginIds({
config: params.config,
extensionsDir: params.extensionsDir,
manifestCache,
})
: undefined;
const includedPluginIds = new Set<string>();
for (const entry of fs.readdirSync(params.extensionsDir, { withFileTypes: true })) {
@@ -506,6 +657,7 @@ export function collectBundledPluginRuntimeDeps(params: {
selectedPluginIds: params.selectedPluginIds,
pluginId,
pluginDir,
configuredModelOwnerPluginIds,
includeConfiguredChannels: params.includeConfiguredChannels,
manifestCache,
})

View File

@@ -1072,6 +1072,20 @@ describe("scanBundledPluginRuntimeDeps config policy", () => {
enabledByDefault: true,
providers: ["amazon-bedrock"],
});
writeBundledPluginPackage({
packageRoot,
pluginId: "anthropic",
deps: { "anthropic-runtime": "4.0.0" },
modelSupport: { modelPrefixes: ["claude-"] },
providers: ["anthropic"],
});
writeBundledPluginPackage({
packageRoot,
pluginId: "openai",
deps: { "openai-runtime": "5.0.0" },
modelSupport: { modelPrefixes: ["gpt-", "o1", "o3", "o4"] },
providers: ["openai", "openai-codex"],
});
return packageRoot;
}
@@ -1175,6 +1189,28 @@ describe("scanBundledPluginRuntimeDeps config policy", () => {
includeConfiguredChannels: false,
expectedDeps: ["alpha-runtime@1.0.0", "bedrock-runtime@3.0.0"],
},
{
name: "includes configured bare model owner deps from model support",
config: { agents: { defaults: { model: "gpt-5.5" } } },
includeConfiguredChannels: false,
expectedDeps: ["alpha-runtime@1.0.0", "openai-runtime@5.0.0"],
},
{
name: "includes configured bare fallback model owner deps from model support",
config: {
agents: {
defaults: { model: { primary: "unknown-model", fallbacks: ["claude-sonnet-4-6"] } },
},
},
includeConfiguredChannels: false,
expectedDeps: ["alpha-runtime@1.0.0", "anthropic-runtime@4.0.0"],
},
{
name: "includes configured model provider deps from manifest provider aliases",
config: { agents: { defaults: { model: "openai-codex/gpt-5.5" } } },
includeConfiguredChannels: false,
expectedDeps: ["alpha-runtime@1.0.0", "openai-runtime@5.0.0"],
},
{
name: "includes configured model provider deps from aliases",
config: { models: { providers: { "aws-bedrock": { baseUrl: "", models: [] } } } },

View File

@@ -49,6 +49,7 @@ import {
createBundledRuntimeDepsPluginIdNormalizer,
isBundledPluginConfiguredForRuntimeDeps,
normalizePluginIdSet,
resolveBundledRuntimeDepsConfiguredModelOwnerPluginIds,
type BundledPluginRuntimeDepsManifestCache,
type RuntimeDepConflict,
} from "./bundled-runtime-deps-selection.js";
@@ -309,6 +310,11 @@ export function ensureBundledPluginRuntimeDeps(params: {
plugins,
pluginId: params.pluginId,
pluginDir: params.pluginRoot,
configuredModelOwnerPluginIds: resolveBundledRuntimeDepsConfiguredModelOwnerPluginIds({
config: params.config,
extensionsDir,
manifestCache,
}),
manifestCache,
})
) {

View File

@@ -46,6 +46,7 @@ export function writeBundledPluginRuntimeDepsPackage(params: {
deps: Record<string, string>;
enabledByDefault?: boolean;
channels?: string[];
modelSupport?: { modelPatterns?: string[]; modelPrefixes?: string[] };
providers?: string[];
}): string {
const pluginRoot = path.join(params.packageRoot, "dist", "extensions", params.pluginId);
@@ -60,6 +61,7 @@ export function writeBundledPluginRuntimeDepsPackage(params: {
id: params.pluginId,
enabledByDefault: params.enabledByDefault === true,
...(params.channels ? { channels: params.channels } : {}),
...(params.modelSupport ? { modelSupport: params.modelSupport } : {}),
...(params.providers ? { providers: params.providers } : {}),
}),
);