Files
ModelRouter/internal/core/channels_trae_headers_test.go
root 7f193a997d fix(channels): trae 目录用错头族——IDE 端点必须发 IDE 头(此前恒回 2001)
## 真因:两套头混用
用户报「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` 文案阳/阴性(上一版)
2026-10-20 09:38:39 +08:00

72 lines
2.6 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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)
}
}
}