diff --git a/internal/lua/adapters/opencode.lua b/internal/lua/adapters/opencode.lua index 9690b87..db588d9 100644 --- a/internal/lua/adapters/opencode.lua +++ b/internal/lua/adapters/opencode.lua @@ -13,15 +13,39 @@ adapter.headers = { } -- 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) local ok, req = pcall(json.decode, raw_body) if not ok then return raw_body end req.disable_thinking = nil req.extra_body = nil if req.messages then + local kept = {} for _, msg in ipairs(req.messages) do 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 + 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) end diff --git a/internal/lua/vm_test.go b/internal/lua/vm_test.go index 3e4c3d4..67e8f6c 100644 --- a/internal/lua/vm_test.go +++ b/internal/lua/vm_test.go @@ -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) { vm := NewVM(freshAdapterDir(t)) if err := vm.Start(); err != nil {