mirror of
https://gitcode.com/JianFeeeee/ModelRouter.git
synced 2026-11-07 09:47:23 +00:00
通读参考 zcode-identity.ts 的 withContextPrefix 后发现我的实现漏了两条官方行为, 且其中一条藏着真实 bug: ## ① ★ 幂等判断 off-by-one(真 bug) `"<system-reminder>"` 是 **17** 个字符,而我写的是 `c:sub(1, 16) == SR` —— 判断**永远为假** ⇒ 幂等完全失效:同一请求重试/多次 transform 时日期块 会叠加成多层,改变 content 的前缀结构(上游检查的正是这个结构)。 修法:用 `#SR` 算长度,不写死数字。 ## ② 只看**第一条**消息,且它必须 role == "user" 我原来实现成「往后扫第一个 user 消息」。官方行为是:只处理 `messages[0]`, 且若它不是 user 就**完全不动** —— 不是去找后面那条 user。已对齐。 ## ③ 其它类型兜底 参考对非字符串/非数组的 content 也会置为 `[prefix]`,照做。 判据 2 条(新增): - TestZcodeContextPrefixIsIdempotent 已含日期块时不重复插 - TestZcodeContextPrefixOnlyTouchesFirstMessage 首条非 user 时不动、首条 user 时插入 变异 2/2 咬住:把 #SR 改回写死的 16 → 报红;去掉幂等判断 → 报红。 ★ 我自己在写这条判据时又犯了一个错并当场自查出来:断言「文本 content 应保持 字符串」,而 anthropic 基座把文本转成块数组是**正常**的协议转换行为 —— 差点把 正确的实现判成错。判据要验的是「**日期块**有没有被插进来」这个事实,不是 content 的容器类型。 验证:internal/core 335s + internal/gateway 207s 全绿。
405 lines
15 KiB
Go
405 lines
15 KiB
Go
package core
|
||
|
||
import (
|
||
"encoding/json"
|
||
"os"
|
||
"path/filepath"
|
||
"strings"
|
||
"testing"
|
||
|
||
"llmsproxy/internal/lua"
|
||
)
|
||
|
||
// ★ ZCode 的身份块是 3012 准入的**唯一开关**。
|
||
//
|
||
// 参考实测(三个账号、17 次请求):system 缺官方身份块结构时,上游直接
|
||
//
|
||
// HTTP 405 + {"code":3012,"msg":"...unusual activity."}
|
||
//
|
||
// 且与 HTTP 头、版本号、请求频率、多轮历史、tools 声明、运行时**全部无关**。
|
||
//
|
||
// 判据矩阵(照参考,逐条锁定):
|
||
//
|
||
// 无 system → 3012
|
||
// 仅 cliPrefix(42 字符) → 3012
|
||
// cliPrefix + stable(约 2898 字符)→ 200
|
||
//
|
||
// ⇒ 判据是「身份块**结构**是否存在」,不是字符数凑够。
|
||
//
|
||
// 本判据看**适配器真正产出给上游的请求体**(transform_request 的返回值),
|
||
// 而不是"适配器装了没"这类内部信号。
|
||
func TestZcodeAdapterInjectsOfficialIdentityBlocks(t *testing.T) {
|
||
h := newZcodeIdentityHarness(t)
|
||
|
||
out, err := h.transformRequest(`{"model":"glm-5.3-flash","messages":[{"role":"user","content":"hi"}]}`)
|
||
if err != nil {
|
||
t.Fatalf("transform_request: %v", err)
|
||
}
|
||
var req map[string]interface{}
|
||
if err := json.Unmarshal([]byte(out), &req); err != nil {
|
||
t.Fatalf("产出不是 JSON: %v\n%s", err, out)
|
||
}
|
||
|
||
// ① system 必须是**块数组**(官方形态),不是纯字符串
|
||
sys, ok := req["system"].([]interface{})
|
||
if !ok {
|
||
t.Fatalf("system 必须是块数组(上游检查的是这个结构),实得 %T: %v",
|
||
req["system"], req["system"])
|
||
}
|
||
if len(sys) < 3 {
|
||
t.Fatalf("system 块数至少 3(cliPrefix + stable + Environment),实得 %d", len(sys))
|
||
}
|
||
|
||
// ② block[0] 必须是官方 CLI 前缀(准入必需)
|
||
b0, _ := sys[0].(map[string]interface{})
|
||
if got, _ := b0["text"].(string); got != "You are ZCode, an interactive coding agent" {
|
||
t.Fatalf("block[0] 必须是官方 CLI 前缀(逐字),实得 %q", got)
|
||
}
|
||
|
||
// ③ block[1] 必须是官方 stable 段(准入必需),长度对得上
|
||
b1, _ := sys[1].(map[string]interface{})
|
||
stable, _ := b1["text"].(string)
|
||
if len(stable) < 2800 {
|
||
t.Fatalf("block[1](stable 段)长度 %d < 2800 —— 身份块不完整会被判为裸请求(3012)",
|
||
len(stable))
|
||
}
|
||
for _, marker := range []string{
|
||
"You are an interactive ZCode agent",
|
||
"# Harness",
|
||
"# ZCode Desktop Context",
|
||
} {
|
||
if !strings.Contains(stable, marker) {
|
||
t.Fatalf("stable 段缺 %q —— 身份块被截断/改写", marker)
|
||
}
|
||
}
|
||
|
||
// ④ block[2] 是 Environment 段(官方每会话都发;不发则模型不知 cwd/平台)
|
||
b2, _ := sys[2].(map[string]interface{})
|
||
env, _ := b2["text"].(string)
|
||
if !strings.HasPrefix(env, "# Environment") {
|
||
t.Fatalf("block[2] 必须是 Environment 段,实得 %q", env[:zcodeMin(40, len(env))])
|
||
}
|
||
for _, marker := range []string{"Primary working directory", "Platform", "powered by the model named"} {
|
||
if !strings.Contains(env, marker) {
|
||
t.Fatalf("Environment 段缺 %q:%s", marker, env)
|
||
}
|
||
}
|
||
|
||
// ⑤ 总长度必须达到参考声明的量级(2898)
|
||
total := len("You are ZCode, an interactive coding agent") + len(stable)
|
||
if total < 2800 {
|
||
t.Fatalf("身份块合计 %d 字符 < 2800(参考实测的准入值约 2898)", total)
|
||
}
|
||
|
||
// ⑥ 日期块:插到第一条 user 消息的 content **数组**最前(官方形态)
|
||
msgs, _ := req["messages"].([]interface{})
|
||
if len(msgs) == 0 {
|
||
t.Fatal("messages 为空")
|
||
}
|
||
m0, _ := msgs[0].(map[string]interface{})
|
||
content, ok := m0["content"].([]interface{})
|
||
if !ok {
|
||
t.Fatalf("首条 user 的 content 必须是**块数组**(拼进字符串会改变结构、仍被判裸请求),实得 %T",
|
||
m0["content"])
|
||
}
|
||
c0, _ := content[0].(map[string]interface{})
|
||
if got, _ := c0["text"].(string); !strings.Contains(got, "<system-reminder>") {
|
||
t.Fatalf("content[0] 必须是日期块(<system-reminder>),实得 %q", got)
|
||
}
|
||
|
||
t.Logf("✓ 身份块注入正确:块数=%d stable=%d 合计=%d", len(sys), len(stable), total)
|
||
}
|
||
|
||
// ★ 调用方的 system 必须**追加在最后**:上游检查看的是**前缀结构**,
|
||
// 官方身份块必须处在开头位置。若被前置会让身份块不在前缀而吃 3012。
|
||
func TestZcodeCallerSystemIsAppendedLast(t *testing.T) {
|
||
h := newZcodeIdentityHarness(t)
|
||
out, err := h.transformRequest(
|
||
`{"model":"glm-5.3-flash","messages":[{"role":"system","content":"CALLER_PROMPT_XYZ"},{"role":"user","content":"hi"}]}`)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
var req map[string]interface{}
|
||
if err := json.Unmarshal([]byte(out), &req); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
sys, _ := req["system"].([]interface{})
|
||
if len(sys) < 4 {
|
||
t.Fatalf("带调用方 system 时应为 4 块(3 官方 + 1 调用方),实得 %d", len(sys))
|
||
}
|
||
last, _ := sys[len(sys)-1].(map[string]interface{})
|
||
lt, _ := last["text"].(string)
|
||
if !strings.Contains(lt, "CALLER_PROMPT_XYZ") {
|
||
t.Fatalf("调用方 system 必须在**最后一块**(上游看前缀结构;前置会让身份块不在前缀)")
|
||
}
|
||
// 且前三块仍是官方块(身份块没有被调用方内容挤走)
|
||
b0, _ := sys[0].(map[string]interface{})
|
||
if got, _ := b0["text"].(string); got != "You are ZCode, an interactive coding agent" {
|
||
t.Fatalf("block[0] 必须仍是官方 CLI 前缀,实得 %q", got)
|
||
}
|
||
}
|
||
|
||
// 适配器自检出口:身份块长度与 anthropic 基座装载状态。
|
||
// 「装了但身份块是空的」这类问题(转义/截断/基座读不到)没有它只能靠猜。
|
||
func TestZcodeAdapterProbeReportsIdentityLength(t *testing.T) {
|
||
h := newZcodeIdentityHarness(t)
|
||
res, err := h.ps.Call("channels", "__adapter_install_probe", map[string]interface{}{})
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
j := res.(map[string]interface{})["json"].(map[string]interface{})
|
||
zc, ok := j["zcode"].(map[string]interface{})
|
||
if !ok {
|
||
t.Fatalf("探针未报告 zcode 适配器装载结果: %v", j)
|
||
}
|
||
if zc["ok"] != true {
|
||
t.Fatalf("zcode 适配器装载失败: %v", zc["err"])
|
||
}
|
||
}
|
||
|
||
// zcodeIdentityHarness:加载插件(它会**自装** channels-zcode 适配器),
|
||
// 然后直接调那个适配器的 transform_request。
|
||
//
|
||
// 直接调已装载的适配器(而不是自己去 load 源码)更接近真实路径:判据验的是
|
||
// 「上游实际收到的请求体」,包含前导注入、桥的装载等全部环节。
|
||
type zcodeIdentityHarness struct {
|
||
t *testing.T
|
||
c *Core
|
||
ps *lua.Plugins
|
||
}
|
||
|
||
func newZcodeIdentityHarness(t *testing.T) *zcodeIdentityHarness {
|
||
t.Helper()
|
||
c := kernelTestCore(t)
|
||
ps := c.Plugins()
|
||
src, err := readBundledChannels(t)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if err := ps.LoadSource("channels", src); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
return &zcodeIdentityHarness{t: t, c: c, ps: ps}
|
||
}
|
||
|
||
// transformRequest 调**已自装的** channels-zcode 适配器的 transform_request。
|
||
func (h *zcodeIdentityHarness) transformRequest(body string) (string, error) {
|
||
h.t.Helper()
|
||
vm := h.c.VM()
|
||
if vm == nil {
|
||
h.t.Fatal("VM 不可用")
|
||
}
|
||
return vm.Transform("channels-zcode", "transform_request", body)
|
||
}
|
||
|
||
func zcodeMin(a, b int) int {
|
||
if a < b {
|
||
return a
|
||
}
|
||
return b
|
||
}
|
||
|
||
// ★ 身份块注入不得破坏协议转换:工具调用、图像、流式帧仍必须走 anthropic 那一份。
|
||
//
|
||
// 为什么必须验:本适配器的手法是把 anthropic.lua 的源码读进来执行、只覆盖
|
||
// transform_request。若「读不到基座」或「覆盖时丢了字段」,协议转换会静默退化
|
||
// —— 表现为工具调用参数丢失、图像块消失,而身份块看起来完全正常。
|
||
func TestZcodeAdapterKeepsAnthropicProtocolConversion(t *testing.T) {
|
||
h := newZcodeIdentityHarness(t)
|
||
|
||
t.Run("工具定义与工具调用存活", func(t *testing.T) {
|
||
in := `{"model":"glm-5.3-flash","messages":[{"role":"user","content":"go"}],` +
|
||
`"tools":[{"type":"function","function":{"name":"get_weather",` +
|
||
`"description":"d","parameters":{"type":"object","properties":{"c":{"type":"string"}}}}}]}`
|
||
out, err := h.transformRequest(in)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
// anthropic 形态:tools[].name / input_schema(顶层,不是嵌套在 function 里)
|
||
if !strings.Contains(out, `"get_weather"`) {
|
||
t.Fatalf("工具定义在转换中丢失(identity 注入不该影响它):%s", out)
|
||
}
|
||
if !strings.Contains(out, `"input_schema"`) {
|
||
t.Fatalf("工具 schema 未转成 anthropic 形态(input_schema 缺失):%s", out)
|
||
}
|
||
})
|
||
|
||
t.Run("多模态图像块存活", func(t *testing.T) {
|
||
in := `{"model":"glm-5.3-flash","messages":[{"role":"user","content":[` +
|
||
`{"type":"text","text":"what"},` +
|
||
`{"type":"image_url","image_url":{"url":"data:image/png;base64,AAAA"}}]}]}`
|
||
out, err := h.transformRequest(in)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if !strings.Contains(out, `"image"`) || !strings.Contains(out, `"base64"`) {
|
||
t.Fatalf("图像块在转换中丢失:%s", out)
|
||
}
|
||
})
|
||
|
||
t.Run("基座装载状态可见", func(t *testing.T) {
|
||
// 读适配器的自检出口:base_loaded 必须为 true(否则身份块虽在、
|
||
// 协议转换已退化为透传)
|
||
res, err := h.ps.Call("channels", "__adapter_install_probe", map[string]interface{}{})
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
j := res.(map[string]interface{})["json"].(map[string]interface{})
|
||
zc := j["zcode"].(map[string]interface{})
|
||
if zc["ok"] != true {
|
||
t.Fatalf("zcode 适配器未装载: %v", zc)
|
||
}
|
||
})
|
||
}
|
||
|
||
// ★ 基座(anthropic.lua)读不到时必须**响亮失败**,不得静默退化。
|
||
//
|
||
// 反例危害:若"尽力而为"(只注入身份块、不作协议转换),会产出一个身份块
|
||
// 看着正确的请求,而 tools/图像/流式结构全未转换 —— 上游可能 200,但模型
|
||
// 拿不到工具定义,症状是"模型变笨了",与准入毫不相干,排查会完全跑偏。
|
||
//
|
||
// 判据看的是**适配器的真实产出**:把适配器目录指到空目录(基座不可得),
|
||
// transform_request 必须返回错误而不是原样透传。
|
||
func TestZcodeAdapterFailsLoudlyWithoutAnthropicBase(t *testing.T) {
|
||
// 隔离目录:插件目录下没有兄弟 adapters/ 目录 ⇒ 基座读不到
|
||
dir := t.TempDir()
|
||
pluginDir := filepath.Join(dir, "plugins")
|
||
if err := os.MkdirAll(pluginDir, 0755); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
t.Setenv("LLMSPROXY_PLUGIN_DIR", pluginDir)
|
||
|
||
c := kernelTestCore(t)
|
||
ps := c.Plugins()
|
||
src, err := readBundledChannels(t)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if err := ps.LoadSource("channels", src); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
vm := c.VM()
|
||
if vm == nil {
|
||
t.Fatal("VM 不可用")
|
||
}
|
||
// 插件自装时若基座读不到,适配器仍应装载(身份块是静态的),
|
||
// 但调用必须报错
|
||
out, err := vm.Transform("channels-zcode", "transform_request",
|
||
`{"model":"m","messages":[{"role":"user","content":"hi"}]}`)
|
||
if err == nil {
|
||
t.Fatalf("基座不可得时必须报错,却成功产出了:%s", out)
|
||
}
|
||
if !strings.Contains(err.Error(), "基座") {
|
||
t.Fatalf("错误信息应点明基座问题(可定位),实得 %v", err)
|
||
}
|
||
t.Logf("✓ 基座缺失时响亮失败: %v", err)
|
||
}
|
||
|
||
// ★ 日期块必须**幂等**:已以 <system-reminder> 开头则不重复插。
|
||
//
|
||
// 不判的后果:同一请求重试/多次 transform 时日期块叠成多层,改变 content 的
|
||
// 前缀结构(上游检查的正是结构)。参考 withContextPrefix 有 startsWithSystemReminder。
|
||
func TestZcodeContextPrefixIsIdempotent(t *testing.T) {
|
||
h := newZcodeIdentityHarness(t)
|
||
// 已是插入后的形态(首条 user 的 content[0] 就是 system-reminder)
|
||
in := `{"model":"m","messages":[{"role":"user","content":[` +
|
||
`{"type":"text","text":"<system-reminder>\nOLD\n</system-reminder>"},` +
|
||
`{"type":"text","text":"hi"}]}]}`
|
||
out, err := h.transformRequest(in)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
var req map[string]interface{}
|
||
if err := json.Unmarshal([]byte(out), &req); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
msgs := req["messages"].([]interface{})
|
||
content := msgs[0].(map[string]interface{})["content"].([]interface{})
|
||
n := 0
|
||
for _, c := range content {
|
||
if b, ok := c.(map[string]interface{}); ok {
|
||
if s, _ := b["text"].(string); strings.Contains(s, "<system-reminder>") {
|
||
n++
|
||
}
|
||
}
|
||
}
|
||
if n != 1 {
|
||
t.Fatalf("日期块必须幂等(已存在则不重复插),实得 %d 个 system-reminder 块", n)
|
||
}
|
||
}
|
||
|
||
// ★ 日期块只处理**第一条**消息,且它必须 role == "user"。
|
||
//
|
||
// 反例(我第一版就是这样):往后扫第一个 user 消息 —— 若首条是 assistant,
|
||
// 官方行为是**不动**,而不是去找后面那条 user。判据钉住这个差别。
|
||
func TestZcodeContextPrefixOnlyTouchesFirstMessage(t *testing.T) {
|
||
h := newZcodeIdentityHarness(t)
|
||
|
||
t.Run("首条不是 user 时不动", func(t *testing.T) {
|
||
in := `{"model":"m","messages":[` +
|
||
`{"role":"assistant","content":"opening"},` +
|
||
`{"role":"user","content":"hi"}]}`
|
||
out, err := h.transformRequest(in)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
var req map[string]interface{}
|
||
if err := json.Unmarshal([]byte(out), &req); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
msgs := req["messages"].([]interface{})
|
||
// ⚠ 注意:文本会被 anthropic 基座转成**块数组**(协议转换的正常行为),
|
||
// 所以这里不能断言 content 仍是字符串 —— 我第一版就这么断言,把正确的
|
||
// 实现判成了错。判据要验的是「**日期块**有没有被插进来」这个事实。
|
||
noReminder := func(content interface{}) bool {
|
||
switch c := content.(type) {
|
||
case string:
|
||
return !strings.Contains(c, "<system-reminder>")
|
||
case []interface{}:
|
||
for _, e := range c {
|
||
if b, ok := e.(map[string]interface{}); ok {
|
||
if t, _ := b["text"].(string); strings.Contains(t, "<system-reminder>") {
|
||
return false
|
||
}
|
||
}
|
||
}
|
||
}
|
||
return true
|
||
}
|
||
// 首条(assistant)不该被插日期块
|
||
if !noReminder(msgs[0].(map[string]interface{})["content"]) {
|
||
t.Fatal("首条是 assistant 时不该插日期块(官方只看第一条且要求 role=user)")
|
||
}
|
||
// 后面的 user 也**不该**被插(官方不往后找)
|
||
if !noReminder(msgs[1].(map[string]interface{})["content"]) {
|
||
t.Fatal("日期块被插到了后面的 user 消息上 —— 官方只看第一条")
|
||
}
|
||
// 且原文本内容必须保留(转换不能丢内容)
|
||
b, _ := json.Marshal(msgs[0])
|
||
if !strings.Contains(string(b), "opening") {
|
||
t.Fatal("首条 assistant 的内容丢了")
|
||
}
|
||
})
|
||
|
||
t.Run("首条是 user 时插入", func(t *testing.T) {
|
||
in := `{"model":"m","messages":[{"role":"user","content":"hi"}]}`
|
||
out, err := h.transformRequest(in)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
var req map[string]interface{}
|
||
if err := json.Unmarshal([]byte(out), &req); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
msgs := req["messages"].([]interface{})
|
||
content, ok := msgs[0].(map[string]interface{})["content"].([]interface{})
|
||
if !ok {
|
||
t.Fatal("首条 user 的 content 应升级为块数组")
|
||
}
|
||
b0 := content[0].(map[string]interface{})
|
||
if s, _ := b0["text"].(string); !strings.Contains(s, "<system-reminder>") {
|
||
t.Fatalf("首条 user 的 content[0] 必须是日期块,实得 %q", s)
|
||
}
|
||
})
|
||
}
|