Skip to content

Commit 94cc962

Browse files
committed
fix(naming): CB 的 Xiaomi MiMo-V2.6-Pro 归一到 mimo-v2.6-pro
CB 上游可读名带空格式厂商前缀(`Xiaomi MiMo-V2.6-Pro`),`_strip_noise` 只削冒号式 `Vendor: Model`,削不到它——归一键成 `xiaomi-mimo-v2.6-pro`, 与 TRAE 的 `mimo-v2.6-pro` 对不上,同一模型被拆成两条、对外多出 `xiaomi-mimo-v2.6-pro`。 在 `MODEL_SYNONYMS` 登记这条人工核实的同模型异名,键与展示名一起收敛到 `mimo-v2.6-pro` / `MiMo V2.6 Pro`,合并在 `_merge_key` 层自然发生。 顺带把 TECHNICAL.md 的异名表说明补上「空格式厂商前缀」这一类。
1 parent 96a590e commit 94cc962

4 files changed

Lines changed: 64 additions & 1 deletion

File tree

‎TECHNICAL.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -469,7 +469,7 @@ PRAGMA foreign_keys = ON; -- 仅 api_keys 之外无外键(users 与 api_k
469469
| 归一键(合并键 / 多渠道对外 id) | `normalize_model_key` | `kilo-auto`、`longcat-2.5-preview`、`nemotron-3-ultra` |
470470
| 展示名 `name` | `display_model_name` | `LongCat 2.5 Preview`、`Qwen3.8 Max` |
471471

472-
`normalize_model_key` 剥掉两类上游噪声再 slug 化(空格/下划线/冒号/括号 → 连字符、小写)。**免费标记**是可用性命名约定、不是模型名的一部分,四种形态:尾缀 `-free`/`_free`(Zen)、路径段 `.../free`(Kilo)、冒号尾缀 `:free`、展示名括号词 `(free)`;同性质还有新版标记 `new`。**命名空间前缀**不是模型身份:路径式 `厂商/模型` 取**最后一段**,冒号式 `厂商: 模型`(要求冒号后有空白)削掉前缀。只删「整段/整词」标记(`freeplay`/`newson` 内嵌形式不动)。点号**不是**分隔(`glm-5.2` 压成 `glm-5-2` 就换了模型);非 ASCII 字母数字保留。剥完只剩噪声时回退原串小写,调用方据此回退展示原 id,不产出空键。`display_model_name` 清洗上游可读名(`Qwen3.8-Max` → `Qwen3.8 Max`、`NVIDIA: Nemotron 3 Ultra (free)` → `Nemotron 3 Ultra`)或由 id 派生;品牌与缩写按表纠正(机械 title-case 会把 `DeepSeek`/`MiMo`/`GLM`/`GPT` 写错)。**幂等**:清洗结果再洗不变。**合并键 = 归一键,八渠道统一**:同一模型各渠道内部代号互不相同(Qoder `kmodel_latest` = TRAE `kimi-k3-1` = CodeBuddy `kimi-k3`),纯 id 规则无法对齐,只能靠展示名——归一键即展示名再 slug 化,两路径永远同源。zen / kilo 也参与合并:两条免费渠道各用不同命名约定与**不同 id 规格**(Zen `nemotron-3-ultra-free` vs Kilo `nvidia/nemotron-3-ultra-550b-a55b:free`),id 对不上,但展示名都清洗成「Nemotron 3 Ultra」,走展示名能并成一条。**同模型异名显式规范**(`MODEL_SYNONYMS`):清洗规则对齐不了「上游给同一模型起了不带版本号的名字」(Qoder `dfmodel` 上游名只叫 `DeepSeek-Flash`,别家叫 `DeepSeek-V4.1-Flash`)。表把「异名归一键 → (规范归一键, 规范展示名)」,键与展示名一起收敛。**只登记人工核实的同模型异名,不做模糊匹配**。**哨兵名不跨渠道合并**(`_SENTINEL_LABELS` = `auto` / `default`):这些是各上游自己的「自动路由 / 默认模型」占位,语义只在本渠道内成立(Kilo 的 `kilo-auto/free` 与 Qoder 的 `auto` 都归一成 `auto` 却是完全不同的东西),故退回原 id 的归一键、各自单列。**同渠道内归一键重复 → 退回原 id 的归一键**;该渠道也不登记歧义的名字别名。
472+
`normalize_model_key` 剥掉两类上游噪声再 slug 化(空格/下划线/冒号/括号 → 连字符、小写)。**免费标记**是可用性命名约定、不是模型名的一部分,四种形态:尾缀 `-free`/`_free`(Zen)、路径段 `.../free`(Kilo)、冒号尾缀 `:free`、展示名括号词 `(free)`;同性质还有新版标记 `new`。**命名空间前缀**不是模型身份:路径式 `厂商/模型` 取**最后一段**,冒号式 `厂商: 模型`(要求冒号后有空白)削掉前缀。只删「整段/整词」标记(`freeplay`/`newson` 内嵌形式不动)。点号**不是**分隔(`glm-5.2` 压成 `glm-5-2` 就换了模型);非 ASCII 字母数字保留。剥完只剩噪声时回退原串小写,调用方据此回退展示原 id,不产出空键。`display_model_name` 清洗上游可读名(`Qwen3.8-Max` → `Qwen3.8 Max`、`NVIDIA: Nemotron 3 Ultra (free)` → `Nemotron 3 Ultra`)或由 id 派生;品牌与缩写按表纠正(机械 title-case 会把 `DeepSeek`/`MiMo`/`GLM`/`GPT` 写错)。**幂等**:清洗结果再洗不变。**合并键 = 归一键,八渠道统一**:同一模型各渠道内部代号互不相同(Qoder `kmodel_latest` = TRAE `kimi-k3-1` = CodeBuddy `kimi-k3`),纯 id 规则无法对齐,只能靠展示名——归一键即展示名再 slug 化,两路径永远同源。zen / kilo 也参与合并:两条免费渠道各用不同命名约定与**不同 id 规格**(Zen `nemotron-3-ultra-free` vs Kilo `nvidia/nemotron-3-ultra-550b-a55b:free`),id 对不上,但展示名都清洗成「Nemotron 3 Ultra」,走展示名能并成一条。**同模型异名显式规范**(`MODEL_SYNONYMS`):清洗规则对齐不了「上游给同一模型起了不带版本号的名字」(Qoder `dfmodel` 上游名只叫 `DeepSeek-Flash`,别家叫 `DeepSeek-V4.1-Flash`)。表把「异名归一键 → (规范归一键, 规范展示名)」,键与展示名一起收敛。另一类是**空格式厂商前缀**(CB 的 `Xiaomi MiMo-V2.6-Pro` vs TRAE 的 `MiMo-V2.6-Pro`,冒号式规则削不到,归一键会多出 `xiaomi-`)→ 登记 `xiaomi-mimo-v2.6-pro` → `mimo-v2.6-pro`。**只登记人工核实的同模型异名,不做模糊匹配**。**哨兵名不跨渠道合并**(`_SENTINEL_LABELS` = `auto` / `default`):这些是各上游自己的「自动路由 / 默认模型」占位,语义只在本渠道内成立(Kilo 的 `kilo-auto/free` 与 Qoder 的 `auto` 都归一成 `auto` 却是完全不同的东西),故退回原 id 的归一键、各自单列。**同渠道内归一键重复 → 退回原 id 的归一键**;该渠道也不登记歧义的名字别名。
473473

474474
**对外 id 一律是归一键**(`_finalize` / `_disambiguate` / `_register_aliases`):多渠道取最短展示名的归一键;单渠道取该条展示名的归一键;兜底/哨兵条目取**原代号的归一键**。**原代号不在对外 id 里露面**,但转发时一律经 `services.model_aliases` 映射回该渠道自己登记的原代号(由 `executor.upstream_model_name` 消费);每渠道登记三条键——对外 id、原代号、展示名(原样与 slug 形式),用户按任一种写法都能直连命中。**对外 id 撞车的消歧**(`_disambiguate`):按 `_sort_key` 序保留先到者,后者按 `_fallback_ids` 依次取(原代号归一键 → 加渠道名后缀 → 数字后缀)。**兜底/哨兵条目用渠道限定键隔离**(`_merge_key` 的 `_scoped_key` = `渠道\0归一原代号`),但对外 id 仍是原代号的归一键。**展示顺序(CB / TR 优先)**:合并后按 `_sort_key(entry)` 排序——渠道权重 `_PROVIDER_RANK`(codebuddy 0 → codebuddy_intl 1 → trae 2 → qoder 3 → codearts 4 → officeace 5 → 其余 6),多渠道条目取 `min` 权重,同级再按**归一键**字典序(`_sort_key` 在 `_finalize` 之前跑,此时对外 id 未算出,而 `_disambiguate` 需要确定顺序决定撞 id 时谁保留)。仅影响展示顺序,调度选号(`model_resolver` 的 `KNOWN_PROVIDERS` 顺序)不变。
475475
### 9.2 按凭证加载与 SWR

‎src/provider/naming.py‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,10 @@
5959
MODEL_SYNONYMS: dict[str, tuple[str, str]] = {
6060
# 异名归一键 → (规范归一键, 规范展示名)
6161
"deepseek-flash": ("deepseek-v4.1-flash", "DeepSeek V4.1 Flash"),
62+
# CodeBuddy 的可读名带厂商前缀(`Xiaomi MiMo-V2.6-Pro`),TRAE 只叫
63+
# `MiMo-V2.6-Pro`:`Vendor: Model` 的冒号式前缀削不到它,键会多出
64+
# `xiaomi-`,同一模型被拆成两条(对外多出 `xiaomi-mimo-v2.6-pro`)。
65+
"xiaomi-mimo-v2.6-pro": ("mimo-v2.6-pro", "MiMo V2.6 Pro"),
6266
}
6367
_SYNONYM_DISPLAY: dict[str, str] = {
6468
key: display for key, (_, display) in MODEL_SYNONYMS.items()}

‎tests/api/test_models_merge.py‎

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,49 @@ def import_credential(self, raw): # pragma: no cover - 未使用
148148
assert aliases["qoder"]["deepseek-v4.1-flash"] == "dfmodel"
149149
assert aliases["qoder"]["dfmodel"] == "dfmodel"
150150

151+
def test_models_synonym_merges_cb_xiaomi_mimo_pro(tmp_path):
152+
"""CB 的 `Xiaomi MiMo-V2.6-Pro` 与 TRAE 的 `MiMo-V2.6-Pro` 并成一条。
153+
154+
CB 上游名带厂商前缀(空格式),清洗规则削不到,归一键会多出 `xiaomi-`,
155+
同一模型拆成两条、对外多出 `xiaomi-mimo-v2.6-pro`。`MODEL_SYNONYMS` 把
156+
两者收敛到同一个规范键与展示名,请求 `mimo-v2.6-pro` 也能路由到 CB。
157+
"""
158+
settings = Settings(_env_file=None, APP_SECRET=SECRET, DATA_DIR=str(tmp_path))
159+
160+
class Stub:
161+
def __init__(self, pid: str, models):
162+
self.id = pid
163+
self._models = models
164+
165+
async def list_models(self, _data):
166+
return list(self._models)
167+
168+
def import_credential(self, raw): # pragma: no cover - 未使用
169+
return raw
170+
171+
from src.provider.base import Model
172+
173+
app = build_app(settings, providers={
174+
"codebuddy": Stub("codebuddy", [Model(id="mimo-v2.6-pro",
175+
name="Xiaomi MiMo-V2.6-Pro",
176+
credit_rate=0.13)]),
177+
"trae": Stub("trae", [Model(id="mimo-v2.6-pro", name="MiMo-V2.6-Pro",
178+
credit_rate=0.15)]),
179+
})
180+
for provider_id in ("codebuddy", "trae"):
181+
app.state.credentials.add(provider=provider_id, credential_data={"t": "x"})
182+
key = app.state.api_keys.create("root")["api_key"]
183+
with TestClient(app) as client:
184+
data = client.get("/v1/models", headers={
185+
"Authorization": f"Bearer {key}"}).json()["data"]
186+
187+
assert [m["id"] for m in data] == ["mimo-v2.6-pro"] # 两渠道并成一条
188+
assert data[0]["providers"] == ["codebuddy", "trae"]
189+
assert data[0]["name"] == "MiMo V2.6 Pro"
190+
aliases = app.state.services.model_aliases
191+
assert aliases["codebuddy"]["mimo-v2.6-pro"] == "mimo-v2.6-pro"
192+
assert aliases["trae"]["mimo-v2.6-pro"] == "mimo-v2.6-pro"
193+
151194
def test_models_name_merge_includes_zen_and_guards_colliding_names(tmp_path):
152195
"""六条渠道统一按归一键合并;同渠道内归一键重复的退回原 id。
153196

‎tests/provider/test_naming.py‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,22 @@ def test_synonym_table_collapses_upstream_aliases(self):
251251
assert normalize_model_key("DeepSeek-Flash") != \
252252
normalize_model_key("DeepSeek-V4-Flash")
253253

254+
def test_synonym_table_collapses_vendor_prefixed_cb_name(self):
255+
"""CodeBuddy 的 `Xiaomi MiMo-V2.6-Pro` 与 TRAE 的 `MiMo-V2.6-Pro` 同一条。
256+
257+
CB 的可读名带厂商前缀(空格式,不是 `Vendor: ` 冒号式,清洗削不到),
258+
归一键会多出 `xiaomi-`,同一模型被拆成两条、对外多出
259+
`xiaomi-mimo-v2.6-pro`。异名表把键与展示名收敛到 TRAE 的写法。
260+
"""
261+
assert normalize_model_key("Xiaomi MiMo-V2.6-Pro") == "mimo-v2.6-pro"
262+
assert normalize_model_key("Xiaomi MiMo-V2.6-Pro") == \
263+
normalize_model_key("MiMo-V2.6-Pro")
264+
assert display_model_name("Xiaomi MiMo-V2.6-Pro") == "MiMo V2.6 Pro"
265+
assert display_model_name("Xiaomi MiMo-V2.6-Pro") == \
266+
display_model_name("MiMo-V2.6-Pro")
267+
# Flash 档不带前缀,不受影响
268+
assert normalize_model_key("MiMo-V2.6-Flash") == "mimo-v2.6-flash"
269+
254270
def test_synonym_display_is_idempotent(self):
255271
"""规范展示名再洗一遍不变(幂等),否则中心层二次清洗会来回横跳。"""
256272
canonical = display_model_name("DeepSeek-Flash")

0 commit comments

Comments
 (0)