mirror of
https://gitcode.com/JianFeeeee/ModelRouter.git
synced 2026-11-12 12:17:23 +00:00
## 背景
用户报三问题:① channel 注册的模型无法使用;② 额度显示依旧异常;
③ 其他渠道应也存在类似问题。三条全部核实并给出结论(③ 见文档)。
## ① 网关解不开 `源名:模型名` 限定名(internal/provider/registry.go)
`ResolvePinned` 用 `strings.IndexAny(model, "-:/")` 找**第一个**分隔符。
而渠道源名形如 `channels-trae-b8dd6b3df30e`(**含 `-`**),于是
`channels-trae-b8dd6b3df30e:Doubao-Seed-2.1-Pro` 被切成
src="channels"、model="trae-b8dd6b3df30e:Doubao-Seed-2.1-Pro"
⇒ 源名永不匹配 ⇒ `Resolve()` 返回 nil
⇒ 客户端看到 `model "..." is not configured`。
**为什么这个写法必须支持**:本实例有 **44 个模型被多源同时提供**
(`glm-5.2` 由 6 个源提供),WebUI 的模型下拉框用 `源名:模型名` 作为
option value 是**有意设计** —— 用户必须能指定"从哪个源调用"。
修法:抽 `splitPinned()`,**按已知源名做最长前缀匹配**(源名是已知集合,
这是唯一可靠办法),保留旧 `-:/` 兜底以兼容非源名形态。
`EffectiveModel` 同步修(同一拆分规则,避免两处漂移)。
## ② trae 适配器三连错(internal/lua/plugins/channels.lua)
三个错误逐层叠加,每个都被上一层掩盖:
**(a) 没有 `build_headers`** ⇒ 内核兜底发 `Authorization: Bearer`,
而 trae 推理端点要 `Cloud-IDE-JWT` + 设备身份头 ⇒
401「We're sorry, but we are not able to authenticate you」。
→ 与上一轮「额度不可用」是**同一根因的两个出口**(额度端点修了、推理端点漏了)。
教训:修"某一族请求的鉴权"时,必须把该族的**所有出口**列出来逐个对齐。
**(b) header 常量的值是错的**(照参考 trae-product.ts 的 TRAE 配置逐项核对):
· `X-App-Id` 抄成 `6eefa6c9`,真实是 `6eefa01c-1036-4c7e-9ca5-d891f63bfcd8`
· `app_version_code` 用了 `0.1.52`,真实是**日期式** `20260811`
⇒ 上游 `4001 bad request: binding: expr_path=app_version_code,
cause=parameter type does not match binding data`
**(c) 流式转换完全不对**:TRAE 是 **SOLO 自定义 SSE**:
event:output → data:{"response":"###","reasoning_content":null,...}
正文在 `response`,推理在 `reasoning_content`。
· 套 `__prelude.openai_stream_chunk` ⇒ 无 `choices` ⇒ `empty stream`
· 改成 OpenAI 外壳 `{choices:[{delta}]}` **仍然空** —— 内核
`json.Unmarshal(unified, &UnifiedChunk)` 解的是**扁平结构**,外壳字段全被丢弃
⇒ 必须输出扁平 `{content, reasoning_content, usage, done, finish_reason}`。
⚠️ 内核只把 `data:` 行交给适配器(`event:` 行在 provider.go 被丢弃),
因此判别帧类型只能靠**内容特征**(`response` / `reasoning_content` /
`finish_reason` / `prompt_tokens`),拿不到 event 名。
## ③ 余额恒为 0(同文件 balance.total_fn)
读的是 `p.credits_limit`(pack **顶层**)—— 该字段**根本不存在**。
真实路径是 `pack.entitlement_base_info.quota.credits_limit`
(参考 trae-credits.ts:341)+ `pack.usage.credits_amount`。
实测上游:`免费`(无该字段) / `每月登录赠送`(500) / `签到奖励`(100) ⇒ 实际 600。
修法:读嵌套路径 + 保留顶层兼容 + `usage_summary.{total,consumed}_amount` 兜底。
修后余额 600 → 实测 598.516(真实扣减)。
## ④ 夹具与生产形状不一致(internal/core/channels_trae_test.go)
夹具原先返回**扁平** `{"Token":...}`,与生产 `{"Result":{"Token":...}}` 不符
⇒ 「代码只读顶层」这个缺陷在夹具里**恰好是对的**,所有 trae 判据都绿。
**夹具形状错 = 判据必然虚假通过**。这是 ExchangeToken 缺陷藏了两轮的原因。
## 判据(新增,变异 4/4 咬住)
- `internal/provider/registry_pinned_name_test.go`(4 用例)
- `internal/core/channels_trae_chat_headers_test.go`(3 条)
- `internal/core/channels_trae_solo_sse_test.go`(2 条)
- `internal/core/channels_trae_credits_hdr_test.go`(1 条)
- `internal/core/channels_trae_exchange_shape_test.go`(2 条)
## 文档
`docs/channel-repair-plan.md`:现状快照(生产实测)+ 各渠道缺陷矩阵 +
后续计划(P0-P4)。**如实记录**:其余 4 个渠道(zcode/cline/gemini/qoder)
有同类缺陷风险,但**账号数均为 0、无法做生产验证**。
## 生产已实测
- `channels-trae-...:Doubao-Seed-2.1-Pro` → 80 帧,正文 27 + 推理 299
- `channels-trae-...:glm-5.2` / `channels-buddy-...:hy4-preview` → OK
- `credits/status` → active:true, dailyCredit:100, todayCheckedIn:true
- `credits/balances` → total:598.516 credits
99 lines
3.3 KiB
Go
99 lines
3.3 KiB
Go
package provider
|
||
|
||
import "testing"
|
||
|
||
// ★ `源名:模型名` 这种限定写法,源名本身**含 `-`** 时必须仍能解开。
|
||
//
|
||
// 真实缺陷(用户报「无法使用 channel 注册的模型」,2026-10-17):
|
||
//
|
||
// WebUI 的模型下拉框用 `源名:模型名` 作为 option value —— 这是**有意**的:
|
||
// 本实例有 44 个模型被多个源同时提供(glm-5.2 被 6 个源提供),
|
||
// 用户必须能指定「从哪个源调用」。
|
||
//
|
||
// 但 ResolvePinned 用 `IndexAny(model, "-:/")` 找**第一个**分隔符,
|
||
// 而渠道源名形如 `channels-trae-b8dd6b3df30e`(含 `-`)⇒ 被切成
|
||
//
|
||
// src="channels"、model="trae-b8dd6b3df30e:Doubao-Seed-2.1-Pro"
|
||
//
|
||
// ⇒ 源名永不匹配 ⇒ Resolve() 返回 nil
|
||
// ⇒ 客户端看到 `model "channels-trae-xxx:..." is not configured`。
|
||
func TestResolvePinnedWithHyphenatedSourceName(t *testing.T) {
|
||
cases := []struct {
|
||
name string
|
||
sources map[string][]string // 源名 → 模型
|
||
input string
|
||
wantSrc string
|
||
wantMdl string
|
||
wantOK bool
|
||
}{
|
||
{
|
||
name: "渠道源名含两个 -(真实形态)",
|
||
sources: map[string][]string{
|
||
"channels-trae-b8dd6b3df30e": {"Doubao-Seed-2.1-Pro", "glm-5.2"},
|
||
"channels-buddy-buddy-main": {"hy4-preview", "glm-5.2"},
|
||
},
|
||
input: "channels-trae-b8dd6b3df30e:Doubao-Seed-2.1-Pro",
|
||
wantSrc: "channels-trae-b8dd6b3df30e",
|
||
wantMdl: "Doubao-Seed-2.1-Pro",
|
||
wantOK: true,
|
||
},
|
||
{
|
||
name: "同名模型跨源:必须能选中第二个源",
|
||
sources: map[string][]string{
|
||
"channels-trae-b8dd6b3df30e": {"glm-5.2"},
|
||
"channels-buddy-buddy-main": {"glm-5.2"},
|
||
},
|
||
input: "channels-buddy-buddy-main:glm-5.2",
|
||
wantSrc: "channels-buddy-buddy-main",
|
||
wantMdl: "glm-5.2",
|
||
wantOK: true,
|
||
},
|
||
{
|
||
name: "斜杠写法仍要支持",
|
||
sources: map[string][]string{
|
||
"channels-trae-b8dd6b3df30e": {"glm-5.2"},
|
||
},
|
||
input: "channels-trae-b8dd6b3df30e/glm-5.2",
|
||
wantSrc: "channels-trae-b8dd6b3df30e",
|
||
wantMdl: "glm-5.2",
|
||
wantOK: true,
|
||
},
|
||
{
|
||
// 自带 ":" 的第三方模型名,且**没有**任何源名与之匹配 ⇒ 走旧兜底。
|
||
// ⚠️ 不能拿 "our-free-model_cohere/..." 当例子:本实例真有一个叫
|
||
// `our-free-model_cohere` 的源,而仓库里还有叫 `our` 的源 ——
|
||
// 最长前缀匹配会正确命中 `our`,那个例子的预期是我写错了。
|
||
name: "无源名匹配时走旧兜底",
|
||
sources: map[string][]string{
|
||
"channels-trae-b8dd6b3df30e": {"glm-5.2"},
|
||
},
|
||
input: "vendor-lib/thing:free",
|
||
wantSrc: "vendor",
|
||
wantMdl: "lib/thing:free",
|
||
wantOK: true,
|
||
},
|
||
}
|
||
for _, c := range cases {
|
||
t.Run(c.name, func(t *testing.T) {
|
||
var ps []*Provider
|
||
for name, models := range c.sources {
|
||
ps = append(ps, newTestProvider(t, src(name, "http://127.0.0.1:1", "openai", models...)))
|
||
}
|
||
src, m, ok := splitPinned(c.input, ps)
|
||
if ok != c.wantOK {
|
||
t.Fatalf("ok 应为 %v,实得 %v(输入 %q)", c.wantOK, ok, c.input)
|
||
}
|
||
if !ok {
|
||
return
|
||
}
|
||
if src != c.wantSrc {
|
||
t.Fatalf("★ 源名解析错:期望 %q,实得 %q\n"+
|
||
" 输入 %q —— 源名含 `-` 时不能按第一个分隔符切", c.wantSrc, src, c.input)
|
||
}
|
||
if m != c.wantMdl {
|
||
t.Fatalf("模型名解析错:期望 %q,实得 %q", c.wantMdl, m)
|
||
}
|
||
})
|
||
}
|
||
}
|