Files
ModelRouter/internal/lua/adapter_prelude_test.go
JianFeeeee 741edfea98 feat(lua): 适配器共享前导 —— 消除 12680 字节的逐字重复
问题(实测证据,按钩子做字节级哈希分组):
  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 个适配器行为不变)。
2026-10-13 22:34:13 +08:00

129 lines
4.7 KiB
Go
Raw Permalink 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 (
"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)
}
}