mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-06 05:51:15 +08:00
fix(commands): guard model provider aliases
This commit is contained in:
69
src/commands/models/provider-aliases.test.ts
Normal file
69
src/commands/models/provider-aliases.test.ts
Normal 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");
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user