openclaw-model-switch
Switch or repair the model configuration of an OpenClaw instance (e.g. Kimi k2p6 → k3): change the default model, add model definitions, and fix model-config failures — 401 "Invalid token", "No available channel / model not found", thinking-level rejections ("Thinking level X is not supported"), and config edits that don't take effect. Use whenever the user wants to switch/upgrade/rollback the OpenClaw model (切换模型/换模型/ 升级模型), or says the OpenClaw/龙虾 bot's model is misconfigured (模型配的错了), or the bot falls back / errors on LLM calls.
1How It Works & Mechanism
Consists of only a single 240-line script that operates strictly via "read openclaw.json → modify JSON → overwrite original file", with no daemon or API wrapper.
def save_config(path: Path, config: dict): with open(path, "w", encoding="utf-8") as f: json.dump(config, f, indent=2, ensure_ascii=False) f.write("\n")
Configuration discovery follows "short-circuit via explicit --config; otherwise, mark all existing files among the three candidates as edit targets", treating them as mirrors that must be modified together.
CONFIG_CANDIDATES = [ Path.home() / ".openclaw" / "openclaw.json", Path.home() / ".kimi" / "kimi-claw" / "openclaw.json", Path.home() / ".kimi_openclaw" / "openclaw.json", ]
The "mirror synchronization" is implemented by executing the identical mutation logic across each configuration file independently, rather than copying the primary configuration to the rest.
print(f"Found {len(configs)} config files; will edit and sync all:")
Before any file is modified, a backup is created at <config_dir>/config-backups/openclaw-<local_timestamp>.json, with no retention cap or pruning logic.
timestamp = datetime.now().strftime("%Y%m%d-%H%M%S") backup_path = backup_dir / f"openclaw-{timestamp}.json" shutil.copy2(path, backup_path)
Automatically injects a built-in definition when a model definition is missing: KNOWN_MODELS hardcodes the id, contextWindow, maxTokens, and input modalities for k3, k2p6, and kimi-k2.7-code, inheriting provider-level headers into them.
"k3": { "id": "k3", "name": "k3", "reasoning": True, "input": ["text", "image"], "contextWindow": 1048576, "maxTokens": 32768, },
Sets the default model reference as agents.defaults.model.primary (direct subscript assignment without shape validation) while rewriting the agents.defaults.models alias mapping.
config["agents"]["defaults"]["model"]["primary"] = f"{provider_name}/{model_id}"
When updating the default model, it purges all keys in the alias mapping prefixed with "current provider/" and writes back only an empty object for the new model.
stale_keys = [k for k in models_map if k.startswith(f"{provider_name}/")] for k in stale_keys: del models_map[k] models_map[f"{provider_name}/{model_id}"] = {}
Maintains metadata timestamps after rewriting: writes the UTC ISO time to meta.lastTouchedAt; preserves meta.lastTouchedVersion if present, or sets it to "unknown" if missing.
config.setdefault("meta", {})["lastTouchedAt"] = datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
Only a single restart path exists: `openclaw gateway restart`; failure merely prints a warning directing the user to restart manually, with no systemd fallback.
["openclaw", "gateway", "restart"],
Beyond the script, the true "depth of the skill" lies in its documentation-layer diagnostic protocol: first execute an actual completion probe against <baseUrl>/v1/messages using curl, and then run a round of validation with openclaw agent after modification.
curl -sS -o /tmp/probe.json -w "HTTP %{http_code}\n" \ -X POST "<baseUrl>/v1/messages" \ -H "Authorization: Bearer <apiKey>" \ -H "Content-Type: application/json" \ -H "anthropic-version: 2023-06-01" \
2Core Capabilities
3External Dependencies
| Kind | Dependency / Endpoint |
|---|---|
| cli | openclaw CLI(gateway restart;脚本实际调用) |
| cli | openclaw gateway status(读 Config (service) 判断网关真正读哪份配置) |
| cli | openclaw agent --local --json(Step 4 端到端验证,看 result/fallbackUsed) |
| cli | curl(Step 2 端点探针;由 agent 按文档执行,非脚本调用) |
| cli | python3 -m json.tool(手工编辑后的 JSON 校验建议) |
| api | Anthropic Messages 兼容接口(探针目标:POST <baseUrl>/v1/messages,Bearer + anthropic-version 头) |
| network | Kimi 官方端点(references 里给出的 provider baseUrl 示例) |
| network | 本地 echo 抓包服务器(trap 目录附录的 wire-capture 配方,读真实 x-api-key) |
| package | Python 3 标准库 only(json/os/shutil/subprocess/argparse/pathlib/datetime;无第三方依赖) |
4Recommended Governance & Evidence
- scripts/: Executes local script or command: python3 scripts/switch-model.py <model-id> [--provider NAME] [--config PATH
- 网络端点: Declares external network access: Script itself: zero network calls (no urllib/requests/socket/httpx, no URL constants)
- 本地工作目录: File I/O and artifact output: <each discovered openclaw.json> (overwritten in-place, indent=2 + ensure_ascii=False + trailing newline)
5Second-Pass Verification
- [discrepancy] 文档称脚本 'syncs mirror files'(SKILL.md Step 3)是否等于内容同步 — 代码只对每个发现的文件各自施加同一处修改(循环里 load→backup→改 provider/默认模型→save),从不把主配置内容复制到其它文件;因此若两份 openclaw.json 原本就有差异,改完仍然有差异,只是「默认模型」这一处一致了。SKILL.md 的 'syncs mirror files' 与脚本实际的 'edit all of them identically' 不是一回事,属措辞夸大(脚本自己的打印也是 'will edit and sync all')。
- [discrepancy] README 声称 openclaw-model-switch 有 'Model validation against provider's model list' — 脚本里没有校验门禁:模型不在 provider 的 models 里时只做 model_exists → 若内置 KNOWN_MODELS 也没有该 id,仅打印 Warning('Model X is not defined in config and not in built-in known models.')然后照常写入并返回 0。SKILL.md 自身并未这样宣称(它靠 Step 2 探针 + Step 4 验证兜底),所以是 README 层面的描述与代码能力不符。
- [ok] 「Step 2 端点探针 / Step 4 端到端验证」是否由 skill 代码实现 — 复核确认这不是代码能力而是文档纪律:grep 全脚本无任何 HTTP 客户端与 curl 调用,Step 2/Step 4 都是给 agent 的操作指令(curl 与 openclaw agent 两条命令)。本报告已在 how_it_works 末条、capabilities 与 external_deps 中统一标注为「文档指示、非脚本执行」,避免把文档承诺记成代码能力。
- [ok] 脚本是否发网络请求 / 读 env 里的 API key(用户指定的两个重点) — 反向扫描 scripts/ 零命中:无 urllib/requests/socket/httpx/curl/wget;无 os.environ/getenv/apiKey 读取。附带发现 import os 在该文件中完全未被使用(无 os.* 调用),说明连标准库层面的环境访问都没接上——凭证交接 100% 发生在文档层(curl 头)而非代码层。
- [ok] 备份是否真的先于写入、且每份被改文件都有备份 — 循环体内顺序为 load_config → 解析 provider → backup_config(打印 '[path] backed up to: …')→ add_model_definition → update_default_model → 写 meta → save_config,备份严格前置于任何 save_config;显式 --config 时不走 discover_configs 的多文件分支,只备份那一个文件,与文档一致。
- [ok] 别名清理的破坏范围(第一遍记录「删除 stale keys」,第二遍量化影响) — 确认删除条件是 k.startswith(f"{provider_name}/"),粒度是整个 provider 前缀而非「旧模型」——同一 provider 下所有其它模型的别名映射条目都会被 del,随后只补回新模型的一条空对象(models_map[f"{provider_name}/{model_id}"] = {});若属性顺序上是先删后写,用户其它模型的显示名别名确实会永久丢失(备份可回滚)。已升级为 verdict.risks 首位。
- [ok] 多文件模式下的失败原子性 — provider 缺失时在循环体内直接 sys.exit(1),若第一个候选文件已成功写入而第二个文件里没有该 provider,进程会在中途退出:出现「有的文件改了、有的没改」的半改状态,且没有回滚逻辑(文档也未提示需人工核对全部镜像)。
- [ok] SKILL.md 的故障知识是否能在 references 里落地(声明 vs 资产) — SKILL.md 表格里的 5 类症状与 references/troubleshooting-model-config.md 的 5 条 trap 一一对应(含 env KIMI_API_KEY 劫持、插件 binary thinking、canonicalModelId 解等级、relay 分组 503、改错文件),kimi-models.md 也确实给出 k3/k2p6/kimi-k2.7-code 的规格与配置片段;文档承诺的知识资产存在且自洽,无空头引用。
6Verdict & Scenarios
c53908d204b24a25…