Skip to content

Commit 327d48a

Browse files
committed
test(models): 同步模型 id 断言到归一后的对外 id
naming 重构(provider/naming.py,上一提交已入库)把对外 id 统一为归一键, TRAE 清单里的 `DeepSeek-V4-Flash` 现在对外是 `deepseek-v4-flash`。该文件 的期望值漏改,CI backend 因此红。改动本身来自工作区里与 naming 重构配套的 同一批编辑(上一提交按文件排除,本次补上以免 main 挂红)。
1 parent 279a679 commit 327d48a

1 file changed

Lines changed: 89 additions & 26 deletions

File tree

‎tests/test_m1b_codebuddy.py‎

Lines changed: 89 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1023,21 +1023,21 @@ def import_credential(self, raw):
10231023
with TestClient(app) as client:
10241024
# 第一次:拉取成功,缓存写入
10251025
first = client.get("/v1/models", headers=auth).json()["data"]
1026-
assert {item["id"] for item in first} == {"glm-5.2", "DeepSeek-V4-Flash"}
1026+
assert {item["id"] for item in first} == {"glm-5.2", "deepseek-v4-flash"}
10271027
cached_keys = app.state.services.model_list_cache["trae"].keys()
10281028
assert cached_keys == {"glm-5.2", "deepseek-v4-flash"}
10291029

10301030
# 第二次:拉取失败 → 用缓存兜底,列表不缺模型
10311031
trae.fail = True
10321032
second = client.get("/v1/models", headers=auth).json()["data"]
1033-
assert {item["id"] for item in second} == {"glm-5.2", "DeepSeek-V4-Flash"}
1033+
assert {item["id"] for item in second} == {"glm-5.2", "deepseek-v4-flash"}
10341034
assert {item["id"] for item in second} and all(
10351035
item["providers"] == ["trae"] for item in second)
10361036

10371037
# 恢复后重新拉取成功,缓存刷新
10381038
trae.fail = False
10391039
third = client.get("/v1/models", headers=auth).json()["data"]
1040-
assert {item["id"] for item in third} == {"glm-5.2", "DeepSeek-V4-Flash"}
1040+
assert {item["id"] for item in third} == {"glm-5.2", "deepseek-v4-flash"}
10411041

10421042

10431043
def test_models_blocklist_filters_noise_and_old(tmp_path):
@@ -1088,6 +1088,51 @@ def import_credential(self, raw):
10881088
assert "kimi-k2.6" not in ids2 and "glm-5.2" in ids2
10891089

10901090

1091+
def test_models_blocklist_matches_normalized_id_and_display_name(tmp_path):
1092+
"""黑名单按**归一后的对外写法**匹配:归一键、展示名都能命中。
1093+
1094+
对外 id 改为归一键后,用户照列表里看到的名字写黑名单必须生效——
1095+
按 zen 归一键 `longcat-2.5-preview`、展示名 `MiMo V2.5` 都能滤掉;
1096+
Qoder 的原代号 `kmodel_latest` 归一键是展示名派生的 `kimi-k3`,按
1097+
`kimi-k3` 也能滤。同时老口径(原代号 `custom_model_*`)仍生效。
1098+
"""
1099+
settings = Settings(_env_file=None, APP_SECRET=SECRET, DATA_DIR=str(tmp_path),
1100+
MODEL_BLOCKLIST="longcat-2.5-preview,MiMo V2.5,kimi-k3,"
1101+
"custom_model_*")
1102+
1103+
class Stub:
1104+
def __init__(self, pid: str, models):
1105+
self.id = pid
1106+
self._models = models
1107+
1108+
async def list_models(self, _data):
1109+
return list(self._models)
1110+
1111+
def import_credential(self, raw): # pragma: no cover - 未使用
1112+
return raw
1113+
1114+
from src.provider.base import Model
1115+
1116+
app = build_app(settings, providers={
1117+
"zen": Stub("zen", [Model(id="longcat-2.5-preview-free",
1118+
name="LongCat 2.5 Preview"),
1119+
Model(id="mimo-v2.5-free", name="MiMo V2.5")]),
1120+
"qoder": Stub("qoder", [Model(id="kmodel_latest", name="Kimi-K3"),
1121+
Model(id="custom_model_claude")]),
1122+
})
1123+
for provider_id in ("zen", "qoder"):
1124+
app.state.credentials.add(provider=provider_id, credential_data={"t": "x"})
1125+
key = app.state.api_keys.create("root")["api_key"]
1126+
with TestClient(app) as client:
1127+
ids = {item["id"] for item in client.get(
1128+
"/v1/models", headers={"Authorization": f"Bearer {key}"}).json()["data"]}
1129+
# 归一键 / 展示名 / 原代号三种写法命中即滤
1130+
assert "longcat-2.5-preview" not in ids
1131+
assert "mimo-v2.5" not in ids
1132+
assert "kimi-k3" not in ids
1133+
assert ids == set()
1134+
1135+
10911136
def test_models_blocklist_hot_reload_applies_without_waiting_for_ttl(tmp_path):
10921137
"""MODEL_BLOCKLIST 热更后下一次 /v1/models 立即过滤,不受 TTL 缓存影响。
10931138
@@ -1306,20 +1351,21 @@ def import_credential(self, raw): # pragma: no cover - 未使用
13061351
with TestClient(app) as client:
13071352
ids = {m["id"] for m in client.get(
13081353
"/v1/models", headers={"Authorization": f"Bearer {key}"}).json()["data"]}
1309-
# zen 单渠道:对外 id 仍是原 id,展示名是清洗后的可读名
1310-
assert ids == {"longcat-2.5-preview-free", "mimo-v2.5-free",
1354+
# 对外 id 一律是归一键:zen 的 free 尾缀被去掉;CB 重名条目因退回原代号
1355+
# (展示名不唯一)也保持原 id。
1356+
assert ids == {"longcat-2.5-preview", "mimo-v2.5",
13111357
"hy4-preview", "hy4-preview-x"}
13121358
# 重名未按名入键:不得留下指向其中一个的歧义名字别名。两个原 id 各自
13131359
# 映射回自己是正确的(用户按原 id 直连仍要能定位到本渠道)。
13141360
assert app.state.services.model_aliases["codebuddy"] == {
13151361
"hy4-preview": "hy4-preview", "hy4-preview-x": "hy4-preview-x"}
13161362

13171363

1318-
def test_models_name_merge_single_channel_keeps_raw_id(tmp_path):
1319-
"""单渠道条目对外仍用上游原 id(Qoder `kmodel_latest`);展示名也能直连。
1364+
def test_models_single_channel_outward_id_is_normalized(tmp_path):
1365+
"""单渠道条目的对外 id 也是归一键(Qoder `kmodel_latest` → `kimi-k3`)。
13201366
1321-
只有多条渠道真正并到一起时才改用归一键——单渠道保持原 id,避免无谓
1322-
改名;展示名同样登记为别名,用户按 `Kimi K3` / `kimi-k3` 也能落到该渠道。
1367+
原代号只留在别名表与 `by_provider.raw_id`,转发时换回;用户按原代号直连
1368+
仍能命中。
13231369
"""
13241370
settings = Settings(_env_file=None, APP_SECRET=SECRET, DATA_DIR=str(tmp_path))
13251371

@@ -1344,12 +1390,12 @@ def import_credential(self, raw): # pragma: no cover - 未使用
13441390
with TestClient(app) as client:
13451391
data = client.get("/v1/models", headers={
13461392
"Authorization": f"Bearer {key}"}).json()["data"]
1347-
assert [m["id"] for m in data] == ["kmodel_latest"]
1348-
assert data[0]["name"] == "Kimi K3" # 展示名统一清洗
1393+
assert [m["id"] for m in data] == ["kimi-k3"] # 对外 id 用归一键
1394+
assert data[0]["name"] == "Kimi K3" # 展示名统一清洗
13491395
aliases = app.state.services.model_aliases
1350-
assert aliases["qoder"]["kmodel_latest"] == "kmodel_latest"
1351-
assert aliases["qoder"]["kimi-k3"] == "kmodel_latest" # 归一后的名字键
1352-
assert aliases["qoder"]["kimi k3"] == "kmodel_latest" # 原样的展示名也能直连
1396+
assert aliases["qoder"]["kimi-k3"] == "kmodel_latest" # 对外 id → 原代号
1397+
assert aliases["qoder"]["kmodel_latest"] == "kmodel_latest" # 原代号也能直连
1398+
assert aliases["qoder"]["kimi k3"] == "kmodel_latest" # 原样的展示名也能直连
13531399

13541400

13551401
def test_models_name_merge_cross_channel_via_normalized_key(tmp_path):
@@ -1379,7 +1425,7 @@ def import_credential(self, raw): # pragma: no cover - 未使用
13791425
# 归一到同一键(`longcat-2.5-preview`)→ 并成一条,对外 id 用该键
13801426
"zen": Stub("zen", [Model(id="longcat-2.5-preview-free", name="")]),
13811427
"trae": Stub("trae", [Model(id="longcat-2.5-preview", name="")]),
1382-
# kilo 的 `kilo-auto/free` 归一到 `kilo-auto`,单渠道仍用原 id 对外
1428+
# kilo 的 `kilo-auto/free` 归一到 `kilo-auto`,单渠道对外 id 用归一键
13831429
"kilo": Stub("kilo", [Model(id="kilo-auto/free", name="")]),
13841430
})
13851431
for provider_id in ("zen", "trae", "kilo"):
@@ -1389,7 +1435,7 @@ def import_credential(self, raw): # pragma: no cover - 未使用
13891435
data = {m["id"]: m for m in client.get(
13901436
"/v1/models", headers={"Authorization": f"Bearer {key}"}).json()["data"]}
13911437

1392-
assert set(data) == {"longcat-2.5-preview", "kilo-auto/free"}
1438+
assert set(data) == {"longcat-2.5-preview", "kilo-auto"}
13931439
merged = data["longcat-2.5-preview"]
13941440
assert merged["providers"] == ["trae", "zen"]
13951441
assert merged["name"] == "LongCat 2.5 Preview"
@@ -1400,7 +1446,7 @@ def import_credential(self, raw): # pragma: no cover - 未使用
14001446
aliases = app.state.services.model_aliases
14011447
assert aliases["zen"]["longcat-2.5-preview"] == "longcat-2.5-preview-free"
14021448
assert aliases["zen"]["longcat-2.5-preview-free"] == "longcat-2.5-preview-free"
1403-
# kilo 单渠道:对外 id 是原 id,但归一后的键也能直连到同一渠道
1449+
# kilo 单渠道:对外 id 是归一键,原代号仍可直连
14041450
assert aliases["kilo"]["kilo-auto"] == "kilo-auto/free"
14051451
assert aliases["kilo"]["kilo-auto/free"] == "kilo-auto/free"
14061452

@@ -1437,7 +1483,7 @@ def import_credential(self, raw): # pragma: no cover - 未使用
14371483
with TestClient(app) as client:
14381484
data = [m for m in client.get(
14391485
"/v1/models", headers={"Authorization": f"Bearer {key}"}).json()["data"]]
1440-
assert {m["id"] for m in data} == {"kilo-auto/free", "auto"}
1486+
assert {m["id"] for m in data} == {"kilo-auto", "auto"}
14411487
assert all(len(m["providers"]) == 1 for m in data) # 各渠道单列
14421488

14431489

@@ -1493,24 +1539,41 @@ def import_credential(self, raw): # pragma: no cover - 未使用
14931539
assert aliases["zen"]["glm-5"] == "glm-5"
14941540

14951541

1496-
def test_models_outward_id_triple_collision_falls_back_to_group_key():
1497-
"""三方撞车、全部候选被占 → 用归一键兜底(`_disambiguate` 的 `for...else`)。
1542+
def test_models_outward_id_collision_falls_back_to_channel_suffix():
1543+
"""对外 id 撞车且候选被占时,依次退回「原代号归一键 → 加渠道名后缀」。
14981544
1499-
entry 的对外 id、主渠道原 id、归一形式全被先定稿的条目占掉时,退回
1500-
构造唯一的归一键,保证对外 id 有值。真实构造里归一键按组唯一、不会与
1501-
已有 id 重复,这里只验证「走到兜底分支」。
1545+
entry 的对外 id 与主渠道原代号归一键都被先定稿的条目占掉时,退回加渠道名
1546+
后缀的候选,保证对外 id 唯一。
15021547
"""
15031548
from src.api.models import _disambiguate
15041549

15051550
entries = [
15061551
{"id": "x", "key": "kx", "providers": ["p1"], "raw_ids": {"p1": "x"}},
15071552
{"id": "y", "key": "ky", "providers": ["p2"], "raw_ids": {"p2": "y"}},
15081553
{"id": "k4", "key": "k4", "providers": ["p3"], "raw_ids": {"p3": "k4"}},
1509-
# id/原 id/归一形式全被占(x、y、k4):只能走 else 兜底
1510-
{"id": "x", "key": "k4", "providers": ["p1"], "raw_ids": {"p1": "y"}},
1554+
# id(x)与主渠道原代号归一(y)都被占:退回 `y-p1`
1555+
{"id": "x", "key": "k9", "providers": ["p1"], "raw_ids": {"p1": "y"}},
1556+
]
1557+
_disambiguate(entries)
1558+
assert [e["id"] for e in entries] == ["x", "y", "k4", "y-p1"]
1559+
1560+
1561+
def test_models_outward_id_collision_falls_back_to_numeric_suffix():
1562+
"""候选全被占时追加数字后缀(`_unique_suffix` 兜底),保证对外 id 唯一。"""
1563+
from src.api.models import _disambiguate
1564+
1565+
entries = [
1566+
{"id": "x", "key": "kx", "providers": ["p1"], "raw_ids": {"p1": "x"}},
1567+
{"id": "y", "key": "ky", "providers": ["p2"], "raw_ids": {"p2": "y"}},
1568+
{"id": "y-p1", "key": "kz", "providers": ["p3"],
1569+
"raw_ids": {"p3": "y-p1"}},
1570+
{"id": "x-2", "key": "kw", "providers": ["p4"],
1571+
"raw_ids": {"p4": "x-2"}},
1572+
# 候选 x / y / y-p1 与后缀 x-2 全被占 → 用 x-3 兜底
1573+
{"id": "x", "key": "k9", "providers": ["p1"], "raw_ids": {"p1": "y"}},
15111574
]
15121575
_disambiguate(entries)
1513-
assert [e["id"] for e in entries] == ["x", "y", "k4", "k4"]
1576+
assert [e["id"] for e in entries] == ["x", "y", "y-p1", "x-2", "x-3"]
15141577

15151578

15161579
def test_models_list_orders_cb_then_trae_first(tmp_path):

0 commit comments

Comments
 (0)