fix: normalize provider aliases in model catalog merge

This commit is contained in:
Gustavo Madeira Santana
2026-02-23 20:32:10 -05:00
parent c28a268b87
commit d0143b0029
2 changed files with 114 additions and 6 deletions

View File

@@ -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);
});
});

View File

@@ -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<ModelCatalogEntry, "provider" | "id">): 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;
}