mirror of
https://gitcode.com/JianFeeeee/ModelRouter.git
synced 2026-11-07 09:47:23 +00:00
## 缺陷 cline 的 3 个模型来自远端 **free 数组**(`recommended-models` 的唯一权威 free 集合), 却声明在 `models` 里 ⇒ 生产实测**全部 isFree=False**,与旁边的注释 「免费模型(远端 free 数组实测)」直接矛盾。用户看不到哪个可以不花钱用。 ## ★ 关键认识:两个参考用的是**不同机制**,不能混成一条链 | 平台 | 免费判定的权威 | |---|---| | cline | 远端 `free[]` → 命名约定 `cline-free/*` → 静态兜底表(**不查 cost**) | | opencode | models.dev 的 `cost`(`/zen/v1/models` 连能力字段都没有,cost 是**唯一**来源) | 我第一版把两者写成一条「OR 链」(任一来源说免费即免费),结果与既有判据 `TestCapabilityOverridesFreeFlag` **正面冲突** —— 那条判据要求「能力表说收费时 必须为 false」。冲突暴露了真问题:**只有平台自己知道它的免费名单在哪**。 ⇒ 改为平台声明 `free_source`: - `"capability"`(opencode)—— cost 权威,本地声明只作能力表拉不到时的兜底; - 缺省 —— 声明链权威(**OR 语义**:任一来源确认免费即免费)。 OR 语义是刻意的:写成「后者覆盖前者」会让「远端说免费、能力表拉不到」 静默退回收费,正是这类静默降级。 ## 其他改动 - `parseRemoteModels` 现在把远端的 `free[]` 记成**权威免费集合** (此前只收了 id、丢掉了"它免费"这层信息),provider 走**显式参数**而非 隐藏全局状态(隐藏状态是漂移源); - cline 补 `free_prefix = "cline-free/"`(远端未响应时的兜底依据)与显式 `models_free`(远端抖动时不把免费模型误标成收费)。 ## 判据 `TestIsFreeUsesAuthorityChain`:① cline 的 `cline-free/*` 与 `stealth/space-bunny-alpha` 必须免费;② 命名约定在远端未响应时能独立判出免费; ③ **反向**:收费通道的同名模型不得因前缀规则被判免费(参考点名 「免费模型是独立 id」)。既有 `TestCapabilityOverridesFreeFlag` 同时保持绿 —— 两条语义靠平台声明共存,而不是二选一。 ## 附带发现 全量回归曾出现 `TestConcurrentPluginAdapterUpdates` 挂 59 分钟: 单独跑 0.058s 通过 ⇒ 真因是 **`/tmp`(8.8G tmpfs)被 Go 构建缓存占满 100%**, 不是代码死锁。清理 6G 后全量 9 包全绿。
68 lines
2.7 KiB
Go
68 lines
2.7 KiB
Go
package core
|
||
|
||
import (
|
||
"testing"
|
||
)
|
||
|
||
// `isFree` 的判定链(照参考 cline-models.ts)。
|
||
//
|
||
// ★ 修的是一个与注释自相矛盾的缺陷:cline 的 3 个模型来自远端 **free 数组**,
|
||
// 却声明在 `models` 里 ⇒ 被标成收费。用户看不到哪个可以不花钱用。
|
||
//
|
||
// 参考的判定链(权威在前):
|
||
//
|
||
// ① 远端 free 数组(`recommended-models` 是**唯一权威**的免费集合;
|
||
// `/api/v1/models` 的 460 个 id 里根本没有 cline-free/*)
|
||
// ② 命名约定前缀 `cline-free/*`(远端未响应时的兜底)
|
||
// ③ 声明的 models_free
|
||
// ④ 能力表的 cost(models.dev)
|
||
//
|
||
// ⚠️ 免费是 **OR** 语义:多个来源任一确认免费就够。写成"后者覆盖前者"会让
|
||
// 「远端说免费、能力表拉不到」退回收费 —— 正是这类静默降级。
|
||
func TestIsFreeUsesAuthorityChain(t *testing.T) {
|
||
_, ps := poolCore(t)
|
||
|
||
// cline 的免费模型必须被标成免费(远端/free_prefix/声明 任一命中即可)
|
||
out, err := ps.Call("channels", "model_list", map[string]interface{}{"platform": "cline"})
|
||
if err != nil {
|
||
t.Fatalf("model_list: %v", err)
|
||
}
|
||
j, _ := out.(map[string]interface{})["json"].(map[string]interface{})
|
||
got := map[string]bool{}
|
||
for _, it := range asArrInterface(j["models"]) {
|
||
if mm, ok := it.(map[string]interface{}); ok {
|
||
if id, ok := mm["id"].(string); ok {
|
||
if f, ok := mm["isFree"].(bool); ok {
|
||
got[id] = f
|
||
}
|
||
}
|
||
}
|
||
}
|
||
// `cline-free/*` 前缀命中 ⇒ 必须免费(命名约定兜底)
|
||
if !got["cline-free/mimo-v2.6-flash"] {
|
||
t.Errorf("cline-free/* 前缀的模型必须免费(命名约定兜底),实得 %v", got)
|
||
}
|
||
// stealth/space-bunny-alpha 来自远端 free 数组,也必须免费
|
||
if !got["stealth/space-bunny-alpha"] {
|
||
t.Errorf("远端 free 数组里的模型必须免费,实得 %v", got)
|
||
}
|
||
|
||
// 命名约定必须生效(远端未响应时唯一的兜底依据)
|
||
probe, _ := ps.Call("channels", "__is_free_probe", map[string]interface{}{
|
||
"platform": "cline", "model_id": "cline-free/some-new-model",
|
||
})
|
||
pj, _ := probe.(map[string]interface{})["json"].(map[string]interface{})
|
||
if pj["is_free"] != true {
|
||
t.Error("远端未响应时,`cline-free/*` 前缀必须能判出免费(这是兜底依据)")
|
||
}
|
||
// 反向:收费通道的同名模型**不得**被判免费
|
||
//(参考点名:免费模型是独立 id,`cline-free/x` 与 `x` 是两个不同模型)
|
||
probe2, _ := ps.Call("channels", "__is_free_probe", map[string]interface{}{
|
||
"platform": "cline", "model_id": "deepseek/deepseek-v4.1-flash",
|
||
})
|
||
pj2, _ := probe2.(map[string]interface{})["json"].(map[string]interface{})
|
||
if pj2["is_free"] == true {
|
||
t.Error("收费通道的模型不得因为前缀规则被判成免费")
|
||
}
|
||
}
|