mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-02 13:47:15 +00:00
revert: keep model-catalog behavior unchanged
This commit is contained in:
@@ -103,153 +103,4 @@ describe("loadModelCatalog", () => {
|
|||||||
expect(spark?.name).toBe("gpt-5.3-codex-spark");
|
expect(spark?.name).toBe("gpt-5.3-codex-spark");
|
||||||
expect(spark?.reasoning).toBe(true);
|
expect(spark?.reasoning).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("includes configured provider models from models.json when registry omits them", async () => {
|
|
||||||
const cfg = {
|
|
||||||
models: {
|
|
||||||
providers: {
|
|
||||||
kilocode: {
|
|
||||||
baseUrl: "https://api.kilo.ai/api/gateway/",
|
|
||||||
api: "openai-completions",
|
|
||||||
models: [
|
|
||||||
{
|
|
||||||
id: "anthropic/claude-opus-4.6",
|
|
||||||
name: "Claude Opus 4.6",
|
|
||||||
reasoning: true,
|
|
||||||
input: ["text", "image"],
|
|
||||||
contextWindow: 200000,
|
|
||||||
maxTokens: 8192,
|
|
||||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "openai/gpt-5.2",
|
|
||||||
name: "GPT-5.2",
|
|
||||||
reasoning: true,
|
|
||||||
input: ["text"],
|
|
||||||
contextWindow: 400000,
|
|
||||||
maxTokens: 8192,
|
|
||||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
} satisfies OpenClawConfig;
|
|
||||||
|
|
||||||
__setModelCatalogImportForTest(
|
|
||||||
async () =>
|
|
||||||
({
|
|
||||||
AuthStorage: class {},
|
|
||||||
ModelRegistry: class {
|
|
||||||
getAll() {
|
|
||||||
return [{ id: "gpt-4.1", name: "GPT-4.1", provider: "openai" }];
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}) as unknown as PiSdkModule,
|
|
||||||
);
|
|
||||||
|
|
||||||
const result = await loadModelCatalog({ config: cfg });
|
|
||||||
expect(result).toContainEqual(
|
|
||||||
expect.objectContaining({
|
|
||||||
provider: "kilocode",
|
|
||||||
id: "anthropic/claude-opus-4.6",
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
expect(result).toContainEqual(
|
|
||||||
expect.objectContaining({
|
|
||||||
provider: "kilocode",
|
|
||||||
id: "openai/gpt-5.2",
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
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);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
import path from "node:path";
|
|
||||||
import { type OpenClawConfig, loadConfig } from "../config/config.js";
|
import { type OpenClawConfig, loadConfig } from "../config/config.js";
|
||||||
import { createSubsystemLogger } from "../logging/subsystem.js";
|
import { createSubsystemLogger } from "../logging/subsystem.js";
|
||||||
import { resolveOpenClawAgentDir } from "./agent-paths.js";
|
import { resolveOpenClawAgentDir } from "./agent-paths.js";
|
||||||
import { normalizeProviderId } from "./model-selection.js";
|
|
||||||
import { ensureOpenClawModelsJson } from "./models-config.js";
|
import { ensureOpenClawModelsJson } from "./models-config.js";
|
||||||
|
|
||||||
const log = createSubsystemLogger("model-catalog");
|
const log = createSubsystemLogger("model-catalog");
|
||||||
@@ -25,14 +23,6 @@ type DiscoveredModel = {
|
|||||||
input?: Array<"text" | "image">;
|
input?: Array<"text" | "image">;
|
||||||
};
|
};
|
||||||
|
|
||||||
type ConfigModelEntry = {
|
|
||||||
id?: unknown;
|
|
||||||
name?: unknown;
|
|
||||||
contextWindow?: unknown;
|
|
||||||
reasoning?: unknown;
|
|
||||||
input?: unknown;
|
|
||||||
};
|
|
||||||
|
|
||||||
type PiSdkModule = typeof import("./pi-model-discovery.js");
|
type PiSdkModule = typeof import("./pi-model-discovery.js");
|
||||||
|
|
||||||
let modelCatalogPromise: Promise<ModelCatalogEntry[]> | null = null;
|
let modelCatalogPromise: Promise<ModelCatalogEntry[]> | null = null;
|
||||||
@@ -88,91 +78,6 @@ function createAuthStorage(AuthStorageLike: unknown, path: string) {
|
|||||||
return new (AuthStorageLike as { new (path: string): unknown })(path);
|
return new (AuthStorageLike as { new (path: string): unknown })(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
function toPositiveNumber(value: unknown): number | undefined {
|
|
||||||
return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeInput(modalities: unknown): Array<"text" | "image"> | undefined {
|
|
||||||
if (!Array.isArray(modalities) || modalities.length === 0) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
const hasImage = modalities.some((value) => String(value ?? "").toLowerCase() === "image");
|
|
||||||
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) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
const entries: ModelCatalogEntry[] = [];
|
|
||||||
for (const [providerIdRaw, providerValue] of Object.entries(providers)) {
|
|
||||||
const provider = normalizeCatalogProvider(providerIdRaw);
|
|
||||||
if (!provider || !providerValue) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const providerModels = (providerValue as { models?: unknown }).models;
|
|
||||||
if (!Array.isArray(providerModels)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
for (const modelValue of providerModels) {
|
|
||||||
if (!modelValue || typeof modelValue !== "object") {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const model = modelValue as ConfigModelEntry;
|
|
||||||
if (typeof model.id !== "string") {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const id = model.id.trim();
|
|
||||||
if (!id) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const name = typeof model.name === "string" && model.name.trim() ? model.name.trim() : id;
|
|
||||||
const contextWindow = toPositiveNumber(model.contextWindow);
|
|
||||||
const reasoning = typeof model.reasoning === "boolean" ? model.reasoning : undefined;
|
|
||||||
const input = normalizeInput(model.input);
|
|
||||||
entries.push({ id, name, provider, contextWindow, reasoning, input });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return entries;
|
|
||||||
}
|
|
||||||
|
|
||||||
function mergeMissingCatalogEntries(
|
|
||||||
discoveredModels: ModelCatalogEntry[],
|
|
||||||
configuredModels: ModelCatalogEntry[],
|
|
||||||
): ModelCatalogEntry[] {
|
|
||||||
const seen = new Set(discoveredModels.map((entry) => modelCatalogMergeKey(entry)));
|
|
||||||
const merged = [...discoveredModels];
|
|
||||||
for (const entry of configuredModels) {
|
|
||||||
const key = modelCatalogMergeKey(entry);
|
|
||||||
if (seen.has(key)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
seen.add(key);
|
|
||||||
merged.push(entry);
|
|
||||||
}
|
|
||||||
return merged;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function loadModelCatalog(params?: {
|
export async function loadModelCatalog(params?: {
|
||||||
config?: OpenClawConfig;
|
config?: OpenClawConfig;
|
||||||
useCache?: boolean;
|
useCache?: boolean;
|
||||||
@@ -206,7 +111,8 @@ export async function loadModelCatalog(params?: {
|
|||||||
// will keep failing until restart).
|
// will keep failing until restart).
|
||||||
const piSdk = await importPiSdk();
|
const piSdk = await importPiSdk();
|
||||||
const agentDir = resolveOpenClawAgentDir();
|
const agentDir = resolveOpenClawAgentDir();
|
||||||
const authStorage = createAuthStorage(piSdk.AuthStorage, path.join(agentDir, "auth.json"));
|
const { join } = await import("node:path");
|
||||||
|
const authStorage = createAuthStorage(piSdk.AuthStorage, join(agentDir, "auth.json"));
|
||||||
const registry = new (piSdk.ModelRegistry as unknown as {
|
const registry = new (piSdk.ModelRegistry as unknown as {
|
||||||
new (
|
new (
|
||||||
authStorage: unknown,
|
authStorage: unknown,
|
||||||
@@ -216,14 +122,14 @@ export async function loadModelCatalog(params?: {
|
|||||||
| {
|
| {
|
||||||
getAll: () => Array<DiscoveredModel>;
|
getAll: () => Array<DiscoveredModel>;
|
||||||
};
|
};
|
||||||
})(authStorage, path.join(agentDir, "models.json"));
|
})(authStorage, join(agentDir, "models.json"));
|
||||||
const entries = Array.isArray(registry) ? registry : registry.getAll();
|
const entries = Array.isArray(registry) ? registry : registry.getAll();
|
||||||
for (const entry of entries) {
|
for (const entry of entries) {
|
||||||
const id = String(entry?.id ?? "").trim();
|
const id = String(entry?.id ?? "").trim();
|
||||||
if (!id) {
|
if (!id) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const provider = normalizeCatalogProvider(entry?.provider);
|
const provider = String(entry?.provider ?? "").trim();
|
||||||
if (!provider) {
|
if (!provider) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -236,10 +142,6 @@ export async function loadModelCatalog(params?: {
|
|||||||
const input = Array.isArray(entry?.input) ? entry.input : undefined;
|
const input = Array.isArray(entry?.input) ? entry.input : undefined;
|
||||||
models.push({ id, name, provider, contextWindow, reasoning, input });
|
models.push({ id, name, provider, contextWindow, reasoning, input });
|
||||||
}
|
}
|
||||||
const configuredModels = readConfiguredModelsFromConfig(cfg);
|
|
||||||
const mergedModels = mergeMissingCatalogEntries(models, configuredModels);
|
|
||||||
models.length = 0;
|
|
||||||
models.push(...mergedModels);
|
|
||||||
applyOpenAICodexSparkFallback(models);
|
applyOpenAICodexSparkFallback(models);
|
||||||
|
|
||||||
if (models.length === 0) {
|
if (models.length === 0) {
|
||||||
|
|||||||
Reference in New Issue
Block a user