From afd354c482d966a4a68d6179104bf289772057a5 Mon Sep 17 00:00:00 2001 From: Mitsuyuki Osabe <24588751+carrotRakko@users.noreply.github.com> Date: Mon, 16 Feb 2026 14:37:20 +0000 Subject: [PATCH] fix: add catalog validation to `models set` command MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `models set` accepts any syntactically valid model ID without checking the catalog, allowing typos to silently persist in config and fail at runtime. It also unconditionally adds an empty `{}` entry to `agents.defaults.models`, bypassing any provider routing constraints. This commit: - Validates the model ID against the catalog (skipped when catalog is empty during initial setup) - Warns when a new entry is added with empty config (no provider routing) Closes openclaw/openclaw#17183 ✍️ Author: Claude Code with @carrotRakko (AI-written, human-approved) --- src/commands/models/set.ts | 40 +++++++++++++++++++++++++++++++++++--- 1 file changed, 37 insertions(+), 3 deletions(-) diff --git a/src/commands/models/set.ts b/src/commands/models/set.ts index d0506acbdf6..163fdc204c5 100644 --- a/src/commands/models/set.ts +++ b/src/commands/models/set.ts @@ -1,12 +1,46 @@ import type { RuntimeEnv } from "../../runtime.js"; +import { loadModelCatalog } from "../../agents/model-catalog.js"; +import { modelKey } from "../../agents/model-selection.js"; +import { readConfigFileSnapshot } from "../../config/config.js"; import { logConfigUpdated } from "../../config/logging.js"; -import { applyDefaultModelPrimaryUpdate, updateConfig } from "./shared.js"; +import { applyDefaultModelPrimaryUpdate, resolveModelTarget, updateConfig } from "./shared.js"; export async function modelsSetCommand(modelRaw: string, runtime: RuntimeEnv) { - const updated = await updateConfig((cfg) => { - return applyDefaultModelPrimaryUpdate({ cfg, modelRaw, field: "model" }); + // 1. Read config and resolve the model reference + const snapshot = await readConfigFileSnapshot(); + if (!snapshot.valid) { + const issues = snapshot.issues.map((i) => `- ${i.path}: ${i.message}`).join("\n"); + throw new Error(`Invalid config at ${snapshot.path}\n${issues}`); + } + const cfg = snapshot.config; + const resolved = resolveModelTarget({ raw: modelRaw, cfg }); + const key = `${resolved.provider}/${resolved.model}`; + + // 2. Validate against catalog (skip when catalog is empty — initial setup) + const catalog = await loadModelCatalog({ config: cfg }); + if (catalog.length > 0) { + const catalogKeys = new Set(catalog.map((e) => modelKey(e.provider, e.id))); + if (!catalogKeys.has(key)) { + throw new Error( + `Unknown model: ${key}\nModel not found in catalog. Run "openclaw models list" to see available models.`, + ); + } + } + + // 3. Track whether this is a new entry + const isNewEntry = !cfg.agents?.defaults?.models?.[key]; + + // 4. Update config (using upstream's helper for the actual mutation) + const updated = await updateConfig((c) => { + return applyDefaultModelPrimaryUpdate({ cfg: c, modelRaw, field: "model" }); }); + // 5. Warn and log + if (isNewEntry) { + runtime.log( + `Warning: "${key}" had no entry in models config. Added with empty config (no provider routing).`, + ); + } logConfigUpdated(runtime); runtime.log(`Default model: ${updated.agents?.defaults?.model?.primary ?? modelRaw}`); }