From 7098e335bfe96f7999f4ff1de5641bf89393f413 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Wed, 3 Jun 2026 19:38:54 +0200 Subject: [PATCH] fix(models): recover bundled provider aliases --- src/commands/models/list.configured.test.ts | 61 +++++++++++++++ src/commands/models/provider-aliases.ts | 85 ++++++++++++++++++--- 2 files changed, 137 insertions(+), 9 deletions(-) diff --git a/src/commands/models/list.configured.test.ts b/src/commands/models/list.configured.test.ts index 3ffa5246a77c..ebb4c77d5a5a 100644 --- a/src/commands/models/list.configured.test.ts +++ b/src/commands/models/list.configured.test.ts @@ -1,3 +1,5 @@ +import fs from "node:fs"; +import os from "node:os"; import path from "node:path"; import { afterEach, describe, expect, it, vi } from "vitest"; @@ -116,4 +118,63 @@ describe("resolveConfiguredEntries", () => { expect(entries[0]?.aliases).toEqual(["GLM"]); expect(entries[0]?.tags).toEqual(new Set(["default", "configured"])); }); + + it("recovers bundled source aliases when stale dist metadata omits them", () => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-model-alias-source-")); + try { + const distPluginRoot = path.join(root, "dist", "extensions", "zai"); + const sourcePluginRoot = path.join(root, "extensions", "zai"); + fs.mkdirSync(distPluginRoot, { recursive: true }); + fs.mkdirSync(sourcePluginRoot, { recursive: true }); + fs.writeFileSync( + path.join(sourcePluginRoot, "openclaw.plugin.json"), + JSON.stringify({ + id: "zai", + configSchema: { type: "object" }, + providers: ["zai"], + modelCatalog: { + aliases: { + "z.ai": { provider: "zai" }, + }, + }, + }), + "utf8", + ); + + const { entries } = resolveConfiguredEntries( + { + agents: { + defaults: { + model: { primary: "z.ai/glm-4.7" }, + }, + }, + models: { providers: {} }, + }, + { + manifestRegistry: { + diagnostics: [], + plugins: [ + { + id: "zai", + origin: "bundled", + rootDir: distPluginRoot, + source: path.join(distPluginRoot, "index.js"), + providers: ["zai"], + channels: [], + cliBackends: [], + skills: [], + hooks: [], + modelCatalog: { providers: {}, discovery: { zai: "static" } }, + manifestPath: path.join(distPluginRoot, "openclaw.plugin.json"), + }, + ], + }, + }, + ); + + expect(entries.map((entry) => entry.key)).toEqual(["zai/glm-4.7"]); + } finally { + fs.rmSync(root, { recursive: true, force: true }); + } + }); }); diff --git a/src/commands/models/provider-aliases.ts b/src/commands/models/provider-aliases.ts index 7c55a559dfcc..d0c1d73da5c2 100644 --- a/src/commands/models/provider-aliases.ts +++ b/src/commands/models/provider-aliases.ts @@ -1,9 +1,12 @@ +import fs from "node:fs"; +import path from "node:path"; import { normalizeProviderId } from "../../agents/model-selection.js"; import type { OpenClawConfig } from "../../config/types.openclaw.js"; import { loadPluginManifestRegistry, type PluginManifestRecord, } from "../../plugins/manifest-registry.js"; +import { loadPluginManifest, type PluginManifestModelCatalog } from "../../plugins/manifest.js"; import type { PluginMetadataSnapshot } from "../../plugins/plugin-metadata-snapshot.js"; type ProviderAliasSource = { @@ -11,21 +14,85 @@ type ProviderAliasSource = { metadataSnapshot?: Pick; }; +const sourcePeerModelCatalogCache = new Map(); + function listManifestPlugins(params: ProviderAliasSource): readonly PluginManifestRecord[] { - return params.metadataSnapshot?.manifestRegistry.plugins ?? loadPluginManifestRegistry({ - config: params.cfg, - }).plugins; + return ( + params.metadataSnapshot?.manifestRegistry.plugins ?? + loadPluginManifestRegistry({ + config: params.cfg, + }).plugins + ); +} + +function resolveSourcePeerPluginRoot( + plugin: Pick, +): string | undefined { + if (plugin.origin !== "bundled") { + return undefined; + } + const parts = path.resolve(plugin.rootDir).split(path.sep); + const pluginDirName = parts.at(-1); + const extensionsDirName = parts.at(-2); + const buildDirName = parts.at(-3); + if ( + pluginDirName !== plugin.id || + extensionsDirName !== "extensions" || + (buildDirName !== "dist" && buildDirName !== "dist-runtime") + ) { + return undefined; + } + const packageRoot = parts.slice(0, -3).join(path.sep) || path.sep; + const sourceRoot = path.join(packageRoot, "extensions", plugin.id); + return fs.existsSync(path.join(sourceRoot, "openclaw.plugin.json")) ? sourceRoot : undefined; +} + +function loadSourcePeerModelCatalog( + plugin: Pick, +): PluginManifestModelCatalog | undefined { + const cacheKey = path.resolve(plugin.rootDir); + const cached = sourcePeerModelCatalogCache.get(cacheKey); + if (cached !== undefined) { + return cached ?? undefined; + } + const sourceRoot = resolveSourcePeerPluginRoot(plugin); + if (!sourceRoot) { + sourcePeerModelCatalogCache.set(cacheKey, null); + return undefined; + } + const loaded = loadPluginManifest(sourceRoot, false); + if (!loaded.ok || loaded.manifest.id !== plugin.id) { + sourcePeerModelCatalogCache.set(cacheKey, null); + return undefined; + } + const modelCatalog = loaded.manifest.modelCatalog ?? null; + sourcePeerModelCatalogCache.set(cacheKey, modelCatalog); + return modelCatalog ?? undefined; +} + +function hasModelCatalogAliases(modelCatalog: PluginManifestModelCatalog | undefined): boolean { + return Object.keys(modelCatalog?.aliases ?? {}).length > 0; +} + +function collectModelCatalogAliases( + aliases: Map, + modelCatalog: PluginManifestModelCatalog | undefined, +): void { + for (const [aliasProvider, target] of Object.entries(modelCatalog?.aliases ?? {})) { + const alias = normalizeProviderId(aliasProvider); + const provider = normalizeProviderId(target.provider); + if (alias && provider) { + aliases.set(alias, provider); + } + } } function buildProviderAliasMap(params: ProviderAliasSource): ReadonlyMap { const aliases = new Map(); for (const plugin of listManifestPlugins(params)) { - for (const [aliasProvider, target] of Object.entries(plugin.modelCatalog?.aliases ?? {})) { - const alias = normalizeProviderId(aliasProvider); - const provider = normalizeProviderId(target.provider); - if (alias && provider) { - aliases.set(alias, provider); - } + collectModelCatalogAliases(aliases, plugin.modelCatalog); + if (!hasModelCatalogAliases(plugin.modelCatalog) && plugin.origin === "bundled") { + collectModelCatalogAliases(aliases, loadSourcePeerModelCatalog(plugin)); } } return aliases;