backend: add tests for image ContentBlock encoding (Anthropic + OpenAI)

TestBuildAnthropicMessages_UserImageBlocks: user message with ContentBlocks
TestBuildAnthropicMessages_ToolImageBlocks: tool result with image blocks
TestBuildAnthropicMessages_UserPlainTextFallback: nil ContentBlocks uses Content
TestEncodeMessages_UserWithImageContentBlocks: OpenAI image_url encoding
TestEncodeMessages_UserWithImageContentBlocks_JSON: wire format verification
This commit is contained in:
ollie 2026-05-25 18:06:01 +02:00
parent 02df4b8dd5
commit 4b4dab3f02
2 changed files with 121 additions and 0 deletions

View File

@ -441,3 +441,73 @@ func TestToolResultCaching_LastLargeResult(t *testing.T) {
t.Errorf("last large result missing cache_control:ephemeral; got %+v", last.Content[1].CacheControl)
}
}
// --- image ContentBlocks ---
func TestBuildAnthropicMessages_UserImageBlocks(t *testing.T) {
_, out := buildAnthropicMessages([]Message{{
Role: "user",
ContentBlocks: []ContentBlock{
{Type: "text", Text: "what is this?"},
{Type: "image", ImageSource: &ImageSource{Type: "base64", MediaType: "image/png", Data: "iVBOR"}},
},
}})
if len(out) != 1 {
t.Fatalf("len = %d; want 1", len(out))
}
if len(out[0].Content) != 2 {
t.Fatalf("content blocks = %d; want 2", len(out[0].Content))
}
if out[0].Content[0].Type != "text" || out[0].Content[0].Text != "what is this?" {
t.Errorf("block[0] = %+v", out[0].Content[0])
}
if out[0].Content[1].Type != "image" || out[0].Content[1].Source == nil {
t.Fatalf("block[1] = %+v", out[0].Content[1])
}
if out[0].Content[1].Source.Type != "base64" || out[0].Content[1].Source.MediaType != "image/png" || out[0].Content[1].Source.Data != "iVBOR" {
t.Errorf("source = %+v", out[0].Content[1].Source)
}
}
func TestBuildAnthropicMessages_ToolImageBlocks(t *testing.T) {
_, out := buildAnthropicMessages([]Message{
{Role: "user", Content: "read image"},
{Role: "assistant", ToolCalls: []ToolCall{{ID: "c1", Name: "file_read", Arguments: json.RawMessage(`{}`)}}},
{Role: "tool", ToolCallID: "c1", Content: "[image]", ContentBlocks: []ContentBlock{
{Type: "image", ImageSource: &ImageSource{Type: "base64", MediaType: "image/jpeg", Data: "AAAA"}},
{Type: "text", Text: "[image: image/jpeg]"},
}},
})
// Last message is the batched tool result (user role).
last := out[len(out)-1]
if last.Role != "user" {
t.Fatalf("role = %q; want user", last.Role)
}
// Should have image block + tool_result block.
var hasImage, hasToolResult bool
for _, b := range last.Content {
if b.Type == "image" && b.Source != nil && b.Source.MediaType == "image/jpeg" {
hasImage = true
}
if b.Type == "tool_result" && b.ToolUseID == "c1" {
hasToolResult = true
}
}
if !hasImage {
t.Error("missing image block in tool result")
}
if !hasToolResult {
t.Error("missing tool_result block")
}
}
func TestBuildAnthropicMessages_UserPlainTextFallback(t *testing.T) {
// When ContentBlocks is nil, should fall back to Content string.
_, out := buildAnthropicMessages([]Message{{Role: "user", Content: "hello"}})
if len(out) != 1 || len(out[0].Content) != 1 {
t.Fatalf("unexpected: %+v", out)
}
if out[0].Content[0].Type != "text" || out[0].Content[0].Text != "hello" {
t.Errorf("block = %+v", out[0].Content[0])
}
}

View File

@ -362,6 +362,57 @@ func TestOpenAI_NoCacheControl(t *testing.T) {
}
}
func TestEncodeMessages_UserWithImageContentBlocks(t *testing.T) {
wire := encodeOpenAIMessages([]Message{{
Role: "user",
ContentBlocks: []ContentBlock{
{Type: "text", Text: "what is this?"},
{Type: "image", ImageSource: &ImageSource{Type: "base64", MediaType: "image/png", Data: "iVBOR"}},
},
}})
if len(wire) != 1 {
t.Fatalf("len = %d; want 1", len(wire))
}
if wire[0].Content != nil {
t.Error("Content should be nil when ContentBlocks is used")
}
if len(wire[0].ContentBlocks) != 2 {
t.Fatalf("ContentBlocks len = %d; want 2", len(wire[0].ContentBlocks))
}
if wire[0].ContentBlocks[0].Type != "text" || wire[0].ContentBlocks[0].Text != "what is this?" {
t.Errorf("block[0] = %+v", wire[0].ContentBlocks[0])
}
if wire[0].ContentBlocks[1].Type != "image_url" || wire[0].ContentBlocks[1].ImageURL == nil {
t.Fatalf("block[1] = %+v", wire[0].ContentBlocks[1])
}
if wire[0].ContentBlocks[1].ImageURL.URL != "data:image/png;base64,iVBOR" {
t.Errorf("image url = %q", wire[0].ContentBlocks[1].ImageURL.URL)
}
}
func TestEncodeMessages_UserWithImageContentBlocks_JSON(t *testing.T) {
wire := encodeOpenAIMessages([]Message{{
Role: "user",
ContentBlocks: []ContentBlock{
{Type: "text", Text: "look"},
{Type: "image", ImageSource: &ImageSource{Type: "base64", MediaType: "image/jpeg", Data: "AAAA"}},
},
}})
data, _ := json.Marshal(wire[0])
var raw map[string]json.RawMessage
json.Unmarshal(data, &raw)
// content must be an array
if raw["content"][0] != '[' {
t.Errorf("content should be array; got %s", raw["content"])
}
if !strings.Contains(string(raw["content"]), "image_url") {
t.Errorf("content missing image_url: %s", raw["content"])
}
if !strings.Contains(string(raw["content"]), "data:image/jpeg;base64,AAAA") {
t.Errorf("content missing data URI: %s", raw["content"])
}
}
func TestEncodeRequest_ToolsOmittedWhenEmpty(t *testing.T) {
req := openAIChatRequest{
Model: "m",