diff --git a/src/agents/model-catalog.test.ts b/src/agents/model-catalog.test.ts index 4e476386116..532d7dbcb30 100644 --- a/src/agents/model-catalog.test.ts +++ b/src/agents/model-catalog.test.ts @@ -162,4 +162,94 @@ describe("loadModelCatalog", () => { }), ); }); + + it("normalizes configured provider aliases in merged catalog entries", async () => { + const cfg = { + models: { + providers: { + "z-ai": { + baseUrl: "https://api.z.ai/api/paas/v4/", + api: "openai-completions", + models: [ + { + id: "glm-5:free", + name: "GLM-5 (Free)", + reasoning: true, + input: ["text"], + contextWindow: 202800, + maxTokens: 131072, + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + }, + ], + }, + }, + }, + } satisfies OpenClawConfig; + + __setModelCatalogImportForTest( + async () => + ({ + AuthStorage: class {}, + ModelRegistry: class { + getAll() { + return []; + } + }, + }) as unknown as PiSdkModule, + ); + + const result = await loadModelCatalog({ config: cfg }); + expect(result).toContainEqual( + expect.objectContaining({ + provider: "zai", + id: "glm-5:free", + }), + ); + }); + + it("dedupes discovered and configured entries using normalized provider aliases", async () => { + const cfg = { + models: { + providers: { + "z-ai": { + baseUrl: "https://api.z.ai/api/paas/v4/", + api: "openai-completions", + models: [ + { + id: "glm-5:free", + name: "GLM-5 (Free)", + reasoning: true, + input: ["text"], + contextWindow: 202800, + maxTokens: 131072, + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + }, + ], + }, + }, + }, + } satisfies OpenClawConfig; + + __setModelCatalogImportForTest( + async () => + ({ + AuthStorage: class {}, + ModelRegistry: class { + getAll() { + return [ + { + id: "glm-5:free", + name: "GLM-5 (Free)", + provider: "zai", + }, + ]; + } + }, + }) as unknown as PiSdkModule, + ); + + const result = await loadModelCatalog({ config: cfg }); + const matches = result.filter((entry) => entry.provider === "zai" && entry.id === "glm-5:free"); + expect(matches).toHaveLength(1); + }); }); diff --git a/src/agents/model-catalog.ts b/src/agents/model-catalog.ts index dcf18e4528b..c3252d26fe7 100644 --- a/src/agents/model-catalog.ts +++ b/src/agents/model-catalog.ts @@ -2,6 +2,7 @@ import path from "node:path"; import { type OpenClawConfig, loadConfig } from "../config/config.js"; import { createSubsystemLogger } from "../logging/subsystem.js"; import { resolveOpenClawAgentDir } from "./agent-paths.js"; +import { normalizeProviderId } from "./model-selection.js"; import { ensureOpenClawModelsJson } from "./models-config.js"; const log = createSubsystemLogger("model-catalog"); @@ -99,6 +100,25 @@ function normalizeInput(modalities: unknown): Array<"text" | "image"> | undefine return hasImage ? ["text", "image"] : ["text"]; } +function normalizeCatalogProvider(value: unknown): string { + if (typeof value !== "string") { + return ""; + } + const trimmed = value.trim(); + if (!trimmed) { + return ""; + } + return normalizeProviderId(trimmed); +} + +function modelCatalogMergeKey(entry: Pick): string { + const provider = normalizeCatalogProvider(entry.provider); + const id = String(entry.id ?? "") + .trim() + .toLowerCase(); + return `${provider}/${id}`; +} + function readConfiguredModelsFromConfig(cfg: OpenClawConfig): ModelCatalogEntry[] { const providers = cfg.models?.providers; if (!providers) { @@ -106,7 +126,7 @@ function readConfiguredModelsFromConfig(cfg: OpenClawConfig): ModelCatalogEntry[ } const entries: ModelCatalogEntry[] = []; for (const [providerIdRaw, providerValue] of Object.entries(providers)) { - const provider = String(providerIdRaw ?? "").trim(); + const provider = normalizeCatalogProvider(providerIdRaw); if (!provider || !providerValue) { continue; } @@ -140,12 +160,10 @@ function mergeMissingCatalogEntries( discoveredModels: ModelCatalogEntry[], configuredModels: ModelCatalogEntry[], ): ModelCatalogEntry[] { - const seen = new Set( - discoveredModels.map((entry) => `${entry.provider.toLowerCase()}/${entry.id.toLowerCase()}`), - ); + const seen = new Set(discoveredModels.map((entry) => modelCatalogMergeKey(entry))); const merged = [...discoveredModels]; for (const entry of configuredModels) { - const key = `${entry.provider.toLowerCase()}/${entry.id.toLowerCase()}`; + const key = modelCatalogMergeKey(entry); if (seen.has(key)) { continue; } @@ -205,7 +223,7 @@ export async function loadModelCatalog(params?: { if (!id) { continue; } - const provider = String(entry?.provider ?? "").trim(); + const provider = normalizeCatalogProvider(entry?.provider); if (!provider) { continue; }