mirror of
https://gitcode.com/JianFeeeee/ModelRouter.git
synced 2026-11-07 09:47:23 +00:00
test(adapters): 逐适配器两向能力断言 + AdapterIngress 访问器
用户要求:16 个适配器逐个验证两个方向(出向序列化 / 入向解析)重构后都没坏。
- 新增 VM.AdapterIngress(name):暴露适配器认领的入站路径,供运维面与测试查看。
- 新增 TestEveryBundledAdapterTwoDirections:对每个内置适配器断言
1) 上游向 transform_request 存在且可调用;
2) 出向 — 声明了 serialize_response 的必须可调用且产出合法 JSON/SSE,
未声明的确认走内核 OpenAI 兜底(两种都合法);
3) 入向 — 声明了 adapter.ingress 的必须同时实现 deserialize_request
(否则内核会让出给空解析器),实现了钩子却没声明 ingress 的也报错
(内核永远不会调用它)。
12 个内置适配器全部通过。
隔离实例实测(19 源 + 16 适配器):
- 启动无解密失败/崩溃,16 适配器全加载。
- 入向让出决定性实证:装入认领 /v1/messages 的 zzclaimer 并移除 anthropic 后,
畸形 body 被适配器解析成 model=ZZ-CLAIMER-WAS-HERE,内核据此路由得
404 model_not_found —— 证明内核确实让出(活实例复现,非仅单测)。
- 真实请求两向贯通:deepseek-v4-flash / sensenova-6.8-flash-lite 返回 200 真实响应;
其余源返回上游 502(配额/渠道),说明请求正确到达了各适配器的上游。
This commit is contained in:
94
internal/lua/adapter_two_directions_test.go
Normal file
94
internal/lua/adapter_two_directions_test.go
Normal file
@ -0,0 +1,94 @@
|
||||
package lua
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestEveryBundledAdapterTwoDirections 逐个验证每个内置适配器在**两个方向**上
|
||||
// 的声明与能力一致,且都能在真实 VM 上被调用而不报错。这是"重构后两个方向
|
||||
// 都没坏"的批量断言(用户要求:16 个适配器逐个验证两个方向)。
|
||||
//
|
||||
// 两个方向:
|
||||
//
|
||||
// 出向(egress):serialize_* 钩子 —— 统一格式 → 客户端协议。
|
||||
// 一个适配器要么声明了 serialize_*(接管),要么依赖内核 OpenAI 兜底。
|
||||
// 两种都合法;不合法的是"声明了钩子但调用即报错"。
|
||||
//
|
||||
// 入向(ingress):adapter.ingress + deserialize_request —— 客户端协议 → 统一格式。
|
||||
// 声明 ingress 的适配器必须同时实现 deserialize_request,否则内核会让出给
|
||||
// 一个空解析器(核心里 parseViaIngress 已挡住,此处再在适配器数据面钉一次)。
|
||||
//
|
||||
// 上游向(upstream):transform_request —— 统一格式 → 上游协议。
|
||||
// 这是所有适配器的本职,必须存在且可调用。
|
||||
func TestEveryBundledAdapterTwoDirections(t *testing.T) {
|
||||
vm := NewVM(filepath.Join(t.TempDir(), "adapters"))
|
||||
if err := vm.Start(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer vm.Stop()
|
||||
|
||||
adapters := vm.ListAdapters()
|
||||
if len(adapters) == 0 {
|
||||
t.Fatal("no adapters loaded")
|
||||
}
|
||||
|
||||
// 一个最小、协议中立的统一请求,供上游向 transform_request 使用。
|
||||
unifiedReq := `{"model":"m","messages":[{"role":"user","content":"hi"}],"max_tokens":16,"stream":false}`
|
||||
|
||||
for _, a := range adapters {
|
||||
a := a
|
||||
t.Run(a.Name, func(t *testing.T) {
|
||||
// ---- 上游向:transform_request 必须存在且可调用 ----
|
||||
if !vm.HasFn(a.Name, "transform_request") {
|
||||
t.Errorf("%s: 缺 transform_request(上游向本职)", a.Name)
|
||||
} else if _, err := vm.Transform(a.Name, "transform_request", unifiedReq); err != nil {
|
||||
t.Errorf("%s: transform_request 调用失败: %v", a.Name, err)
|
||||
}
|
||||
|
||||
// ---- 出向:serialize_* 声明与调用一致性 ----
|
||||
declaredEgress := false
|
||||
for _, fn := range []string{"serialize_response", "serialize_chunk", "serialize_open_preamble", "serialize_close", "serialize_done_marker"} {
|
||||
if vm.HasFn(a.Name, fn) {
|
||||
declaredEgress = true
|
||||
}
|
||||
}
|
||||
if vm.HasFn(a.Name, "serialize_response") {
|
||||
// 统一结果形状(见 types.UnifiedResult)
|
||||
res := `{"model":"m","id":"x","content":"hello","finish_reason":"stop","token_usage":{"prompt":1,"completion":1,"total":2}}`
|
||||
out, err := vm.Transform(a.Name, "serialize_response", res)
|
||||
if err != nil {
|
||||
t.Errorf("%s: serialize_response 调用失败: %v", a.Name, err)
|
||||
} else if strings.TrimSpace(out) == "" {
|
||||
t.Errorf("%s: serialize_response 返回空", a.Name)
|
||||
} else if !json.Valid([]byte(out)) && !strings.HasPrefix(strings.TrimSpace(out), "event:") {
|
||||
t.Errorf("%s: serialize_response 既非 JSON 也非 SSE: %q", a.Name, out)
|
||||
}
|
||||
}
|
||||
if declaredEgress {
|
||||
t.Logf("%s: 出向由适配器接管", a.Name)
|
||||
} else {
|
||||
t.Logf("%s: 出向走内核 OpenAI 兜底(未声明 serialize_*)", a.Name)
|
||||
}
|
||||
|
||||
// ---- 入向:声明 ingress 必须配 deserialize_request ----
|
||||
ingress := vm.AdapterIngress(a.Name)
|
||||
hasDeser := vm.HasFn(a.Name, "deserialize_request")
|
||||
if ingress != "" {
|
||||
if !hasDeser {
|
||||
t.Errorf("%s: 声明 ingress=%q 但没有 deserialize_request —— 内核会让出给空解析器", a.Name, ingress)
|
||||
} else {
|
||||
// 用该适配器自己服务的那个端点的典型 body 调一次,确认不报错。
|
||||
if _, err := vm.Transform(a.Name, "deserialize_request", `{"model":"m","max_tokens":8,"messages":[{"role":"user","content":"hi"}]}`); err != nil {
|
||||
t.Errorf("%s: deserialize_request 调用失败: %v", a.Name, err)
|
||||
}
|
||||
t.Logf("%s: 入向认领 %s", a.Name, ingress)
|
||||
}
|
||||
} else if hasDeser {
|
||||
t.Errorf("%s: 实现了 deserialize_request 却没声明 ingress —— 内核永远不会调用它", a.Name)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -1621,6 +1621,20 @@ func (v *VM) AdapterVersion(name string) string {
|
||||
return p.static.version
|
||||
}
|
||||
|
||||
// AdapterIngress returns the client-facing endpoint path an adapter CLAIMS
|
||||
// (its static.ingress), or "" when it claims none. Exported so operator-facing
|
||||
// surfaces (and tests) can see which adapter, if any, has taken over parsing of
|
||||
// an inbound endpoint — the same declaration AdapterForIngress consults.
|
||||
func (v *VM) AdapterIngress(name string) string {
|
||||
p := v.pool(name)
|
||||
if p == nil {
|
||||
return ""
|
||||
}
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
return p.static.ingress
|
||||
}
|
||||
|
||||
// EffectiveAdapterName resolves the name an adapter is actually registered
|
||||
// under, given the key it was loaded with.
|
||||
//
|
||||
|
||||
Reference in New Issue
Block a user