痛点:很多模型都是 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,配置结构与内容完好。
146 lines
4.4 KiB
Python
146 lines
4.4 KiB
Python
"""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
|