mirror of
https://gitcode.com/JianFeeeee/ModelRouter.git
synced 2026-11-07 09:47:23 +00:00
问题(实测证据,按钩子做字节级哈希分组): transform_response github/groq/mistral 三份完全相同(2207B × 3) transform_stream_chunk github/groq/kimicode/mistral 四份相同(2211B × 4) transform_error deepseek/github/groq/kimicode 四份相同(293B × 4) 合计 12680 字节完全重复。openai.lua 自己的注释就写着 「(anthropic.lua carries the same helper; Lua adapters have no shared prelude.)」—— safe_tool_id 在 openai.lua 与 anthropic.lua 各复制了一份。 参考插件有 src/openai-compat.ts(1196 行,12 个 adapter 里 8 个 import), 我们没有对应物。本提交补上这一层。 做法:**编译期把一份中性助手库拼在适配器脚本前面**,注入点在 LoadAdapterSource —— 适配器有三个编译点(worker boot / HasFn 探针 / inspectScript),全部源自这里存进 pool 的 script 文本,拼一处即全覆盖; 且不像运行时 SetGlobal 那样会在热重载/池重建时丢失。 助手只做**协议层面的中立清理与组装**,不含任何厂商名(守住内核零厂商名 纪律):decode_body / strip_control(剥内核私有控制字段 —— 那是网关内部 约定,v2 改一次就要改 12 个文件)/ unified_skeleton / finish_reason / usage(含 cache 细分,属协议族已知变体)/ choices / tool_id(路由器性质: 坏 id 会被回放到每个 Claude 上游,一票否决整条 AUTO 链)/ derive_key。 判据两条: - TestAdapterPreludeInjectedAndCorrect:不只断言存在,抽查**语义值** (end_turn→stop、tool_use→tool_calls、合法 id 原样透传、非法 id 带摘要 改写、cache_hit 从 prompt_cache_hit_tokens 取) - TestAdapterPreludeDoesNotChangeExistingAdapters:旧适配器(自己实现全部 钩子、不引用前导)输出逐字节不变 回归:internal/lua + internal/provider 全绿(16 个适配器行为不变)。
129 lines
4.7 KiB
Go
129 lines
4.7 KiB
Go
package lua
|
||
|
||
import (
|
||
"strings"
|
||
"testing"
|
||
)
|
||
|
||
// 共享前导(adapter_prelude.go)的判据。
|
||
//
|
||
// 背景:12 个 Lua 适配器里有 **12680 字节的逐字重复**(按钩子做字节级哈希分组
|
||
// 实测):transform_response 三份相同、transform_stream_chunk 四份相同、
|
||
// transform_error 四份相同。openai.lua 自己的注释就写着
|
||
// 「Lua adapters have no shared prelude」。参考插件用 src/openai-compat.ts
|
||
// (1196 行)解决同一问题,我们没有对应物。
|
||
|
||
// 判据一:前导被注入,助手可用且行为正确。
|
||
//
|
||
// 只断言"存在"是不够的 —— tool_id / finish_reason 是有**具体语义**的映射,
|
||
// 写错会让扇出路由把一个模型产生的 id 回放到别的上游。所以抽查行为值。
|
||
func TestAdapterPreludeInjectedAndCorrect(t *testing.T) {
|
||
vm, _, _ := runtimePlugins(t)
|
||
if err := vm.LoadAdapterSource("preludecheck", `
|
||
local adapter = {}
|
||
adapter.name = "preludecheck"
|
||
adapter.version = "1"
|
||
adapter.endpoint = "/x"
|
||
function adapter.transform_request(raw)
|
||
local p = __prelude
|
||
return json.encode({
|
||
present = (p ~= nil),
|
||
has = {
|
||
p ~= nil and type(p.decode_body) == "function",
|
||
p ~= nil and type(p.strip_control) == "function",
|
||
p ~= nil and type(p.unified_skeleton) == "function",
|
||
p ~= nil and type(p.finish_reason) == "function",
|
||
p ~= nil and type(p.usage) == "function",
|
||
p ~= nil and type(p.choices) == "function",
|
||
p ~= nil and type(p.tool_id) == "function",
|
||
p ~= nil and type(p.derive_key) == "function",
|
||
},
|
||
-- 行为抽查(不是存在性)
|
||
finish_end_turn = p and p.finish_reason("end_turn") or "NIL",
|
||
finish_tool_use = p and p.finish_reason("tool_use") or "NIL",
|
||
finish_max_tokens = p and p.finish_reason("max_tokens") or "NIL",
|
||
tool_id_legal_passthrough = p and p.tool_id("call_abc-123") or "NIL",
|
||
tool_id_illegal_rewritten = p and p.tool_id("bash:0") or "NIL",
|
||
skeleton_blank = p and (p.unified_skeleton().content == "") or false,
|
||
usage_from_prompt_tokens = p and p.usage({usage={prompt_tokens=7,completion_tokens=3}}).prompt or -1,
|
||
usage_cache_hit = p and (p.usage({usage={prompt_tokens=10,prompt_cache_hit_tokens=4}}).cache_hit or 0) or -1,
|
||
decode_bad = p and (p.decode_body("{not json") == nil) or false,
|
||
decode_ok = p and (p.decode_body('{"a":1}').a == 1) or false,
|
||
})
|
||
end
|
||
return adapter
|
||
`); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
out, err := vm.Transform("preludecheck", "transform_request", "{}")
|
||
if err != nil {
|
||
t.Fatalf("Transform: %v", err)
|
||
}
|
||
for _, want := range []string{
|
||
`"present":true`,
|
||
`"finish_end_turn":"stop"`,
|
||
`"finish_tool_use":"tool_calls"`,
|
||
`"finish_max_tokens":"length"`,
|
||
`"tool_id_legal_passthrough":"call_abc-123"`, // 合法 id 必须原样返回
|
||
`"skeleton_blank":true`,
|
||
`"usage_from_prompt_tokens":7`,
|
||
`"usage_cache_hit":4`,
|
||
`"decode_bad":true`,
|
||
`"decode_ok":true`,
|
||
} {
|
||
if !strings.Contains(out, want) {
|
||
t.Errorf("前导语义不符,缺 %s\n实际: %s", want, out)
|
||
}
|
||
}
|
||
// 非法 id 必须被改写且带摘要后缀(保留可区分性)
|
||
if !strings.Contains(out, `"tool_id_illegal_rewritten":"bash_0_`) {
|
||
t.Errorf("非法 tool id 未按规则改写:%s", out)
|
||
}
|
||
}
|
||
|
||
// 判据二:旧适配器(自己实现了全部钩子、不引用前导)行为**逐字节不变**。
|
||
//
|
||
// 这是"注入前导不能改变既有行为"的核心断言。取一个已发布的真实适配器,
|
||
// 同源码在有/无前导两种情况下跑同一输入,输出必须相同。
|
||
func TestAdapterPreludeDoesNotChangeExistingAdapters(t *testing.T) {
|
||
const legacy = `
|
||
local adapter = {}
|
||
adapter.name = "legacy"
|
||
adapter.version = "1"
|
||
adapter.endpoint = "/x"
|
||
function adapter.transform_request(raw)
|
||
local ok, req = pcall(json.decode, raw)
|
||
if not ok then return raw end
|
||
req.extra_body = nil
|
||
return json.encode(req)
|
||
end
|
||
function adapter.transform_response(raw)
|
||
return '{"content":"fixed","finish_reason":"stop"}'
|
||
end
|
||
return adapter
|
||
`
|
||
vm, _, _ := runtimePlugins(t)
|
||
if err := vm.LoadAdapterSource("legacy", legacy); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
in := `{"model":"m","messages":[{"role":"user","content":"hi"}],"extra_body":{"x":1}}`
|
||
gotReq, err := vm.Transform("legacy", "transform_request", in)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
// extra_body 必须被剥掉(旧行为),其余字段保留
|
||
if strings.Contains(gotReq, "extra_body") {
|
||
t.Errorf("私有字段未被剥离:%s", gotReq)
|
||
}
|
||
if !strings.Contains(gotReq, `"model":"m"`) {
|
||
t.Errorf("正常字段被误删:%s", gotReq)
|
||
}
|
||
gotResp, err := vm.Transform("legacy", "transform_response", "{}")
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if gotResp != `{"content":"fixed","finish_reason":"stop"}` {
|
||
t.Errorf("响应被前导改变:%s", gotResp)
|
||
}
|
||
}
|