mirror of
https://gitcode.com/JianFeeeee/ModelRouter.git
synced 2026-11-07 09:47:23 +00:00
## 真因:两套头混用
用户报「trae 还是无法从上游拉取,示例插件都可以」。用探针把**原始响应**打出来
才看清:目录端点 HTTP **200**,但 22 个 function 全部
`error_code 2001 "Config detail for function: X not found"` —— 于是远端目录恒空,
插件只能退回兜底表,用户侧表现成「模型不正确 / 渠道不可用」。
根因不是网络、不是账号、不是 body:是**头族用错了**。
- `traeCheckinHeaders` 面向 `api.trae.cn` 的签到/积分:VSCode UA + `X-Market-*`
+ `X-Lscbd-*` + `Vscode-Sessionid` + **派生**的 market device id;
- `traeIdeHeaders` 面向 `trae-api-cn.mchost.guru` 的目录/对话:`Trae/0.1.52` UA
+ `X-App-Id` + `X-Ide-Version(-Code)` + `Request-Traffic-Type` +
**凭据里登录时的** `device_id`/`machine_id`。
目录刷新当时复用的是签到头(注释还写着"复用更安全"),发到 IDE 端点就被逐
function 判 2001。用 IDE 头直发同一端点:**HTTP 200 + 2.6 MB**、每个 function
都带 `config_info_list`。
修法(全部在插件内,内核零改动):
1. 新增 `traeIdeHeaders()`,与签到头**明确分开**,注释写清两套头的适用面;
2. 目录刷新(`sign="trae_solo"`)改用它,`Accept: application/json`;
3. 加判据 `__trae_headers_probe` + `TestTraeHeadersKeptSeparate`:钉死两套头的
键集合互不越界(IDE 头必须有 X-App-Id/X-Machine-Id、不得有 X-Market-*;
签到头反之)。这类"都能跑、但跑错了"的错只能靠契约钉死。
## 目录拉通后暴露的第二个问题:目录≠可调用
拿到真实目录(22 function / 93 条去重模型)后逐个实测,发现**同一个
config_name 在不同 function 里可用性不同**,而本插件适配器恒发
`solo_work_lite`:
- `kimi-k2.8-preview` / `deepseek-V3` / `glm-4.7` / `code-review-judge` /
`multimodal_image_kimi-k2.5` → HTTP 200 但 **content 为空**;
- `sagitta` → 自称 Claude、`aquila` → 自称 ChatGPT、`custom_model_gpt-5` →
自称 GPT-4 系列 ⇒ 这些是**未知 config 时上游悄悄回落默认模型** ——
这正是"模型不正确"的真实来源。
于是补两道准入(都是**声明式**平台数据,不写死逻辑):
- `models_remote.admit_functions = { "solo_work_lite" }`:只收本适配器**会发**
的那条通道里的模型(白名单只说"该通道在本端点可调用",不等于"适配器会发");
- `models_fallback_no_supplement = true`:远端目录可用时**不再补兜底表**
(兜底表里 sagitta/aquila/glm-5/DeepSeek-V4-Flash 等在上游已无对应配置)。
结果:trae 可选模型 = **20 个**(glm-5.3 / glm-5.3-flash / glm-5.3-flashx /
glm-5.2 / kimi-k3 / kimi-k2.7-code / kimi-k2.6 / minimax-m3 / minimax-m3.1-flash /
qwen3.8-max / qwen-3.7-plus / step-5-preview / mimo-v2.6-{flash,pro} /
deepseek-v4.1-flash / DeepSeek-V4-{Flash,Pro}-Official / Doubao-Seed-2.1-{Pro,Turbo,Evolving}),
**20/20 逐个实测返回非空内容**。
## 回归测试
- `TestTraeHeadersKeptSeparate`(新):两套头键集合互斥(不联网即可复现该类回归)
- `TestTraeCustomModelSlotsAreNotOffered` / `TestTraeSourceOmitsCustomModelSlots`
(上一版):custom_model_* 槽位不进 UI 也不进上游源
- 死模型判据:opencode 的 `has been deprecated` 文案阳/阴性(上一版)
72 lines
2.6 KiB
Go
72 lines
2.6 KiB
Go
package core
|
||
|
||
import "testing"
|
||
|
||
// TRAE 的**两套头不能混用** —— 这是 2026-10-20 那次"目录永远拉不到"的真因。
|
||
//
|
||
// 事故链:模型目录端点在 `trae-api-cn.mchost.guru`(IDE 面),需要 IDE 头族
|
||
// (Trae UA + X-App-Id + X-Ide-Version* + 凭据里的 X-Machine-Id/X-Device-Id);
|
||
// 而 `api.trae.cn` 的签到/积分端点需要另一套(VSCode UA + X-Market-* +
|
||
// X-Lscbd-* + 派生 market device id)。插件当时"复用签到头"去拉目录,上游对
|
||
// 22 个 function 全部回 `error_code 2001 Config detail for function: X not found`
|
||
// (HTTP 200!),于是远端目录恒空、只能退回兜底表 —— 用户看到的却是
|
||
// "trae 模型不正确 / 渠道不可用",与真因隔了整整一层协议。
|
||
//
|
||
// 这个测试把两套头的**契约**钉死:键集合必须互不越界。它不联网,因此能
|
||
// 在 CI 里稳定复现"用错头"这一类回归。
|
||
func TestTraeHeadersKeptSeparate(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)
|
||
}
|
||
out, err := ps.Call("channels", "__trae_headers_probe", map[string]interface{}{})
|
||
if err != nil {
|
||
t.Fatalf("probe: %v", err)
|
||
}
|
||
j, _ := out.(map[string]interface{})["json"].(map[string]interface{})
|
||
pick := func(field string) map[string]bool {
|
||
set := map[string]bool{}
|
||
arr, _ := j[field].([]interface{})
|
||
for _, raw := range arr {
|
||
if s, ok := raw.(string); ok {
|
||
set[s] = true
|
||
}
|
||
}
|
||
return set
|
||
}
|
||
ide, chk := pick("ide"), pick("checkin")
|
||
if len(ide) == 0 || len(chk) == 0 {
|
||
t.Fatalf("两套头都应有内容;实得 ide=%v checkin=%v", ide, chk)
|
||
}
|
||
// IDE 面(目录/对话):身份与版本头必须在
|
||
for _, k := range []string{"X-App-Id", "X-Ide-Version", "X-App-Version-Code",
|
||
"X-Machine-Id", "X-Device-Id", "Request-Traffic-Type"} {
|
||
if !ide[k] {
|
||
t.Errorf("IDE 头缺少 %q(目录端点会回 2001)", k)
|
||
}
|
||
}
|
||
// IDE 面**不得**带 UG/积分的头
|
||
for _, k := range []string{"X-Market-Client-Id", "X-Lscbd-Aid", "X-Market-User-Id"} {
|
||
if ide[k] {
|
||
t.Errorf("IDE 头不应包含 %q(那是签到头)", k)
|
||
}
|
||
}
|
||
// 签到头:UG/积分标记必须在
|
||
for _, k := range []string{"X-Market-Client-Id", "X-Market-User-Id", "X-Lscbd-Aid"} {
|
||
if !chk[k] {
|
||
t.Errorf("签到头缺少 %q", k)
|
||
}
|
||
}
|
||
// 签到头不得冒用 IDE 的 App/机型身份
|
||
for _, k := range []string{"X-App-Id", "X-Ide-Version", "X-Machine-Id"} {
|
||
if chk[k] {
|
||
t.Errorf("签到头不应包含 %q(那是 IDE 身份)", k)
|
||
}
|
||
}
|
||
}
|