Files
xiaoli-cli/xcli_core/model_registry.py
T
shuiyu67 1383d32f8b feat: OpenAI 多模型注册表 + /model 命令,告别手改配置文件
痛点:很多模型都是 OpenAI 格式,但换模型只能手改 config.json 的单槽位,
且只能存一个。新增模型注册表与 /model 命令族:

- 新增 xcli_core/model_registry.py:在 api.engines.openai 下维护
  models 列表(name + base_url + api_key + model)与 current 指针,全部落盘。
  向后兼容:无 models 时把扁平 api_key/base_url/model 当作单条隐式模型。
- OpenaiAI 接入:__init__ 应用注册表 current 模型;新增
  set_registry_model / list_registry_models / add_model / remove_model /
  current_model_name。注意保留引擎原有的轻量 set_model(只改 model 名)
  与 list_models(查 API /models 端点),新方法与它们不重名、互不干扰。
- 新增 /model 命令族(仅 openai 引擎):
    /model            列出已配置模型(* 标记当前)
    /model <名称>     切换当前模型并持久化
    /model add        交互式录入(名称/base_url/api_key/model),回车即存并切换
    /model rm <名称>  删除(删当前则回退到列表第一个)
- 启动横幅引擎名后显示当前模型;紧凑提示与 /help 速查表均补 /model 入口。
- 测试:新增 tests/test_model_registry.py(13 项,覆盖兼容/增删/切换/落盘/
  引擎接入/CLI 路由,全程临时配置不污染真实 config.json)。
  全量 230 passed(原 217 passed)。

交互实测:/model add → 已添加并切换到 gpt4o;/model gpt4o → 已切换;
/model rm gpt4o → 回退到 xophunyuan7bmt,配置结构与内容完好。
2026-08-05 16:12:35 +08:00

146 lines
4.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""OpenAI 格式模型注册表。
把「多个 OpenAI 格式模型」从 config.json 的单槽位升级为可管理的列表,
支持在线增删、切换、持久化,免去每次手改配置文件。
配置结构(api.engines.openai):
{
"models": [
{"name": "讯飞xop", "base_url": "...", "api_key": "...", "model": "xophunyuan7bmt"},
{"name": "gpt4o", "base_url": "https://api.openai.com/v1", "api_key": "...", "model": "gpt-4o"}
],
"current": "讯飞xop"
}
向后兼容:若没有 models 列表,把扁平的 api_key/base_url/model 当作单条隐式模型,
其 name 取 model 值(空则 "default")。
"""
from xcli_core.config import load_config, save_config
# 测试可临时覆盖配置路径
_CONFIG_PATH = None
def _set_config_path(path):
"""仅供测试:指向临时 config.json"""
global _CONFIG_PATH
_CONFIG_PATH = path
def _load():
if _CONFIG_PATH:
try:
with open(_CONFIG_PATH, 'r', encoding='utf-8') as f:
return __import__('json').load(f)
except (FileNotFoundError, __import__('json').JSONDecodeError):
return {"api": {"engines": {}}, "system": {}}
return load_config()
def _persist(cfg):
if _CONFIG_PATH:
with open(_CONFIG_PATH, 'w', encoding='utf-8') as f:
__import__('json').dump(cfg, f, ensure_ascii=False, indent=2)
return
save_config(cfg)
def _openai_block(cfg):
return cfg.setdefault("api", {}).setdefault("engines", {}).setdefault("openai", {})
def list_models(cfg=None):
"""返回 (models:list[dict], current:str|None)。
无 models 列表时返回单条隐式模型(由扁平 api_key/base_url/model 构成)。
"""
cfg = cfg or _load()
block = _openai_block(cfg)
models = block.get("models")
if not models:
m = block.get("model") or "default"
implicit = {
"name": m,
"base_url": block.get("base_url", ""),
"api_key": block.get("api_key", ""),
"model": block.get("model", ""),
}
return [implicit], (block.get("current") or m)
current = block.get("current") or (models[0]["name"] if models else None)
return models, current
def get_model(name=None, cfg=None):
"""取某模型配置;name=None 取 current。找不到返回 None。"""
models, current = list_models(cfg)
target = name or current
if not target:
return None
for m in models:
if m.get("name") == target:
return dict(m)
return None
def set_current(name):
"""把 current 指针指向 name 并落盘。name 必须存在,否则返回 False。"""
cfg = _load()
block = _openai_block(cfg)
models = block.get("models") or []
if not any(m.get("name") == name for m in models):
return False
block["current"] = name
_persist(cfg)
return True
def add_model(entry):
"""新增或更新模型,落盘并返回其 name。
entry: {"name", "base_url", "api_key", "model"}。同名则更新。
首次添加会从扁平配置迁移出第一条隐式模型,且保持 current 不跳变。
"""
name = (entry.get("name") or "").strip()
if not name:
raise ValueError("模型 name 不能为空")
cfg = _load()
block = _openai_block(cfg)
models = block.get("models")
if not models:
old_name = block.get("model") or "default"
models = [{
"name": old_name,
"base_url": block.get("base_url", ""),
"api_key": block.get("api_key", ""),
"model": block.get("model", ""),
}]
block["models"] = models
if not block.get("current"):
block["current"] = old_name
for m in models:
if m.get("name") == name:
m.update(entry)
_persist(cfg)
return name
models.append(dict(entry))
_persist(cfg)
return name
def remove_model(name):
"""删除模型,返回是否成功(找不到返回 False)。
若删除的是 current,则 current 回退到列表第一个。
"""
cfg = _load()
block = _openai_block(cfg)
models = block.get("models") or []
new = [m for m in models if m.get("name") != name]
if len(new) == len(models):
return False
block["models"] = new
if block.get("current") == name:
block["current"] = new[0]["name"] if new else None
_persist(cfg)
return True