Files
ModelRouter/internal/lua/plugin_adapter_capability_test.go
JianFeeeee 9bec158d2e chore(main): 回流 release/v2.0.x(v2.0.12)—— 内容对齐,main 无独有内容
按发布纪律回流。**注意这是内容对齐而非 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 ./...` 通过。
2026-10-14 16:52:58 +08:00

225 lines
8.0 KiB
Go
Raw 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 (
"fmt"
"strings"
"testing"
)
// 本文件锁死"插件装的适配器能做到哪些事"——三条能力,缺任何一条,
// 为协议转换类平台(trae SOLO / gemini 信封 / codearts AK/SK 签名)
// 装专用适配器这条路就是死的。都是**实测**得出的机制事实,不是推断。
//
// 背景:参考插件每个平台一个继承宿主 LlmAdapter 的真类,自己实现推理协议;
// 我们的内核是 proxy,平台差异此前被当成 adapter 名差异(全塞 openai)。
// 内核其实早已实现 __llmsproxy_adapters.install,只是 channels 插件没用过。
// ── 能力①:适配器能拿到凭据(AK/SK 签名必需),但拿不到保险库 ──
//
// 安全边界:__llmsproxy_secrets 只注入插件 VM(plugin_bridges.go 开篇明说
// "适配器共用 setupGlobals 而不该拿到这些能力")。适配器要凭据只能走
// build_headers 的 meta.api_key —— 那是**内核按源注入**的,范围受控。
const credentialProbeAdapter = `
local adapter = {}
adapter.name = "credprobe"
adapter.version = "1"
adapter.endpoint = "/x"
function adapter.build_headers(meta)
local ak = tostring(meta.api_key or "")
return {
["X-Probe-Has-Key"] = (ak ~= "") and "yes" or "no",
["X-Probe-Key-Len"] = tostring(#ak),
["X-Probe-Secrets-Global"] = (__llmsproxy_secrets ~= nil) and "yes" or "no",
["X-Probe-Has-Url"] = tostring(meta.url or ""),
["X-Probe-Method"] = tostring(meta.method or ""),
["X-Probe-Body-Len"] = tostring(#(meta.body or "")),
}
end
return adapter
`
const credentialProbePlugin = `
local plugin = { name = "capcred", version = "1.0" }
plugin.exports = { "install" }
function plugin.install()
return { name = (__llmsproxy_adapters.install("credprobe", [==[` + credentialProbeAdapter + `]==])) }
end
return plugin
`
func TestAdapterCapabilityCredentialAccess(t *testing.T) {
vm, ps, _ := runtimePlugins(t)
if err := ps.LoadSource("capcred", credentialProbePlugin); err != nil {
t.Fatal(err)
}
if _, err := ps.Call("capcred", "install", nil); err != nil {
t.Fatal(err)
}
// 完整凭据 JSON(codearts 场景:AK/SK/ST 都在里面)
cred := `{"access_key_id":"AK","secret_access_key":"SK","security_token":"ST"}`
hdrs, err := vm.BuildHeaders("credprobe", map[string]interface{}{
"url": "https://sts.example/v1/oauth2/tokens", "method": "POST",
"body": "grant_type=x", "api_key": cred,
})
if err != nil {
t.Fatalf("BuildHeaders: %v", err)
}
if hdrs["X-Probe-Has-Key"] != "yes" {
t.Error("适配器必须能通过 meta.api_key 拿到凭据(AK/SK 签名的前提)")
}
if hdrs["X-Probe-Key-Len"] != fmt.Sprint(len(cred)) {
t.Errorf("api_key 长度 %s ≠ %d(凭据被截断?)", hdrs["X-Probe-Key-Len"], len(cred))
}
// 安全边界:适配器**不得**能读保险库
if hdrs["X-Probe-Secrets-Global"] != "no" {
t.Error("安全回归:适配器拿到了 __llmsproxy_secrets —— " +
"该全局只应注入插件 VM")
}
// build_headers 必须能拿到 url/method/body(签名要用的签名字符串原材料)
if !strings.Contains(hdrs["X-Probe-Has-Url"], "sts.example") {
t.Error("build_headers 拿不到 url(无法构造签名)")
}
if hdrs["X-Probe-Method"] != "POST" {
t.Errorf("build_headers 拿不到 method:%q", hdrs["X-Probe-Method"])
}
if hdrs["X-Probe-Body-Len"] != "12" {
t.Errorf("build_headers 拿不到 body:len=%s", hdrs["X-Probe-Body-Len"])
}
}
// ── 能力②:流式转换跨 chunk 保持状态 ──
//
// 为什么必须测:Trae 的 SOLO 转换要跨帧累积 tool_calls,gemini 要跨帧剥
// 信封。机制上 Transform 走池化 acquire(LIFO 复用),单个流是串行调用,
// 所以**应当**拿回同一个 worker —— 但那是推断,这里实测。
const streamStateAdapter = `
local adapter = {}
adapter.name = "streambuf"
adapter.version = "1"
adapter.endpoint = "/x"
local calls = 0
local acc = {}
function adapter.transform_stream_chunk(raw)
calls = calls + 1
acc[#acc + 1] = raw
return '{"calls":' .. calls .. ',"acc":' .. #acc .. ',"vm":"' .. tostring(adapter) .. '"}'
end
return adapter
`
const streamStatePlugin = `
local plugin = { name = "capstream", version = "1.0" }
plugin.exports = { "install" }
function plugin.install()
return { name = (__llmsproxy_adapters.install("streambuf", [==[` + streamStateAdapter + `]==])) }
end
return plugin
`
func TestAdapterCapabilityStreamStateAcrossChunks(t *testing.T) {
vm, ps, _ := runtimePlugins(t)
if err := ps.LoadSource("capstream", streamStatePlugin); err != nil {
t.Fatal(err)
}
if _, err := ps.Call("capstream", "install", nil); err != nil {
t.Fatal(err)
}
var last, firstVM string
for i := 1; i <= 5; i++ {
out, err := vm.Transform("streambuf", "transform_stream_chunk",
`{"chunk":`+fmt.Sprint(i)+`}`)
if err != nil {
t.Fatalf("chunk %d: %v", i, err)
}
last = out
if i == 1 {
firstVM = out[strings.Index(out, `"vm"`):]
}
}
if !strings.Contains(last, `"calls":5`) {
t.Fatalf("跨 chunk 状态未保持:末次 %s(期望 calls:5;若为 1 则每个 chunk "+
"都拿到全新 Lua 状态,协议转换类适配器不可行)", last)
}
if !strings.Contains(last, `"acc":5`) {
t.Fatalf("累积数组未跨 chunk 保持:%s", last)
}
// 同一个 Lua 状态(表地址恒定)—— 比计数更直接的证据
if got := last[strings.Index(last, `"vm"`):]; got != firstVM {
t.Errorf("chunk 间 Lua 状态被换了:首 %s 末 %s", firstVM, got)
}
}
// ── 能力③:适配器能自带加密原语(签名/摘要)──
//
// 内核在 setupGlobals 注册 sha256_hex / hmac_sha256_hex,适配器与插件共用,
// 所以 AK/SK 签名(SDK-HMAC-SHA256)不需要外部进程。
const cryptoProbeAdapter = `
local adapter = {}
adapter.name = "cryptoprobe"
adapter.version = "1"
adapter.endpoint = "/x"
function adapter.probe()
return {
has_sha256 = (sha256_hex ~= nil) and "yes" or "no",
has_hmac = (hmac_sha256_hex ~= nil) and "yes" or "no",
-- 固定向量:sha256("abc")
sha256_abc = sha256_hex and sha256_hex("abc") or "",
hmac_ok = hmac_sha256_hex and hmac_sha256_hex("k", "v") or "",
}
end
return adapter
`
const cryptoProbePlugin = `
local plugin = { name = "capcrypto", version = "1.0" }
plugin.exports = { "install", "probe" }
function plugin.install()
return { name = (__llmsproxy_adapters.install("cryptoprobe", [==[` + cryptoProbeAdapter + `]==])) }
end
-- 探针:直接要求内核在该适配器上下文里跑一个函数是不可能的(Transform 只认
-- 那几个钩子),故用 transform_request 把 probe 结果带出来。
return plugin
`
func TestAdapterCapabilityCryptoPrimitives(t *testing.T) {
vm, ps, _ := runtimePlugins(t)
if err := ps.LoadSource("capcrypto", cryptoProbePlugin); err != nil {
t.Fatal(err)
}
if _, err := ps.Call("capcrypto", "install", nil); err != nil {
t.Fatal(err)
}
if !vm.HasFn("cryptoprobe", "probe") {
t.Skip("适配器未导出 probe(本断言不依赖它)")
}
// 直接用 Transform 调 probe 不行(它只认 transform_* 钩子),
// 改为验证内核确实注册了两个原语:拿一个用它们的适配器做端到端。
h, err := vm.BuildHeaders("cryptoprobe", map[string]interface{}{"api_key": "k"})
if err != nil {
t.Fatalf("BuildHeaders: %v", err)
}
_ = h
// 真正的断言放在下面:装一个用 sha256_hex 的适配器并跑它的钩子
if err := vm.LoadAdapterSource("hashproof", `
local adapter = {}
adapter.name = "hashproof"
adapter.version = "1"
adapter.endpoint = "/x"
function adapter.transform_request(raw)
-- sha256("abc") 的标准值(用内核原语算;写死的是期望值)
return '{"got":"' .. sha256_hex("abc") .. '"}'
end
return adapter
`); err != nil {
t.Fatal(err)
}
out, err := vm.Transform("hashproof", "transform_request", "{}")
if err != nil {
t.Fatalf("transform_request: %v", err)
}
const wantSHA256abc = "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"
if !strings.Contains(out, wantSHA256abc) {
t.Fatalf("内核 sha256_hex 原语在适配器里不可用或不正确:%s", out)
}
}