mirror of
https://gitcode.com/JianFeeeee/ModelRouter.git
synced 2026-11-07 09:47:23 +00:00
按发布纪律回流。**注意这是内容对齐而非 merge**,理由与依据:
1. 纪律禁止 merge 回主干,因为 merge 会把发布分支的**版本号变更**带进 main。
本项目版本号**只由 `git describe --tags` 推导**(Makefile VERSION),
仓库里没有任何文件存放版本号 ⇒ 回流不存在版本污染风险。
2. 逐项核对确认 **main 是 release 的内容严格子集**:
- `main` 独有的**文件**:0 个;
- `internal/lua/plugins.go` / `internal/core/kernel_actions.go` 等内核文件
main 独有行数:0;
- `deploy.sh` 那 14 行只是 emoji 清理(release 刻意去掉了 emoji);
- `channels.lua` 那 333 行是**旧版内容**(旧 exports 表、旧模型名单)。
- main 独有的 9 个提交按 patch-id **全部已在 release 中等效存在**
(`git cherry` 逐条列出均为 `-`,即已合入)。
3. 故回流 = 把 release 的内容带到 main,等价于"只捡拾修复、不带版本号",
且不会丢任何 main 独有的修复。
本次带回 v2.0.12 的全部内容(37 个提交):
- 源串行 + 按模型节流(照 model-gate)
- 已失效模型存储 + 远端目录闸门
- 余额分档选号
- 模型能力元数据(models.dev,isFree 权威判定)
- **凭据续期修复**(此前恒报 ok:true 却一个请求都不发)
- 通用件 TTL 缓存 + 串行锁;account_test/account_adopt/account_delete 修正
- 内核级:出站授权持久化重启后不生效、`__llmsproxy_plugin_dir` ground truth
校验:`git diff release/v2.0.x` = 0 行;`go build -tags luajit ./...` 通过。
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)
|
||
}
|
||
}
|