mirror of
https://gitcode.com/JianFeeeee/ModelRouter.git
synced 2026-11-07 17:57:23 +00:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b871c0c713 |
@ -294,3 +294,111 @@ func TestZcodeAdapterFailsLoudlyWithoutAnthropicBase(t *testing.T) {
|
||||
}
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@ -9250,18 +9250,40 @@ function adapter.transform_request(raw_body)
|
||||
|
||||
req.system = buildSystemBlocks(caller, opts)
|
||||
|
||||
-- 日期块:插到**第一条** user 消息的 content 数组最前(官方形态)。
|
||||
-- ★ 日期块:只处理**第一条**消息,且它必须是 role == "user"(官方行为)。
|
||||
--
|
||||
-- 两条容易漏掉的规则(照参考 withContextPrefix,「优化」它们会偏离官方):
|
||||
-- ① **幂等**:已以 <system-reminder> 开头则不重复插。不判的话,同一请求
|
||||
-- 在重试/多次 transform 时会把日期块叠成好几层,改变前缀结构;
|
||||
-- ② **只看第一条**:若首条不是 user(例如 assistant 开场),**不动** ——
|
||||
-- 不是"往后找一个 user 消息"。我第一版就是往后找,与官方行为不符。
|
||||
local msgs = req.messages
|
||||
if type(msgs) == "table" then
|
||||
for _, m in ipairs(msgs) do
|
||||
if type(m) == "table" and m.role == "user" then
|
||||
local c = m.content
|
||||
local first = msgs[1]
|
||||
if type(first) == "table" and first.role == "user" then
|
||||
local c = first.content
|
||||
-- 已插过(字符串或块数组两种形态都要判)→ 不重复插。
|
||||
-- ⚠ 长度必须用 #SR 算,不能写死 —— 我第一版写 `sub(1, 16)` 而
|
||||
-- "<system-reminder>" 是 **17** 字符,判断**永远为假** ⇒ 幂等失效、
|
||||
-- 重试时日期块叠加(判据当场抓到)。
|
||||
local SR = "<system-reminder>"
|
||||
local already = false
|
||||
if type(c) == "string" then
|
||||
already = c:sub(1, #SR) == SR
|
||||
elseif type(c) == "table" and type(c[1]) == "table" then
|
||||
local t = c[1].text
|
||||
already = type(t) == "string" and t:sub(1, #SR) == SR
|
||||
end
|
||||
if not already then
|
||||
local blk = contextPrefixBlock()
|
||||
if type(c) == "table" then
|
||||
table.insert(c, 1, contextPrefixBlock())
|
||||
table.insert(c, 1, blk)
|
||||
elseif type(c) == "string" then
|
||||
m.content = { contextPrefixBlock(), { type = "text", text = c } }
|
||||
-- 纯文本 content 转成块数组(官方就是数组形态)
|
||||
first.content = { blk, { type = "text", text = c } }
|
||||
else
|
||||
first.content = { blk }
|
||||
end
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user