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:
parent
02df4b8dd5
commit
4b4dab3f02
|
|
@ -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])
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
Reference in New Issue