Files
ModelRouter/internal
JianFeeeee 2a2067284d fix(channels): isFree 与声明自相矛盾 —— 免费判定改为**按平台声明权威来源**
## 缺陷

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 包全绿。
2026-10-14 20:00:58 +08:00
..