模型归一(unify)
上游各站对同一个底座模型的命名碎片化:[A] glm-5.2、deepseek-ai/glm-5.2、glm-5.2-0731、glm-5.2-1。
模型归一把这些原名路由合并成一个对外名(canonical),让下游只认一个名字。它是一次可撤销的批次操作,不是一个开关。
一、它做什么
一次 apply 会(在一个事务里,逐组):
- 创建或复用 canonical 名的路由。
- 为每个「渠道 × 原名」变体创建成员,并写上转发名映射。
- 删除被这个组完全覆盖的原名路由。
- 把以上每一步记成一个 op,归属到该组的批次。
成员的形状
| 字段 | 值 | 为什么 |
|---|---|---|
mapping_json | {"real":"<原名>"} | 对外叫 canonical,实际发给上游的是原名 |
auto | true | 标记为自动创建 |
manual_override | true | 让发现流程不会覆盖它 |
priority / weight | 0 / 100 | 之后由操作员调 |
为什么需要批次记录:没有它,操作员只能靠猜哪些成员来自归一。旧的
auto=1 AND manual_override=1指纹与「人工固定的成员」无法区分。
复用已存在的同名路由时
canonical 名已经有一条停用的路由时,apply 会把它打开并记录 route_enabled op。
理由:一条停用的同名路由会吞掉下面创建的所有成员——别名存在但永不服务,看起来就是「统一后的模型消失了」。
二、op 类型
model_unify_ops 记录每一次改动,撤销时逆序回放:
| op | 含义 |
|---|---|
route_created | 本次创建了 canonical 路由 |
member_created | 本次创建了一个成员 |
route_enabled | 把一条已存在的停用同名路由打开了(prev_enabled 记下原状态) |
route_deleted | 删除了一个被取代的原名路由(带整行快照) |
route_archived | 旧方案:删除语义之前,批次是把原名路由停用而不是删除。回放仍然认识它,所以升级前的批次照样可撤销 |
三、四条不变量
这四条都是承重的,改动 unify 之前必须先看它们。
1. 被合并的原名路由是删除,不是停用
留一条 enabled=0 的旧名,它仍会在模型目录里显示成一行死名。
删除前必须 snapshotRouteRows 快照整行(含全部成员列);重建 = restoreRouteRows 按原 id 原样插回;单条重建 = 把对应的 op 置为 undone。
2. 撤销 route_created 前必须确认没有别的依赖
ensureRouteUndoable 拒绝两种情况:
| 情况 | 原因 |
|---|---|
| 另一个仍然活跃的批次在这条路由上创建过成员 | 删路由会让那个批次变成「标记为活跃但成员已消失」的孤儿 |
| 路由上有任何批次都没创建过的成员 | 那是操作员手工加的,撤销不能静默丢掉人工成果 |
判据是「路由今天的所有成员是否都能被批次 op 解释」。本批次自己创建的成员级联删掉是允许的。
3. 只在「完全覆盖」时才删原名路由
deleteSupersededRoutes 逐条检查:
- 名字等于 canonical → 跳过;
- 名字含通配符(
*/?) → 跳过; - 该路由的每一个成员所属的渠道都被本组绑定到统一名 → 才删;否则跳过;
- 成员数为 0 → 跳过。
宁可留下一个该退未退的绑定,也不冒删掉「没人要求退役」的绑定的风险。
4. 预览可以省略的条件很窄
一个组只有在「全部变体都已 mapped」且「没有 exposed originals」时才从预览里省略。
exposed_originals = 名字能折叠到这个 canonical 的其他启用路由(例如从历史重建回来的、或被旧方案停用的原名)。
这一条不能放宽:否则重建过的原名永远无法再被删除——它会一直被当作「已经归一完成」而跳过。
四、撤销与单条重建
| 操作 | 行为 |
|---|---|
| 批次撤销 | 逆序回放 op:本批次创建的成员与路由删掉,本批次删除的路由从快照重建 |
| 单条重建 | 只把某个被删的原名路由按快照插回,不撤销整个批次——别名路由继续存在 |
部分回滚仍然优于不回滚:已经 undone 的 op、或目标已经消失的 op,会被跳过而不是中止。批次已撤销过时,再次撤销会被拒绝。
单条重建过的条目仍留在历史里(界面上灰显),所以一次重建会留下可见痕迹,而不是静默消失。
五、快照机制的两个细节
| 细节 | 原因 |
|---|---|
用 PRAGMA table_info 动态取列 | 以后给 routes / route_members 加列不需要改快照代码 |
| 恢复时显式写回原 id | SQLite 的 AUTOINCREMENT 不复用被删的 id,所以 routes.single_member_id 与成员自带的 route_id 都无需重映射 |
同名路由已存在时拒绝重建(cannot rebuild)——避免把两份历史悄悄合成一份。
六、归一规则
系统按一组规则把原名折叠到 canonical,规则按应用顺序排列:
| 规则 | 例子 | 安全 |
|---|---|---|
account_prefix | [A] / 【次】 这类按账号的前缀 | 永远安全,所以不会出现在预览的规则列表里 |
vendor_prefix | deepseek-ai/… → … | 可能把两个确实不同的模型混同 |
date_suffix | -0731 / -20250514 / -2024-08-13 | 同上 |
index_suffix | 结尾的按账号序号 -1、-2 | 同上 |
会混同模型的规则若被用到,该组会被标记为需要人工确认,而不是自动应用。
账号前缀剥离为什么豁免标记:它剥掉的是「哪个账号」的信息,不可能把两个不同的模型混成一个,所以不值得让操作员逐组确认。
七、典型流程
GET /admin/unify/preview—— 看分组、变体、规则与exposed_originals。- 逐组确认(带风险的规则会标记出来)。
POST /admin/unify/apply—— 事务内落地,返回批次。- 不满意 →
POST /admin/unify/batches/{id}/undo;或只重建某一个原名 → 单条重建接口。