Files
ModelRouter/internal/lua/channels_trae_solo_test.go
JianFeeeee de7553eaa2 feat(lua)!: 插件只通过 modelrouter SDK 与内核交互,且不再随内核安装
- 新增唯一公开接口表 `modelrouter`(sdk_version=1.0):http / secrets /
  kernel / adapters / plugin_dir。成员与内部桥是**同一对象**而非副本 ——
  复制会让凭据保险库分叉(插件写 A、内核读 B)。
- `llmsproxy` 与 `__llmsproxy_*` 降为同对象兼容别名,只为已装插件继续可用。
  仓库内 4 个插件(channels / billing / quota / usage)全部改用
  `modelrouter.*`:此前它们直接依赖内核私有全局,内核无法在不炸插件的前提下
  改实现(重命名、加守卫、换实现都算破坏性变更)。
- 插件不再由网关安装:`SeedBundled()` 变彻底 no-op,`core.New` 不再调用;
  `ReadBundledPlugin` 改为按源文件定位(脱离 cwd),只服务测试与开发工具。
- 桌面端删除 `BUILTIN_PLUGINS` 与 `syncBundledPluginsOnUpgrade`:升级不再
  删除、改名或覆盖用户插件文件。插件是运维资产,不是内核资源。
- `Builtin` 不再靠"源码与仓库示例逐字节相同"推断,插件一律视为外部文件。
- 判据:SDK 存在/版本/形状、别名与内部桥同一张表、适配器 VM 拿不到 SDK、
  经 SDK 真实写读凭据并驱动内核动作、GUI 升级不动插件文件、release 包不含插件。

说明:channels.lua / quota.lua 同时带有此前的渠道与额度改动(含 OpenCode 免费池
监控),与 SDK 改名同处一个文件,无法文件级拆分,故一并纳入。
2026-10-26 22:27:12 +08:00

103 lines
4.3 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 lua
import (
"encoding/json"
"strings"
"testing"
)
// TRAE 不是 OpenAI 兼容:参考 trae.ts:30 明确「载荷必须转换 —— 映射为 SOLO
// 格式(function、config_name 等字段),不能透传」。此前我们把 trae 声明成
// adapter=openai 直发,端点对了、载荷全错。
//
// 本测试走**真实路径**:装插件(插件自己装 channels-trae 适配器)→ 用
// vm.Transform 调那个**已安装**的适配器。这比在插件 VM 里 load 一份副本更
// 可信:副本拿不到内核注入的共享前导,且无法发现"装了但没生效"。
func TestChannelsTraeSoloAdapter(t *testing.T) {
vm, ps, _ := runtimePlugins(t)
src, err := ReadBundledPlugin("channels")
if err != nil {
t.Fatal(err)
}
if err := ps.LoadSource("channels", string(src)); err != nil {
t.Fatal(err)
}
// 插件应在加载时自装 TRAE SOLO 适配器(名字 channels-trae,避开内置 trae)
names := map[string]bool{}
for _, a := range vm.ListAdapters() {
names[a.Name] = true
}
if !names["channels-trae"] {
t.Fatalf("插件未装 channels-trae 适配器;已装: %v", names)
}
if owner := ps.AdapterOwnerOf("channels-trae"); owner != "channels" {
t.Errorf("归属应为 channels,实得 %q", owner)
}
// 内置 trae(供运维的 trae-local-api 反代用)必须**不被顶掉**
if !names["trae"] {
t.Error("内置 trae 适配器被顶掉了(它仍服务运维自建的本地反代)")
}
in := `{"model":"glm-5.2","messages":[` +
`{"role":"system","content":"you are helpful"},` +
`{"role":"user","content":"hi"},` +
`{"role":"assistant","content":"ok","tool_calls":[` +
`{"id":"c1","type":"function","function":{"name":"f","arguments":"{}"}},` +
`{"id":"c2","type":"function","function":{"name":"","arguments":"{}"}}]}` +
`],"tools":[{"type":"function","function":{"name":"f","description":"d",` +
`"parameters":{"type":"object","properties":{"a":{"type":"string"}}}}}],` +
`"tool_choice":{"type":"function","function":{"name":"f"}},` +
`"stream":false,"extra_body":{"x":1}}`
out, err := vm.Transform("channels-trae", "transform_request", in)
if err != nil {
t.Fatalf("Transform: %v", err)
}
t.Logf("SOLO 载荷: %s", out)
for _, want := range []struct{ desc, needle string }{
{"规则2 stream 必须强制 true(非流式由服务端聚合)", `"stream":true`},
{"规则3 model → config_name + model 双字段", `"config_name":"glm-5.2"`},
{"规则4 function 通道(缺省 solo_work_lite)", `"function":"solo_work_lite"`},
{"规则1 content 字符串 → parts 数组", `"text":"hi"`},
{"规则6 tool_calls 的 function → function_call", `"function_call"`},
{"规则7 parameters 必须是 JSON 字符串(SOLO 要求)", `{\"properties\"`},
{"规则5 tool_choice 为 function 时归一成 name 字符串", `"tool_choice":"f"`},
} {
if !strings.Contains(out, want.needle) {
t.Errorf("%s\n 缺 %s\n 实际: %s", want.desc, want.needle, out)
}
}
// 反向断言
if strings.Contains(out, `"extra_body"`) {
t.Errorf("内核私有字段 extra_body 未剥离:%s", out)
}
if strings.Contains(out, `"id":"c2"`) {
t.Errorf("无 name 的 tool_call 应被剔除(孤儿会让整条会话被拒):%s", out)
}
// 必须是合法 JSON,且 model/config_name 同值、stream 为 true
var parsed map[string]interface{}
if err := json.Unmarshal([]byte(out), &parsed); err != nil {
t.Fatalf("转换结果不是合法 JSON: %v\n%s", err, out)
}
if parsed["model"] != "glm-5.2" || parsed["config_name"] != "glm-5.2" {
t.Errorf("model/config_name 应同为 glm-5.2:%v / %v", parsed["model"], parsed["config_name"])
}
if parsed["stream"] != true {
t.Errorf("stream 应为 true,实得 %v", parsed["stream"])
}
// tool_choice=none 时必须同时删掉 tools 与 tool_choice 本身
noneIn := `{"model":"glm-5.2","messages":[{"role":"user","content":"x"}],` +
`"tools":[{"type":"function","function":{"name":"f","parameters":{"type":"object"}}}],` +
`"tool_choice":"none"}`
noneOut, err := vm.Transform("channels-trae", "transform_request", noneIn)
if err != nil {
t.Fatalf("Transform(none): %v", err)
}
if strings.Contains(noneOut, `"tools"`) {
t.Errorf("tool_choice=none 时 tools 必须被删:%s", noneOut)
}
if strings.Contains(noneOut, `"tool_choice"`) {
t.Errorf("tool_choice=none 时该字段本身也应被删:%s", noneOut)
}
}