mirror of
https://gitcode.com/JianFeeeee/ModelRouter.git
synced 2026-09-20 08:57:57 +00:00
fix: zen adapter strips multimodal parts (upstream is text-only)
This commit is contained in:
@ -13,16 +13,40 @@ adapter.headers = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
-- OpenAI /chat/completions format (pass-through, strip provider-specific fields)
|
-- OpenAI /chat/completions format (pass-through, strip provider-specific fields)
|
||||||
|
-- zen 上游 schema 只接受 text content part(无视觉/音频能力):多模态 part
|
||||||
|
-- (image_url / input_audio / file 等)一律剥离;因此失去全部 content 的
|
||||||
|
-- 消息整条丢弃,避免上游 "unknown variant `image_url`, expected `text`"。
|
||||||
function adapter.transform_request(raw_body)
|
function adapter.transform_request(raw_body)
|
||||||
local ok, req = pcall(json.decode, raw_body)
|
local ok, req = pcall(json.decode, raw_body)
|
||||||
if not ok then return raw_body end
|
if not ok then return raw_body end
|
||||||
req.disable_thinking = nil
|
req.disable_thinking = nil
|
||||||
req.extra_body = nil
|
req.extra_body = nil
|
||||||
if req.messages then
|
if req.messages then
|
||||||
|
local kept = {}
|
||||||
for _, msg in ipairs(req.messages) do
|
for _, msg in ipairs(req.messages) do
|
||||||
msg.reasoning_content = nil
|
msg.reasoning_content = nil
|
||||||
|
local drop = false
|
||||||
|
if type(msg.content) == "table" then
|
||||||
|
local parts = {}
|
||||||
|
for _, part in ipairs(msg.content) do
|
||||||
|
if type(part) == "table" and part.type ~= nil and part.type ~= "text" then
|
||||||
|
-- multimodal part not supported by zen
|
||||||
|
else
|
||||||
|
table.insert(parts, part)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
if #parts == 0 then
|
||||||
|
drop = true
|
||||||
|
else
|
||||||
|
msg.content = parts
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if not drop then
|
||||||
|
table.insert(kept, msg)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
req.messages = kept
|
||||||
|
end
|
||||||
return json.encode(req)
|
return json.encode(req)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@ -135,6 +135,37 @@ func TestOpenCodeAdapterFingerprint(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestOpenCodeStripsMultiModalParts(t *testing.T) {
|
||||||
|
vm := NewVM(freshAdapterDir(t))
|
||||||
|
if err := vm.Start(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer vm.Stop()
|
||||||
|
body := `{"model":"x","messages":[
|
||||||
|
{"role":"user","content":[
|
||||||
|
{"type":"text","text":"describe"},
|
||||||
|
{"type":"image_url","image_url":{"url":"data:image/png;base64,QUJD"}},
|
||||||
|
{"type":"input_audio","input_audio":{"data":"QQ","format":"wav"}}
|
||||||
|
]},
|
||||||
|
{"role":"user","content":[{"type":"image_url","image_url":{"url":"https://ex.com/a.png"}}]},
|
||||||
|
{"role":"assistant","content":"plain"}
|
||||||
|
]}`
|
||||||
|
out, err := vm.Transform("opencode", "transform_request", body)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if strings.Contains(out, "image_url") || strings.Contains(out, "input_audio") {
|
||||||
|
t.Fatalf("opencode must strip multimodal parts: %s", out)
|
||||||
|
}
|
||||||
|
if !strings.Contains(out, "describe") {
|
||||||
|
t.Fatalf("text parts must survive: %s", out)
|
||||||
|
}
|
||||||
|
// image-only message dropped: only the text message and plain assistant message remain
|
||||||
|
if got := strings.Count(out, `"role"`); got != 2 {
|
||||||
|
t.Fatalf("image-only message must be dropped, got %d messages: %s", got, out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestBuildHeadersCustomHook(t *testing.T) {
|
func TestBuildHeadersCustomHook(t *testing.T) {
|
||||||
vm := NewVM(freshAdapterDir(t))
|
vm := NewVM(freshAdapterDir(t))
|
||||||
if err := vm.Start(); err != nil {
|
if err := vm.Start(); err != nil {
|
||||||
|
|||||||
Reference in New Issue
Block a user