mirror of
https://gitcode.com/JianFeeeee/ModelRouter.git
synced 2026-11-07 09:47:23 +00:00
补上两个此前报告为"缺口"的东西。其中第一个的旧结论是**错的**,这里更正。 ## R4 Lua 绑定:__llmsproxy_adapters 用户问的"为什么 Lua 绑不上"—— 结论是**能绑,之前的归因错了**。 旧结论是「NewTable + PushGoClosure + SetField 是 golua 雷区,会 SIGSEGV」。 反证:内核自己的 buildJSONTable 用的就是这同一个模式,长期稳定。真正的变量 是当时 Phase 4 的**双 VM 半成品**(钩子一个 VM、定时任务一个私有 VM), bootPluginState 签名与调用点不一致,我改到一半编译不过就仓促回滚,留下的 中间态让 billing 一跑就崩。把双 VM 改回单 VM 之后,同一模式一次就通。 顺带回答一个备选方案(用户提出:Lua 只投递请求、Go 串行执行):不需要。 p.mu 已经天然串行化同一插件的所有入口;再加队列只会引入"谁排空/超时后积压 怎么算"的新问题。而且桥接调用本身是微秒级的(交给 Go 重建一个小池),真正 长耗时的是插件自己去取 token 的 HTTP 请求,那跟桥接无关。 能力(Go 侧强制,非约定): - install(name, source) → (生效名, nil) | (nil, err) - update(name, source) → (true, nil) | (nil, err) 只限自己装的 - remove(name) → (true, nil) | (nil, err) 只限自己装的 - 调用身份是插件**启动时的固定名**,不从 Lua 状态现读 ⇒ 无法冒充别的插件 - 不能影子内置(Lua 侧同样被拒,不是绕过 Go 校验的后门) - 出错返回 (nil, err) 而非抛异常,与钩子/HTTP 处理器同一套约定 **分片 VM 故意不给**:分片是文档化的「只读、可丢弃」工作单元,而适配器 install 是**全局**副作用(改共享注册表)。给分片等于让"只读"的按需调用悄悄 改写所有请求共用的适配器 —— 正是分片契约要防的不可见变更。 测试 6 个:install/rotate/remove 从 Lua 调用、影子内置被拒、**定时任务里 轮换 token 且请求钩子立即可见**(R4 闭环)、分片无桥接。 ## 真二进制 e2e:适配器接手对外服务 用户问到的第二个点。此前 /v1/messages 的 Anthropic 接管只在**进程内** gateway 层测过(internal/gateway/messages_test.go),绕过了只会在真实进程里 发生的事:适配器磁盘 seed、配置加载、真 HTTP 栈。 新增 e2e/adapter_takeover_test.go,打**真编译的二进制** + 只说 Anthropic 的 假上游(该上游对不符形状的请求主动报错,不做宽容接受): - 非流式:出向必须是 Anthropic message 形状,不能有 OpenAI choices 外壳 - 流式:必须是 message_start…message_stop 序列,**不得**出现 [DONE] - 压力:96 并发(48 流式)混合打真二进制,全部 200,无 5xx,无形状串味 压力测试初版把并发上限设成 4 于是大量 429,我把断言改正为:429 是内核**正确** 的容量背压,不算失败;失败的定义是 5xx 或 200 但内容/形状错。现在 96/96 通过。
235 lines
8.5 KiB
Go
235 lines
8.5 KiB
Go
package lua
|
||
|
||
import (
|
||
"testing"
|
||
"time"
|
||
)
|
||
|
||
// 本文件验证 R4 的 Lua 侧绑定:**插件自己的 Lua 代码**能在运行时安装/改写/
|
||
// 删除自己的适配器。
|
||
//
|
||
// 为什么需要单独一层测试:Go 侧的 InstallAdapter/UpdateAdapter 在
|
||
// plugin_adapter_bridge_test.go 里已直接验证过,但那证明的是"Go API 能工作"。
|
||
// token 轮换的真实形态是插件在定时任务或钩子里**从 Lua 发起**这个调用,所以
|
||
// 绑定本身(__llmsproxy_adapters 表)必须被证明可用 —— 而且它曾经在 cgo 层
|
||
// SIGSEGV,所以"不崩"本身就是一条断言。
|
||
|
||
const bridgeProbe = `
|
||
local plugin = { name = "authtoken", version = "1.0" }
|
||
plugin.exports = { "install_via_lua", "update_via_lua", "remove_via_lua", "install_operator_owned" }
|
||
|
||
local function src(ver, tok)
|
||
return 'return { name = "myauth", version = "' .. ver .. '", endpoint = "/x", token = "' .. tok .. '" }'
|
||
end
|
||
|
||
function plugin.install_via_lua(args)
|
||
local name, err = __llmsproxy_adapters.install("myauth", src("1.0", args.tok))
|
||
return { name = name, err = err }
|
||
end
|
||
|
||
function plugin.update_via_lua(args)
|
||
local ok, err = __llmsproxy_adapters.update("myauth", src("2.0", args.tok))
|
||
return { ok = ok, err = err }
|
||
end
|
||
|
||
function plugin.remove_via_lua()
|
||
local ok, err = __llmsproxy_adapters.remove("myauth")
|
||
return { ok = ok, err = err }
|
||
end
|
||
|
||
-- 试图替换一个非自己拥有的适配器:必须被拒(不能影子内置)
|
||
function plugin.install_operator_owned()
|
||
local name, err = __llmsproxy_adapters.install("openai", src("9.9", "evil"))
|
||
return { name = name, err = err }
|
||
end
|
||
return plugin
|
||
`
|
||
|
||
// TestPluginInstallsAdapterFromLua 是 R4 绑定的主断言。
|
||
func TestPluginInstallsAdapterFromLua(t *testing.T) {
|
||
_, ps, _ := runtimePlugins(t)
|
||
if err := ps.LoadSource("authtoken", bridgeProbe); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
out, err := ps.Call("authtoken", "install_via_lua", map[string]interface{}{"tok": "T1"})
|
||
if err != nil {
|
||
t.Fatalf("Lua 侧 install 调用失败: %v", err)
|
||
}
|
||
m, ok := out.(map[string]interface{})
|
||
if !ok {
|
||
t.Fatalf("返回值不是 table: %#v", out)
|
||
}
|
||
if m["name"] != "myauth" {
|
||
t.Fatalf("install 应返回生效名 myauth, 实得 %#v (err=%#v)", m["name"], m["err"])
|
||
}
|
||
// 归属确实被记录(这条规则是"插件只能改自己装的"的基础)
|
||
if owner := ps.AdapterOwnerOf("myauth"); owner != "authtoken" {
|
||
t.Errorf("归属应为 authtoken, 实得 %q", owner)
|
||
}
|
||
}
|
||
|
||
// TestPluginRotatesTokenViaLua 是 token 轮换的完整形态:
|
||
// 同一个插件先装、再改,改后新版本生效。
|
||
func TestPluginRotatesTokenViaLua(t *testing.T) {
|
||
_, ps, _ := runtimePlugins(t)
|
||
if err := ps.LoadSource("authtoken", bridgeProbe); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if _, err := ps.Call("authtoken", "install_via_lua", map[string]interface{}{"tok": "OLD"}); err != nil {
|
||
t.Fatalf("install: %v", err)
|
||
}
|
||
// 轮换
|
||
out, err := ps.Call("authtoken", "update_via_lua", map[string]interface{}{"tok": "NEW"})
|
||
if err != nil {
|
||
t.Fatalf("update: %v", err)
|
||
}
|
||
m := out.(map[string]interface{})
|
||
if m["ok"] != true {
|
||
t.Fatalf("Lua 侧 update 未成功: %#v", m)
|
||
}
|
||
// 新代码必须真的生效 —— 这是"轮换有意义"的定义
|
||
if rev := ps.vm.AdapterRevision("myauth"); rev < 2 {
|
||
t.Errorf("适配器修订号应 >=2(已被替换一次),实得 %d", rev)
|
||
}
|
||
}
|
||
|
||
// TestPluginRemoveAdapterFromLua:插件能删掉自己装的。
|
||
func TestPluginRemoveAdapterFromLua(t *testing.T) {
|
||
_, ps, _ := runtimePlugins(t)
|
||
if err := ps.LoadSource("authtoken", bridgeProbe); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if _, err := ps.Call("authtoken", "install_via_lua", map[string]interface{}{"tok": "T"}); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
out, err := ps.Call("authtoken", "remove_via_lua", map[string]interface{}{})
|
||
if err != nil {
|
||
t.Fatalf("remove: %v", err)
|
||
}
|
||
if out.(map[string]interface{})["ok"] != true {
|
||
t.Fatalf("Lua 侧 remove 未成功: %#v", out)
|
||
}
|
||
if owner := ps.AdapterOwnerOf("myauth"); owner != "" {
|
||
t.Errorf("删除后归属应清空, 实得 %q", owner)
|
||
}
|
||
}
|
||
|
||
// TestLuaBridgeCannotShadowBuiltin:从 Lua 侧同样不能影子内置适配器 ——
|
||
// 否则绑定就成了绕过 Go 侧校验的后门。
|
||
func TestLuaBridgeCannotShadowBuiltin(t *testing.T) {
|
||
_, ps, _ := runtimePlugins(t)
|
||
if err := ps.LoadSource("authtoken", bridgeProbe); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
out, err := ps.Call("authtoken", "install_operator_owned", map[string]interface{}{})
|
||
if err != nil {
|
||
t.Fatalf("调用本身不应报错(错误应作为返回值): %v", err)
|
||
}
|
||
m := out.(map[string]interface{})
|
||
if m["name"] != nil {
|
||
t.Fatalf("影子内置 openai 必须被拒,却返回了 name=%#v", m["name"])
|
||
}
|
||
if m["err"] == nil || m["err"] == "" {
|
||
t.Fatal("影子内置应带回错误信息")
|
||
}
|
||
t.Logf("拒绝信息: %v", m["err"])
|
||
}
|
||
|
||
// TestScheduledTaskRotatesTokenEndToEnd 是 R4 的闭环验收。
|
||
//
|
||
// 它把三个特性串成一条链,任何一个环节坏掉都会让它失败:
|
||
// 1. 内核 ticker 驱动定时任务(无外部触发)
|
||
// 2. 任务与钩子跑在同一个 VM(所以任务的 install 与 state 写入对钩子可见)
|
||
// 3. Lua 侧适配器桥接可用(__llmsproxy_adapters.install)
|
||
//
|
||
// 这正是用户描述的 token 轮换形态:插件自己按时间拿到新 token,重写适配器,
|
||
// 后续请求自然使用新逻辑 —— 全程没有任何浏览器或人工操作。
|
||
func TestScheduledTaskRotatesTokenEndToEnd(t *testing.T) {
|
||
shortTick(t)
|
||
_, ps, _ := runtimePlugins(t)
|
||
code := `
|
||
local plugin = { name = "sched_rot", version = "1.0" }
|
||
plugin.state = { token = "OLD", rotates = 0 }
|
||
plugin.schedule = { { fn = "rotate", every = 1 } }
|
||
function plugin.rotate()
|
||
plugin.state.rotates = plugin.state.rotates + 1
|
||
local tok = "T" .. tostring(plugin.state.rotates)
|
||
local src = 'return { name = "rotauth", version = "1.0", endpoint = "/x", token = "' .. tok .. '" }'
|
||
plugin.state.last_adapter, plugin.state.rotate_err = __llmsproxy_adapters.install("rotauth", src)
|
||
plugin.state.token = tok
|
||
end
|
||
plugin.hooks = { routed = "observe" }
|
||
function plugin.observe(payload)
|
||
payload.token = plugin.state.token
|
||
payload.rotates = plugin.state.rotates
|
||
payload.adapter = plugin.state.last_adapter
|
||
return payload
|
||
end
|
||
return plugin
|
||
`
|
||
if err := ps.LoadSource("sched_rot", code); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
deadline := time.Now().Add(4 * time.Second)
|
||
for time.Now().Before(deadline) {
|
||
out := ps.Fire(StageRouted, map[string]interface{}{})
|
||
if n, ok := out["rotates"].(float64); ok && n >= 1 {
|
||
t.Logf("任务轮换了 %v 次; 钩子看到 token=%v adapter=%v", n, out["token"], out["adapter"])
|
||
if out["adapter"] != "rotauth" {
|
||
t.Fatalf("任务里 install 未生效: adapter=%v", out["adapter"])
|
||
}
|
||
return
|
||
}
|
||
time.Sleep(30 * time.Millisecond)
|
||
}
|
||
t.Fatal("定时任务未完成 token 轮换")
|
||
}
|
||
|
||
// TestShardVMHasNoAdapterBridge 锁定一条边界:分片 VM(ShardCall 用的私有 VM)
|
||
// **不能**拿到适配器管理桥接。
|
||
//
|
||
// 理由:分片是文档化的「只读、可丢弃」工作单元,而 install/update/remove 是
|
||
// **全局**副作用(改的是共享的适配器注册表,不是本 VM 的状态)。若把桥接给
|
||
// 分片,一个被声明为只读的按需调用就能悄悄改写所有请求共用的适配器 ——
|
||
// 正是分片契约要防的那类不可见变更。
|
||
//
|
||
// 断言方式是:分片里的插件代码检查 __llmsproxy_adapters 是否为 nil,返回结果。
|
||
func TestShardVMHasNoAdapterBridge(t *testing.T) {
|
||
_, ps, _ := runtimePlugins(t)
|
||
code := `
|
||
local plugin = { name = "sharded", version = "1.0" }
|
||
plugin.exports = { "probe" }
|
||
-- 只读探测:分片里这个全局必须是 nil
|
||
function plugin.probe(args)
|
||
return { has_bridge = __llmsproxy_adapters ~= nil }
|
||
end
|
||
return plugin
|
||
`
|
||
if err := ps.LoadSource("sharded", code); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
// 主 VM 上应当有桥接(对照)
|
||
mainOut, err := ps.Call("sharded", "probe", map[string]interface{}{})
|
||
if err != nil {
|
||
t.Fatalf("主 VM 调用失败: %v", err)
|
||
}
|
||
if m, ok := mainOut.(map[string]interface{}); !ok || m["has_bridge"] != true {
|
||
t.Errorf("主 VM 上应有 __llmsproxy_adapters, 实得 %#v", mainOut)
|
||
}
|
||
|
||
// 分片 VM 上不应有
|
||
outs, err := ps.ShardCall("sharded", "probe", []map[string]interface{}{{}, {}})
|
||
if err != nil {
|
||
t.Fatalf("ShardCall: %v", err)
|
||
}
|
||
for i, o := range outs {
|
||
m, ok := o.(map[string]interface{})
|
||
if !ok {
|
||
t.Fatalf("分片 %d 返回非 table: %#v", i, o)
|
||
}
|
||
if m["has_bridge"] == true {
|
||
t.Errorf("分片 %d 不应拿到适配器桥接(分片是只读可丢弃的)", i)
|
||
}
|
||
}
|
||
}
|