mirror of
https://gitcode.com/JianFeeeee/ModelRouter.git
synced 2026-11-07 09:47:23 +00:00
## 真实缺陷 ①:glm-5.1 恒 400(用户报"这个模型不可用")
上游回 `400 {"code":11150,"msg":"the reasoning effort value is not supported
by the current model"}`。探针逐字对照实验(body 完全由探针给定):
glm-5.1 裸(无该字段) → 400 11150 ✗
glm-5.1 + reasoning_effort:medium → 200 ✓
glm-5.1 空串 → 400 11150 ✗
**任何非空档位都被接受**(low/medium/high/xhigh/max/minimal 逐个 200);
而其余 15 个 buddy 模型裸跑全 200(多带该字段也无害)⇒ 这是 glm-5.1 **独有**要求。
参考 product.ts:237 声明它只有 `['medium']` 一档。
## 真实缺陷 ②:流式请求缺 Accept: text/event-stream → 空回复
不发该头时上游**接受请求(200)但只回一帧空内容**(completion_tokens=0、
finish_reason 直接 stop),既不报错也不产出正文 —— 表现是"模型不回答",
极易误判成模型坏了或额度不足。同 body 加该头 → 39~43 帧正常 SSE。
参考 buddy-adapter.ts 的 send() 固定发它。
## 实现:内置 `buddy.lua`(不是插件自装)
**为什么不用插件自装适配器**(channels-zcode/qoder/cline 那条路):
`channels.lua` 的**主 chunk 已 248 个 local**,Lua/LuaJIT 上限是 200 ——
超出的 local 会**静默退化为全局**(`luac -p` 通过,运行时才 `attempt to call
global 'x' (a nil value)`)。实测证据:`adaptersDir` 是第 208 个 ⇒ 调用点拿到 nil
⇒ `loadAnthropic` 静默返回 nil ⇒ **zcode 适配器从来没有基座**
("复用 anthropic"的意图从未生效,只是它自写了全部转换所以能用)。
我第一版就是这么写的,结果适配器装不上(`adapter channels-buddy not loaded`),
且 `hy3`/`glm-5.3`/`kimi-k3-1` 全被打成空回复 —— 只有 `glm-5.2` 因多源兜底
"看起来正常",症状再次指向错误方向。
⇒ 改走内置适配器(与 trae/sensenova/opencodezen 同性质,`deploy.sh` 自动同步),
`channels.lua` 只改**一行** `adapter = "buddy"`,不再触碰那个已贴上限的文件。
## ★ 排查中发现的沙箱事实(值得记)
**适配器运行在另一个沙箱 VM**:那里 `__llmsproxy_plugin_dir` 与
`LLMSPROXY_PLUGIN_DIR` **都不存在**(实测均为 nil),**也没有文件系统访问**。
故任何"在适配器里读兄弟适配器源码"的写法都拿不到基座且静默降级。
判据 `TestBuddyAdapterDefinesAllTransforms` 咬住这条。
## 顺带修:hy4-preview-x 从目录剔除
实测它对上游**恒回 11102**(`model service info not found`,加/不加
reasoning_effort 都一样 ⇒ 与 effort 无关,是后端未开放)。它被服务端 agents
引用,于是 `agentRef` 例外把它放回目录 ⇒ 用户选中必 400 而错误指向模型。
参考用**白名单重建**达到同样效果(其兜底表里没有它)。本仓加等价的精确黑名单
(只放实测确认的,不做猜测),并在两处兜底目录里一并剔除。
## 验证
buddy **15/15 模型全部可用**(含 glm-5.1),逐一实测无失败;
`hy4-preview-x` 已从源目录消失(16 → 15)。
## 判据(3 条 + 变异 2/2)
- `TestBuddyAdapterEffortAndHeaders`:glm-5.1 补 medium / hy3 不补 /
调用方给的档位被尊重 / Accept 必须是 SSE + 身份头齐备
- `TestBuddyAdapterKeepsOpenAIWire`:协议转换与 openai 一致(只是补两个字段)
- `TestBuddyAdapterDefinesAllTransforms`:**四个转换函数都存在** ——
咬"静默降级"而非"某模型能回话"(后者会被多源兜底掩盖,本会话已栽两次)
- 变异:删 stream_chunk / 不补 effort → 均报红
## 一起提交的其它独立缺陷(均在本次调查中发现)
- buddy 目录过滤(四类判据)与 hy4-preview-x 剔除
117 lines
4.5 KiB
Go
117 lines
4.5 KiB
Go
package lua
|
||
|
||
import (
|
||
"encoding/json"
|
||
"strings"
|
||
"testing"
|
||
)
|
||
|
||
// loadBuddyAdapter 装载内置 buddy 适配器。
|
||
func loadBuddyAdapter(t *testing.T) *VM {
|
||
t.Helper()
|
||
src, err := bundledAdapters.ReadFile("adapters/buddy.lua")
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
vm := NewVM(t.TempDir())
|
||
t.Cleanup(vm.Stop)
|
||
if err := vm.LoadAdapterSource("buddy", string(src)); err != nil {
|
||
t.Fatalf("装载失败: %v", err)
|
||
}
|
||
return vm
|
||
}
|
||
|
||
// ★ buddy 适配器:两个厂商要求,各有实测依据。
|
||
//
|
||
// ① glm-5.1 **必须**带非空 reasoning_effort(否则上游 400 + 11150)
|
||
// ② 流式请求必须带 Accept: text/event-stream(否则只回空帧、completion_tokens=0)
|
||
func TestBuddyAdapterEffortAndHeaders(t *testing.T) {
|
||
vm := loadBuddyAdapter(t)
|
||
|
||
// ① glm-5.1 → 补 medium
|
||
out, err := vm.Transform("buddy", "transform_request",
|
||
`{"model":"glm-5.1","messages":[{"role":"user","content":"hi"}],"max_tokens":100,"stream":true}`)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
var b map[string]interface{}
|
||
if err := json.Unmarshal([]byte(out), &b); err != nil {
|
||
t.Fatalf("%v\n%s", err, out)
|
||
}
|
||
if b["reasoning_effort"] != "medium" {
|
||
t.Errorf("glm-5.1 必须补 reasoning_effort=medium(否则上游 11150),实得 %v", b["reasoning_effort"])
|
||
}
|
||
|
||
// ② 其它模型**不补**(避免把单模型要求扩成全平台行为)
|
||
out2, err := vm.Transform("buddy", "transform_request",
|
||
`{"model":"hy3","messages":[{"role":"user","content":"hi"}],"max_tokens":100,"stream":true}`)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
var b2 map[string]interface{}
|
||
json.Unmarshal([]byte(out2), &b2)
|
||
if b2["reasoning_effort"] != nil {
|
||
t.Errorf("hy3 不该被补 reasoning_effort(实测不需要),实得 %v", b2["reasoning_effort"])
|
||
}
|
||
|
||
// ③ 调用方已给档位时**尊重它**
|
||
out3, _ := vm.Transform("buddy", "transform_request",
|
||
`{"model":"glm-5.1","messages":[{"role":"user","content":"hi"}],"reasoning_effort":"high","stream":true}`)
|
||
var b3 map[string]interface{}
|
||
json.Unmarshal([]byte(out3), &b3)
|
||
if b3["reasoning_effort"] != "high" {
|
||
t.Errorf("调用方给的档位必须保留,实得 %v", b3["reasoning_effort"])
|
||
}
|
||
|
||
// ④ 头:Accept 必须是 SSE
|
||
h, err := vm.BuildHeaders("buddy", map[string]interface{}{"api_key": "TOK"})
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if h["Accept"] != "text/event-stream" {
|
||
t.Errorf("Accept 必须是 text/event-stream(否则上游只回空帧),实得 %q", h["Accept"])
|
||
}
|
||
for _, k := range []string{"X-Domain", "X-Product", "X-Product-Code"} {
|
||
if h[k] == "" {
|
||
t.Errorf("缺少 buddy 身份头 %s", k)
|
||
}
|
||
}
|
||
t.Logf("✓ glm-5.1 补 effort;hy3 不补;Accept=%s", h["Accept"])
|
||
}
|
||
|
||
// 配对判据:协议转换必须与 openai 一致(buddy 只是补两个字段,不该改转换)。
|
||
func TestBuddyAdapterKeepsOpenAIWire(t *testing.T) {
|
||
vm := loadBuddyAdapter(t)
|
||
chunk := `{"id":"x","object":"chat.completion.chunk","created":1,"model":"m","choices":[{"index":0,"delta":{"content":"hi","reasoning_content":"think"},"finish_reason":null}]}`
|
||
out, err := vm.Transform("buddy", "transform_stream_chunk", chunk)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if !strings.Contains(out, `"content":"hi"`) || !strings.Contains(out, `"reasoning_content":"think"`) {
|
||
t.Errorf("流式转换必须保留 content 与 reasoning_content,实得 %s", out)
|
||
}
|
||
}
|
||
|
||
// ★ 回归护栏:适配器**必须**定义全部四个转换函数。
|
||
//
|
||
// 真实事故(2026-10-16):我第一版把基座继承写成"在适配器里读兄弟适配器源码",
|
||
// 而适配器运行在**另一个沙箱 VM**(无 __llmsproxy_plugin_dir、无文件系统),
|
||
// 于是基座加载**静默失败** ⇒ 适配器只有 transform_request,
|
||
// **缺 transform_stream_chunk** ⇒ 内核无法解析 SSE ⇒ 上游明明回了 39 帧正文,
|
||
// 网关却记 completion_tokens=0、客户端看到**空回复**。
|
||
// 更糟的是:hy3/glm-5.3/kimi-k3-1 全被它弄坏,而 glm-5.2 因多源兜底
|
||
// "看起来正常" ⇒ 症状指向错误方向。
|
||
//
|
||
// ⇒ 判据必须咬住「四个转换函数都存在」,而不是"某个模型能回话"
|
||
//
|
||
// (后者会被别的源兜底掩盖 —— 本会话已栽过两次)。
|
||
func TestBuddyAdapterDefinesAllTransforms(t *testing.T) {
|
||
vm := loadBuddyAdapter(t)
|
||
for _, fn := range []string{"transform_request", "transform_response", "transform_stream_chunk", "transform_error"} {
|
||
if !vm.HasFn("buddy", fn) {
|
||
t.Errorf("适配器缺 %s ⇒ 内核该路径会静默降级(流式会变成空回复)", fn)
|
||
}
|
||
}
|
||
t.Log("✓ 四个转换函数齐备")
|
||
}
|