我写了一个单文件 Python 小工具:用交互方式切换 Claude 模型配置
最近我折腾了一个很小的单文件 Python 脚本。
它不是什么大工程,也没有复杂框架,但我自己写完之后用着挺顺手,所以想把它整理成一篇偏爱好者分享的技术文章,聊聊它到底解决了什么问题、代码为什么这样写、以及这种小工具为什么值得自己动手做一个。
这个脚本只做一件事:
把 ~/.claude/settings.json 里的模型配置,变成交互式选择。
也就是说,不再手动打开配置文件、一行一行改模型名,而是直接拉取可用模型列表,用 fzf 选中后再写回本地配置。
如果你平时会在默认模型、Opus、Sonnet、Haiku 之间切换,或者想顺手决定某个模型是否带上 [1m] 后缀,这种写法会比纯手改 JSON 轻松很多。
它到底做了什么
先用一句话概括这个脚本的功能:
- 读取当前
~/.claude/settings.json - 打印现在的模型配置
- 询问你要不要修改
- 调用
ANTHROPIC_BASE_URL对应接口的/v1/models拉取模型列表 - 用
fzf让你分别选择默认模型、Opus、Sonnet、Haiku - 额外询问是否给部分模型追加
[1m]后缀 - 把新结果写回
~/.claude/settings.json
这就是它的全部职责:读取、选择、写回。
完整代码
# /// script
# requires-python = ">=3.12"
# dependencies = []
# ///
from __future__ import annotations
import json
import shutil
import subprocess
import urllib.request
from pathlib import Path
from typing import cast
SETTINGS_PATH = Path("~/.claude/settings.json").expanduser()
MODEL_KEYS = (
"ANTHROPIC_MODEL",
"ANTHROPIC_DEFAULT_OPUS_MODEL",
"ANTHROPIC_DEFAULT_SONNET_MODEL",
"ANTHROPIC_DEFAULT_HAIKU_MODEL",
)
MODEL_SELECTIONS = (
("ANTHROPIC_MODEL", "默认模型> "),
("ANTHROPIC_DEFAULT_OPUS_MODEL", "OPUS 模型> "),
("ANTHROPIC_DEFAULT_SONNET_MODEL", "SONNET 模型> "),
("ANTHROPIC_DEFAULT_HAIKU_MODEL", "HAIKU 模型> "),
)
ONE_MILLION_SELECTIONS = (
("ANTHROPIC_MODEL", "默认模型开启 100 万上下文> "),
("ANTHROPIC_DEFAULT_OPUS_MODEL", "OPUS 开启 100 万上下文> "),
("ANTHROPIC_DEFAULT_SONNET_MODEL", "SONNET 开启 100 万上下文> "),
)
ModelPayload = dict[str, list[dict[str, str]]]
Settings = dict[str, dict[str, str] | str | bool]
ModelConfig = dict[str, str]
def read_settings() -> Settings:
return json.loads(SETTINGS_PATH.read_text())
def write_settings(settings: Settings) -> None:
SETTINGS_PATH.write_text(json.dumps(settings, indent=2, ensure_ascii=False) + "\n")
def fetch_model_names(base_url: str, token: str) -> list[str]:
request = urllib.request.Request(
f"{base_url}/v1/models",
headers={
"Authorization": f"Bearer {token}",
"User-Agent": "curl/8.5.0",
},
method="GET",
)
with urllib.request.urlopen(request) as response:
payload = json.loads(response.read())
return sorted(item["id"] for item in payload["data"])
def pick_with_fzf(prompt: str, options: list[str]) -> str:
return subprocess.run(
["fzf", "--prompt", prompt, "--no-sort"],
input="\n".join(options) + "\n",
text=True,
capture_output=True,
check=True,
).stdout.strip()
def confirm_yes_no(prompt: str, *, default: bool = False) -> bool:
suffix = "[Y/n]" if default else "[y/N]"
answer = input(f"{prompt} {suffix} ").strip().lower()
if not answer:
return default
return answer in {"y", "yes"}
def append_1m_suffix(model_name: str, enabled: bool) -> str:
return f"{model_name}[1m]" if enabled else model_name
def read_model_config(env: dict[str, str]) -> ModelConfig:
return {key: env[key] for key in MODEL_KEYS}
def print_model_config(title: str, config: ModelConfig) -> None:
print(title)
for key in MODEL_KEYS:
print(f"- {key}: {config[key]}")
def select_model_config(models: list[str]) -> ModelConfig:
config = {
key: pick_with_fzf(prompt, models)
for key, prompt in MODEL_SELECTIONS
}
for key, prompt in ONE_MILLION_SELECTIONS:
enabled = confirm_yes_no(prompt, default=False)
config[key] = append_1m_suffix(config[key], enabled)
return config
def update_model_config(settings: Settings, config: ModelConfig) -> None:
cast(dict[str, str], settings["env"]).update(config)
def main() -> int:
if shutil.which("fzf") is None:
raise RuntimeError("fzf not found")
settings = read_settings()
env = cast(dict[str, str], settings["env"])
current_config = read_model_config(env)
print_model_config("当前模型配置:", current_config)
if not confirm_yes_no("是否更改模型配置?", default=False):
print("未更改模型配置。")
return 0
models = fetch_model_names(env["ANTHROPIC_BASE_URL"], env["ANTHROPIC_AUTH_TOKEN"])
updated_config = select_model_config(models)
update_model_config(settings, updated_config)
write_settings(settings)
print_model_config("已更新模型配置:", updated_config)
return 0
if __name__ == "__main__":
raise SystemExit(main())为什么我会觉得它有分享价值
这种脚本的价值,不在于代码量大,而在于它很像爱好者日常折腾里会自然长出来的东西。
很多时候,我们并不是不会改 JSON,而是:
- 手动改配置容易出错
- 改完后不容易一眼确认当前值
- 一次切 4 个模型项时,重复劳动比较明显
- 想试不同模型组合时,交互式选择明显更顺手
所以我很喜欢把这种高频但机械的动作,顺手收口成一个小工具。
它未必是一个“正式产品”,但对经常自己折腾终端、脚本和工作流的人来说,这种小东西往往最有分享意义。
先看脚本设计:它是典型的“单文件工具”写法
这个脚本最让我喜欢的一点,就是结构非常收敛。
它没有拆模块,也没有引入额外工程结构,而是把职责压缩在一个文件里:
- 常量定义
- 配置读写
- 远端模型列表获取
- 终端交互
- 最终写回
这种写法很适合爱好者自己做的小工具。因为目标不是做成通用 SDK,而是快速、清楚、可维护地解决眼前的问题。
第 1 部分:脚本头部说明了运行方式
# /// script
# requires-python = ">=3.12"
# dependencies = []
# ///这几行很短,但信息量很明确:
- 这是一个脚本化入口,不是完整包工程
- 运行环境要求 Python 3.12+
- 没有 Python 第三方依赖
不过要注意一点:
它虽然没有 Python 包依赖,但它依赖系统里有 fzf。
这一点不是通过 dependencies = [] 声明的,而是后面通过 shutil.which("fzf") 在运行时检查出来的。
第 2 部分:常量把目标键位写死,避免到处散落字符串
SETTINGS_PATH = Path("~/.claude/settings.json").expanduser()
MODEL_KEYS = (
"ANTHROPIC_MODEL",
"ANTHROPIC_DEFAULT_OPUS_MODEL",
"ANTHROPIC_DEFAULT_SONNET_MODEL",
"ANTHROPIC_DEFAULT_HAIKU_MODEL",
)这里做了两件很实用的事。
第一,直接把配置文件路径固定到 ~/.claude/settings.json。
第二,把模型相关 key 全部集中到 MODEL_KEYS 里,后续读取、打印、更新时都复用这一组常量,而不是到处重复硬编码字符串。
这类小工具特别适合这么写:
把会重复使用、而且写错代价高的字符串先收成常量。
第 3 部分:读取与写回配置,逻辑非常干净
def read_settings() -> Settings:
return json.loads(SETTINGS_PATH.read_text())
def write_settings(settings: Settings) -> None:
SETTINGS_PATH.write_text(json.dumps(settings, indent=2, ensure_ascii=False) + "\n")这两段几乎没有多余逻辑。
read_settings()
- 直接读文本
- 直接
json.loads() - 不做额外包装
write_settings()
indent=2:写回后的 JSON 更易读ensure_ascii=False:如果文件里有中文,不会被强制转义成\uXXXX- 最后补一个换行:让文件结尾更规整
这是典型的“本地配置工具”写法:
不追求抽象层次,优先保证读起来一眼就明白。
第 4 部分:模型列表不是手写,而是实时拉取
def fetch_model_names(base_url: str, token: str) -> list[str]:
request = urllib.request.Request(
f"{base_url}/v1/models",
headers={
"Authorization": f"Bearer {token}",
"User-Agent": "curl/8.5.0",
},
method="GET",
)
with urllib.request.urlopen(request) as response:
payload = json.loads(response.read())
return sorted(item["id"] for item in payload["data"])这部分是整段脚本里最关键的自动化点。
它不是让你自己维护模型列表,而是直接请求 base_url/v1/models,然后把返回的 payload["data"] 里的 id 全部取出来。
这带来两个好处:
- 减少手工维护成本:模型有增减时,不用自己再改脚本里的候选列表
- 候选项和服务端保持一致:本地看到什么,取决于接口真实返回了什么
这里还做了一步 sorted(...),所以最终给 fzf 的是按字典序排好的模型 ID 列表。
第 5 部分:fzf 负责交互选择,脚本本身不处理复杂 UI
def pick_with_fzf(prompt: str, options: list[str]) -> str:
return subprocess.run(
["fzf", "--prompt", prompt, "--no-sort"],
input="\n".join(options) + "\n",
text=True,
capture_output=True,
check=True,
).stdout.strip()这段我很喜欢,因为思路非常朴素:
- Python 负责准备候选数据
fzf负责做终端交互- Python 再把选择结果接回来
也就是说,它没有自己发明一个选择界面,而是直接借用成熟 CLI 工具。
参数里有两个点值得注意:
--prompt:让每一次选择都显示明确提示语--no-sort:告诉fzf不要自行重新排序
再结合前面的 sorted(...),整个顺序控制就很清楚:
先由 Python 决定候选顺序,再由 fzf 原样展示。
第 6 部分:是否开启 [1m],用单独确认而不是混在模型选择里
def append_1m_suffix(model_name: str, enabled: bool) -> str:
return f"{model_name}[1m]" if enabled else model_name后面在 select_model_config() 里,这个函数会配合 confirm_yes_no() 一起用:
for key, prompt in ONE_MILLION_SELECTIONS:
enabled = confirm_yes_no(prompt, default=False)
config[key] = append_1m_suffix(config[key], enabled)这里的设计我觉得挺合理。
它没有把“选模型”和“是否追加 [1m]”混成一个超长列表,而是拆成两步:
- 先选基础模型名
- 再单独确认是否加
[1m]
这样做的好处是交互负担更低,也更容易读懂。
当然,这里也有一个前提:
你的接口体系需要认可 model_name[1m] 这种命名约定。
如果你自己的服务端并不是靠这个后缀表示 100 万上下文,那么这段逻辑就要按你的实际规则调整。
第 7 部分:当前配置先打印,再决定要不要改
def print_model_config(title: str, config: ModelConfig) -> None:
print(title)
for key in MODEL_KEYS:
print(f"- {key}: {config[key]}")再看 main() 里的调用顺序:
print_model_config("当前模型配置:", current_config)
if not confirm_yes_no("是否更改模型配置?", default=False):
print("未更改模型配置。")
return 0这个顺序非常符合真实使用习惯:
- 先把当前状态展示出来
- 然后再问你要不要继续
- 如果不改,直接退出
这一步看起来小,但非常重要,因为它避免了“脚本一运行就开始改配置”的冒进感。
对本地工具来说,先展示、再确认、再落盘,通常是比“直接执行”更稳妥的交互顺序。
第 8 部分:真正写回时,只更新 env 里的目标键
def update_model_config(settings: Settings, config: ModelConfig) -> None:
cast(dict[str, str], settings["env"]).update(config)这行的重点不在 cast(),而在 update(config)。
它的意思很明确:
- 不重写整个 settings 结构
- 只更新
settings["env"]里的目标模型项 - 其他无关配置保持不动
这就是我很认同的一种“最小修改”思路。
哪怕只是个人脚本,也尽量别做无关覆盖。只碰需要改的那几个 key,风险会小很多。
第 9 部分:main() 把整个流程串起来了
如果把这段脚本抽象成流程图,其实非常简单:
- 检查
fzf是否存在 - 读取
settings.json - 取出当前
env - 打印当前模型配置
- 用户确认是否继续
- 拉取模型列表
- 逐项交互选择
- 更新内存中的配置对象
- 写回磁盘
- 打印更新后的结果
这种写法的好处是:
所有流程都在 main() 一眼看完。
你不需要跳很多文件,也不用来回追调用链,非常适合这种一次性就想看懂的小工具。
这个脚本适合什么场景
我觉得它适合下面这些场景:
- 你本来就通过
~/.claude/settings.json管理模型相关环境变量 - 你经常在多种模型之间切换
- 你不想每次都手改 JSON
- 你本机已经装了
fzf - 你当前
ANTHROPIC_BASE_URL对应接口能正常返回/v1/models
如果这些条件都满足,这个脚本的体验会很顺。
也要说清楚它的边界
分享这种脚本时,我觉得把边界讲清楚比单纯夸“好用”更重要。
这份代码按当前写法,有几个前提:
1. settings["env"] 必须存在
代码里直接写了:
env = cast(dict[str, str], settings["env"])如果你的 settings.json 没有 env 字段,这里会直接报错。
2. 目标模型 key 必须已经存在
read_model_config() 用的是:
return {key: env[key] for key in MODEL_KEYS}这意味着如果缺任何一个 key,就会触发 KeyError。
3. 远端接口返回结构必须符合预期
fetch_model_names() 默认返回 JSON 里有 data,并且每个元素都有 id。
如果你的接口返回格式不同,这段逻辑也需要跟着改。
4. [1m] 后缀是约定,不是通用标准
这不是 Python 语法问题,而是你的模型命名约定问题。
脚本只是机械地拼接字符串,不会替你判断服务端是否真的支持这个名字。
我为什么喜欢这种脚本
因为它非常像“给自己用的好工具”应有的样子:
- 单文件
- 标准库优先
- 逻辑直给
- 依赖少
- 修改范围小
- 用完就能解决问题
它不试图变成一个大而全的配置管理器,也不试图抽象出复杂框架。
它更像是一个爱好者在真实使用里,为了让自己的工作流更顺一点,慢慢磨出来的小工具。
它只是把“模型切换”这件事,做得比手改 JSON 更顺手一点。
很多时候,这种小改进反而最有实际价值。
结语
如果让我给这个脚本下一个简单定义,我会说:
它是一个面向个人工作流的交互式模型配置切换器。
代码不长,但思路很完整:
- 先读当前配置
- 再拉远端模型列表
- 用
fzf做选择 - 最后最小范围写回本地设置
它不是什么“标准答案”,也不是要替所有人设计一套通用方案。
但如果你刚好也喜欢折腾终端、喜欢把重复动作变顺手、也愿意为自己的工作流写一些小工具,那这类单文件脚本其实特别值得分享。
它不惊艳,却很实用;不大,却足够解决真实问题。对爱好者来说,这往往就是最有意思的部分。