From 2b395be43bee79b9ccb5079815b4d0a46340a39f Mon Sep 17 00:00:00 2001 From: liyutong02 Date: Tue, 18 Aug 2026 15:23:46 +0800 Subject: [PATCH 1/2] Fix Responses API image forwarding --- responses.go | 21 ++++++++++++++++++++- responses_test.go | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 1 deletion(-) create mode 100644 responses_test.go diff --git a/responses.go b/responses.go index 286e5b2..15f7151 100644 --- a/responses.go +++ b/responses.go @@ -92,7 +92,9 @@ func respToUpstream(instructions string, input json.RawMessage) (string, []upMes } else { um.Content = respPartsToOAI(it.Content) } - if txt := respPartsText(it.Content); txt != "" { + if it.Role == "user" { + um.Contents = respPartsToUpstream(it.Content) + } else if txt := respPartsText(it.Content); txt != "" { um.Contents = []upPart{{Type: "text", Text: txt}} } msgs = append(msgs, um) @@ -167,6 +169,23 @@ func respPartsToOAI(raw json.RawMessage) any { return out } +func respPartsToUpstream(raw json.RawMessage) []upPart { + parts := parseRespParts(raw) + out := make([]upPart, 0, len(parts)) + for _, p := range parts { + switch p.Type { + case "input_text", "output_text", "text": + out = append(out, upPart{Type: "text", Text: p.Text}) + case "input_image": + out = append(out, upPart{ + Type: "image_url", + ImageURL: &upImageURL{URL: p.ImageURL}, + }) + } + } + return out +} + func parseRespParts(raw json.RawMessage) []respContentPart { if len(raw) == 0 { return nil diff --git a/responses_test.go b/responses_test.go new file mode 100644 index 0000000..127534f --- /dev/null +++ b/responses_test.go @@ -0,0 +1,36 @@ +package main + +import ( + "encoding/json" + "testing" +) + +func TestRespToUpstreamPreservesInputImage(t *testing.T) { + input := json.RawMessage(`[ + { + "type": "message", + "role": "user", + "content": [ + {"type": "input_text", "text": "describe this"}, + {"type": "input_image", "image_url": "data:image/png;base64,abc"} + ] + } + ]`) + + _, msgs, err := respToUpstream("", input) + if err != nil { + t.Fatalf("respToUpstream returned error: %v", err) + } + if len(msgs) != 1 { + t.Fatalf("got %d messages, want 1", len(msgs)) + } + if len(msgs[0].Contents) != 2 { + t.Fatalf("got %d upstream content parts, want 2", len(msgs[0].Contents)) + } + if got := msgs[0].Contents[0].Text; got != "describe this" { + t.Fatalf("text content = %q, want %q", got, "describe this") + } + if got := msgs[0].Contents[1].ImageURL.URL; got != "data:image/png;base64,abc" { + t.Fatalf("image URL = %q, want data URL", got) + } +} From a08439262bc70470fe6959000d52093961a3b570 Mon Sep 17 00:00:00 2001 From: liyutong02 Date: Tue, 18 Aug 2026 16:42:09 +0800 Subject: [PATCH 2/2] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=20Responses=20=E7=AE=80?= =?UTF-8?q?=E5=86=99=E6=B6=88=E6=81=AF=E8=BE=93=E5=85=A5=E5=85=BC=E5=AE=B9?= =?UTF-8?q?=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- responses.go | 12 +++++++++++- responses_test.go | 25 +++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/responses.go b/responses.go index 15f7151..e2f09ce 100644 --- a/responses.go +++ b/responses.go @@ -80,7 +80,17 @@ func respToUpstream(instructions string, input json.RawMessage) (string, []upMes } for _, it := range items { switch it.Type { - case "message": + case "message", "": + // Hermes (and some other Responses clients) send input items in the + // compact chat form {"role": "user", "content": "..."} with no + // `type` field. Treat a missing/empty type as a message so the user + // text is not silently dropped. See: ds4f "model can't see user" + // reports through Hermes -> sub2api -> qodercli2api. + if it.Type == "" && it.Role == "" && it.Content == nil && it.CallID == "" && it.Output == "" { + // not a recognizable message item; keep old default behavior + flushAssistant() + continue + } flushAssistant() if it.Role == "system" || it.Role == "developer" { sysParts = append(sysParts, respPartsText(it.Content)) diff --git a/responses_test.go b/responses_test.go index 127534f..a7227f9 100644 --- a/responses_test.go +++ b/responses_test.go @@ -34,3 +34,28 @@ func TestRespToUpstreamPreservesInputImage(t *testing.T) { t.Fatalf("image URL = %q, want data URL", got) } } + +func TestRespToUpstreamAcceptsCompactRoleContentItem(t *testing.T) { + // Hermes sends Responses input in this compact form: it has role/content + // but omits the optional type:"message" discriminator. + input := json.RawMessage(`[ + {"role": "user", "content": "只回复四个字:测试成功"} + ]`) + + _, msgs, err := respToUpstream("", input) + if err != nil { + t.Fatalf("respToUpstream returned error: %v", err) + } + if len(msgs) != 1 { + t.Fatalf("got %d messages, want 1", len(msgs)) + } + if got := msgs[0].Role; got != "user" { + t.Fatalf("role = %q, want user", got) + } + if got := msgs[0].Content; got != "只回复四个字:测试成功" { + t.Fatalf("content = %q, want compact user text", got) + } + if len(msgs[0].Contents) != 1 || msgs[0].Contents[0].Text != "只回复四个字:测试成功" { + t.Fatalf("upstream contents = %#v, want compact user text", msgs[0].Contents) + } +}