mirror of
https://gitcode.com/JianFeeeee/ModelRouter.git
synced 2026-11-07 09:47:23 +00:00
## ① trae:模型选择器里那几个 `custom_model_*` 是**槽位不是模型** 用户报「trae 的模型不正确 / 渠道不可用」。逐个实测(生产): `custom_model_kimi` / `custom_model_claude` / `custom_model_1M` 返回 **空 content**(`custom_model_gpt-5` 也答非所问),而真实模型 (glm-5.2 → Z.ai GLM、kimi-k3 → Moonshot、minimax-m3 → MiniMax、 Doubao-Seed-2.1-Pro → 字节)全部正常 —— 所以渠道是通的,问题在**表**。 根因:`custom_model_*` 是 TRAE 的「自定义模型槽位」,上游目录里它们的权威 判据是 `is_custom_model: true`。插件自己的 `trae_batch` 解析器早已按这条 **准入**剔除,但**兜底表**把它们当普通模型列了出来;而远端目录在部分账号 下恒空(企业租户账号实测:22 个 function 全回 `error_code 2001 Config detail for function: X not found`),用户只能看到 兜底表 ⇒ 选中槽位后上游回空 ⇒ 「模型坏了」。 修法(**纯插件**,内核零改动): - 新增 `models_unavailable` 一档:声明「本协议下不可调用」的条目, **既不进 UI 选择器,也不进上游源**(与 `models_hidden` 区别:后者只是 不进 UI,内部子 agent 仍可显式调用); - trae 的 11 个 `custom_model_*` 移入该档;把 4 个内部子 agent 留在 `models_hidden`(语义不变); - 生产验证:trae 源/选择器从 28 → **17**,逐个实测 17 个全部返回非空内容。 ## ② 远端目录「空」要能区分原因 `refresh_models` 此前只会报「远端返回空目录」,把两种完全不同的故障混为一谈: 网络/HTTP 故障 vs 服务端 200 但账号侧没有可下发配置。 现在后者如实报:「远端 200 但无模型配置(账号侧未下发 function config, 企业/租户账号需管理员开启,实测 error_code 2001)」——运维据此知道该找 租户管理员,而不是反复重试网络。 另加 `__trae_catalog_probe`(诊断探针):原样回传目录端点的 status/body 前 N 字节/解析条数,并在 2001 现场定位时立了大功(这类"上游 200 却空目录"没有 原始响应只能瞎猜)。 ## ③ opencode / localzen:免费模型按**实测**收敛 用户报「localzen 也不能用了」。逐项实测(生产): - `space-bunny-free` → **410** `Model space-bunny-free has been deprecated. Use minimax-m3.1-flash-free instead.`(仍在本地源里 ⇒ 选中必失败) - `jev-1.13-free` → 400 `Model does not support this protocol.`(非 OpenAI chat 协议,本端点永远调不通) - `beam-free` → 200 但 content 空、无 finish_reason - 其余 7 个(mimo / nemotron-super-vl / nemotron-lightning / longcat / deepseek-v4.1 / glm-5.3-flash / minimax-m3.1-flash)全部正常返回内容 修法: - `DEAD_GONE_PATTERNS` 补 opencode 的下架文案 (`Model … has been deprecated`);配阴/阳性判据(参数/字段被弃用**不算**); - opencode 平台把 jev-1.13-free / beam-free 列入 `models_unavailable`, 并从兜底表与 models_free 里移除; - 生产 `localzen` 源(手写源,插件不管)按实测刷新为 7 个可用免费模型, 逐个验证返回内容。 ## 纪律 渠道逻辑全部留在插件里(channels.lua / 插件自装适配器);内核这轮**没有** 新增任何厂商语义 —— 上一轮加进内核的只有通用的 `adapter.hidden` 机制。
112 lines
3.8 KiB
Go
112 lines
3.8 KiB
Go
package core
|
||
|
||
import "testing"
|
||
|
||
// TRAE 的模型选择器**不许**出现 `custom_model_*`。
|
||
//
|
||
// 真实缺陷(2026-10-20 用户报「trae 的模型不正确 / 渠道不可用」):
|
||
// 这些 id 是 TRAE 的「自定义模型槽位」,上游目录里它们的权威判据是
|
||
// `is_custom_model: true`(插件 trae_batch 解析器早已按这条准入剔除)。
|
||
// 但**兜底表**把它们当普通模型列了出来 —— 而远端目录在部分账号下恒空
|
||
// (企业租户账号实测:22 个 function 全回 error_code 2001 "Config detail
|
||
// for function: X not found"),于是用户只能看到兜底表,选中这些槽位后
|
||
// 上游回**空 content**(实测 custom_model_kimi / custom_model_claude /
|
||
// custom_model_1M 均空),表现就是"模型坏了、渠道不可用"。
|
||
//
|
||
// 判据分两层:
|
||
// - model_list(UI 选择器)不含它们;
|
||
// - 交给内核的上游源里也不含它们(effectiveModels 走 models_unavailable)。
|
||
func TestTraeCustomModelSlotsAreNotOffered(t *testing.T) {
|
||
c := kernelTestCore(t)
|
||
ps := c.Plugins()
|
||
src, err := readBundledChannels(t)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if err := ps.LoadSource("channels", src); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
_ = ps.SetPluginKernelPermission("channels",
|
||
[]string{"add_source", "remove_source", "list_sources", "status"}, false)
|
||
|
||
out, err := ps.Call("channels", "model_list", map[string]interface{}{"platform": "trae"})
|
||
if err != nil {
|
||
t.Fatalf("model_list: %v", err)
|
||
}
|
||
j, _ := out.(map[string]interface{})["json"].(map[string]interface{})
|
||
models, _ := j["models"].([]interface{})
|
||
if len(models) == 0 {
|
||
t.Fatal("trae 应至少有一个可选模型(兜底表)")
|
||
}
|
||
for _, raw := range models {
|
||
m, _ := raw.(map[string]interface{})
|
||
id, _ := m["id"].(string)
|
||
if len(id) > 13 && id[:13] == "custom_model_" {
|
||
t.Fatalf("model_list 不应包含自定义模型槽位 %q(选中后上游回空内容)", id)
|
||
}
|
||
}
|
||
// 可调用模型必须还在(不能把整张表干掉换绿)
|
||
found := false
|
||
for _, raw := range models {
|
||
m, _ := raw.(map[string]interface{})
|
||
if m["id"] == "glm-5.2" {
|
||
found = true
|
||
}
|
||
}
|
||
if !found {
|
||
t.Fatalf("可调用的 glm-5.2 不应被一起藏掉;实得 %v", models)
|
||
}
|
||
}
|
||
|
||
// 上游源的模型清单同样不含占位槽位(客户端按裸名就能调,藏 UI 不够)。
|
||
func TestTraeSourceOmitsCustomModelSlots(t *testing.T) {
|
||
c := kernelTestCore(t)
|
||
ps := c.Plugins()
|
||
src, err := readBundledChannels(t)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if err := ps.LoadSource("channels", src); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
_ = ps.SetPluginKernelPermission("channels",
|
||
[]string{"add_source", "remove_source", "list_sources", "status"}, false)
|
||
if _, err := ps.Call("channels", "account_add", map[string]interface{}{
|
||
"platform": "trae", "account_id": "trae-a",
|
||
"credential": `{"access_token":"jwt","uid":"1","device_id":"d","machine_id":"m"}`,
|
||
}); err != nil {
|
||
t.Fatalf("account_add: %v", err)
|
||
}
|
||
ls, err := ps.Call("channels", "__list_sources_probe", map[string]interface{}{})
|
||
if err != nil {
|
||
t.Fatalf("probe: %v", err)
|
||
}
|
||
j, _ := ls.(map[string]interface{})["json"].(map[string]interface{})
|
||
arr, _ := j["sources"].([]interface{})
|
||
haveTrae := false
|
||
for _, raw := range arr {
|
||
m, _ := raw.(map[string]interface{})
|
||
if m["name"] == "channels-trae" {
|
||
haveTrae = true
|
||
}
|
||
}
|
||
if !haveTrae {
|
||
t.Fatalf("trae 账号加好后应产出 channels-trae 源;实得 %v", arr)
|
||
}
|
||
// 源里的模型清单从内核真实源读(不是插件的 model_list)
|
||
srcs := c.Sources()
|
||
for _, s := range srcs {
|
||
if s.Name != "channels-trae" {
|
||
continue
|
||
}
|
||
for _, m := range s.Models {
|
||
if len(m.ID) > 13 && m.ID[:13] == "custom_model_" {
|
||
t.Fatalf("上游源不应包含自定义模型槽位 %q", m.ID)
|
||
}
|
||
}
|
||
if len(s.Models) == 0 {
|
||
t.Fatal("channels-trae 源应带模型清单")
|
||
}
|
||
}
|
||
}
|