fix(commands): guard model provider aliases

This commit is contained in:
Vincent Koc
2026-06-04 03:44:35 +02:00
parent ca3250a3c1
commit 4a96a6804f
2 changed files with 156 additions and 10 deletions

View File

@@ -0,0 +1,69 @@
import { describe, expect, it } from "vitest";
import { canonicalizeModelCatalogProviderAlias } from "./provider-aliases.js";
const basePlugin = {
channels: [],
cliBackends: [],
diagnostics: [],
hooks: [],
manifestPath: "/tmp/openclaw.plugin.json",
providers: [],
rootDir: "/tmp/plugin",
skills: [],
source: "test",
};
describe("canonicalizeModelCatalogProviderAlias", () => {
it("skips unreadable manifest alias metadata while preserving healthy aliases", () => {
const unreadableModelCatalog = {
...basePlugin,
id: "unreadable-model-catalog",
origin: "global",
get modelCatalog() {
throw new Error("model alias metadata exploded");
},
};
const unreadableAliasMap = {
...basePlugin,
id: "unreadable-alias-map",
origin: "global",
modelCatalog: {
aliases: new Proxy(
{},
{
ownKeys() {
throw new Error("model alias map exploded");
},
},
),
},
};
const healthyAlias = {
...basePlugin,
id: "healthy-alias",
origin: "global",
modelCatalog: {
aliases: {
broken: {
get provider() {
throw new Error("model alias provider exploded");
},
},
kimi: { provider: "moonshot" },
},
},
};
expect(
canonicalizeModelCatalogProviderAlias("kimi", {
cfg: {},
metadataSnapshot: {
manifestRegistry: {
plugins: [unreadableModelCatalog, unreadableAliasMap, healthyAlias],
diagnostics: [],
},
} as never,
}),
).toBe("moonshot");
});
});

View File

@@ -14,6 +14,8 @@ type ProviderAliasSource = {
metadataSnapshot?: Pick<PluginMetadataSnapshot, "manifestRegistry">;
};
type SourcePeerPlugin = Pick<PluginManifestRecord, "id" | "origin" | "rootDir">;
const sourcePeerModelCatalogCache = new Map<string, PluginManifestModelCatalog | null>();
function listManifestPlugins(params: ProviderAliasSource): readonly PluginManifestRecord[] {
@@ -25,9 +27,36 @@ function listManifestPlugins(params: ProviderAliasSource): readonly PluginManife
);
}
function resolveSourcePeerPluginRoot(
plugin: Pick<PluginManifestRecord, "id" | "origin" | "rootDir">,
): string | undefined {
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
function readPluginModelCatalog(
plugin: PluginManifestRecord,
): PluginManifestModelCatalog | undefined {
try {
return plugin.modelCatalog;
} catch {
return undefined;
}
}
function readSourcePeerPlugin(plugin: PluginManifestRecord): SourcePeerPlugin | undefined {
try {
if (
typeof plugin.id !== "string" ||
typeof plugin.origin !== "string" ||
typeof plugin.rootDir !== "string"
) {
return undefined;
}
return { id: plugin.id, origin: plugin.origin, rootDir: plugin.rootDir };
} catch {
return undefined;
}
}
function resolveSourcePeerPluginRoot(plugin: SourcePeerPlugin): string | undefined {
if (plugin.origin !== "bundled") {
return undefined;
}
@@ -48,7 +77,7 @@ function resolveSourcePeerPluginRoot(
}
function loadSourcePeerModelCatalog(
plugin: Pick<PluginManifestRecord, "id" | "origin" | "rootDir">,
plugin: SourcePeerPlugin,
): PluginManifestModelCatalog | undefined {
const cacheKey = path.resolve(plugin.rootDir);
const cached = sourcePeerModelCatalogCache.get(cacheKey);
@@ -70,17 +99,64 @@ function loadSourcePeerModelCatalog(
return modelCatalog ?? undefined;
}
function loadSourcePeerModelCatalogForPlugin(
plugin: PluginManifestRecord,
): PluginManifestModelCatalog | undefined {
const sourcePeerPlugin = readSourcePeerPlugin(plugin);
return sourcePeerPlugin ? loadSourcePeerModelCatalog(sourcePeerPlugin) : undefined;
}
function readModelCatalogAliasEntries(
modelCatalog: PluginManifestModelCatalog | undefined,
): readonly (readonly [string, Record<string, unknown>])[] {
let aliases: unknown;
try {
aliases = modelCatalog?.aliases;
} catch {
return [];
}
if (!isRecord(aliases)) {
return [];
}
let keys: string[];
try {
keys = Object.keys(aliases);
} catch {
return [];
}
const entries: [string, Record<string, unknown>][] = [];
for (const key of keys) {
try {
const target = aliases[key];
if (isRecord(target)) {
entries.push([key, target]);
}
} catch {
continue;
}
}
return entries;
}
function hasModelCatalogAliases(modelCatalog: PluginManifestModelCatalog | undefined): boolean {
return Object.keys(modelCatalog?.aliases ?? {}).length > 0;
return readModelCatalogAliasEntries(modelCatalog).length > 0;
}
function readAliasTargetProvider(target: Record<string, unknown>): string {
try {
return typeof target.provider === "string" ? target.provider : "";
} catch {
return "";
}
}
function collectModelCatalogAliases(
aliases: Map<string, string>,
modelCatalog: PluginManifestModelCatalog | undefined,
): void {
for (const [aliasProvider, target] of Object.entries(modelCatalog?.aliases ?? {})) {
for (const [aliasProvider, target] of readModelCatalogAliasEntries(modelCatalog)) {
const alias = normalizeProviderId(aliasProvider);
const provider = normalizeProviderId(target.provider);
const provider = normalizeProviderId(readAliasTargetProvider(target));
if (alias && provider) {
aliases.set(alias, provider);
}
@@ -90,9 +166,10 @@ function collectModelCatalogAliases(
function buildProviderAliasMap(params: ProviderAliasSource): ReadonlyMap<string, string> {
const aliases = new Map<string, string>();
for (const plugin of listManifestPlugins(params)) {
collectModelCatalogAliases(aliases, plugin.modelCatalog);
if (!hasModelCatalogAliases(plugin.modelCatalog) && plugin.origin === "bundled") {
collectModelCatalogAliases(aliases, loadSourcePeerModelCatalog(plugin));
const modelCatalog = readPluginModelCatalog(plugin);
collectModelCatalogAliases(aliases, modelCatalog);
if (!hasModelCatalogAliases(modelCatalog)) {
collectModelCatalogAliases(aliases, loadSourcePeerModelCatalogForPlugin(plugin));
}
}
return aliases;