package service import ( "encoding/json" "testing" "github.com/stretchr/testify/require" ) func TestGeminiResponseToChatCompletionsPreservesInlineData(t *testing.T) { tests := []struct { name string parts []any want string }{ { name: "image only", parts: []any{ map[string]any{"inlineData": map[string]any{"mimeType": "image/png", "data": "aW1hZ2U="}}, }, want: "![image](data:image/png;base64,aW1hZ2U=)", }, { name: "text and image", parts: []any{ map[string]any{"text": "rendered image:\n"}, map[string]any{"inlineData": map[string]any{"mimeType": "image/webp", "data": "d2VicA=="}}, }, want: "rendered image:\n![image](data:image/webp;base64,d2VicA==)", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { geminiResp := map[string]any{ "candidates": []any{map[string]any{ "content": map[string]any{"parts": tt.parts}, "finishReason": "STOP", }}, } rawData, err := json.Marshal(geminiResp) require.NoError(t, err) got, _, err := geminiResponseToChatCompletions(geminiResp, "gemini-test", rawData, nil) require.NoError(t, err) require.Len(t, got.Choices, 1) var content string require.NoError(t, json.Unmarshal(got.Choices[0].Message.Content, &content)) require.Equal(t, tt.want, content) require.Equal(t, "stop", got.Choices[0].FinishReason) }) } } func TestGeminiResponseToChatCompletionsOmitsInvalidInlineData(t *testing.T) { tests := []struct { name string inlineData map[string]any }{ { name: "unsupported MIME type", inlineData: map[string]any{"mimeType": "image/svg+xml", "data": "PHN2Zz48L3N2Zz4="}, }, { name: "malformed MIME type", inlineData: map[string]any{"mimeType": "image/png; charset=utf-8", "data": "aW1hZ2U="}, }, { name: "malformed base64", inlineData: map[string]any{"mimeType": "image/png", "data": "not-valid-base64!!!"}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { geminiResp := map[string]any{ "candidates": []any{map[string]any{ "content": map[string]any{"parts": []any{map[string]any{"text": "before"}, map[string]any{"inlineData": tt.inlineData}, map[string]any{"text": "after"}}}, "finishReason": "STOP", }}, } rawData, err := json.Marshal(geminiResp) require.NoError(t, err) got, _, err := geminiResponseToChatCompletions(geminiResp, "gemini-test", rawData, nil) require.NoError(t, err) var content string require.NoError(t, json.Unmarshal(got.Choices[0].Message.Content, &content)) require.Equal(t, "beforeafter", content) }) } } func TestConvertGeminiToClaudeMessageOmitsInlineDataForAnthropicMessages(t *testing.T) { geminiResp := map[string]any{ "candidates": []any{map[string]any{ "content": map[string]any{"parts": []any{ map[string]any{"text": "before"}, map[string]any{"inlineData": map[string]any{"mimeType": "image/png", "data": "aW1hZ2U="}}, map[string]any{"functionCall": map[string]any{"name": "get_weather", "args": map[string]any{"city": "Paris"}}}, map[string]any{"text": "after"}, }}, "finishReason": "STOP", }}, } rawData, err := json.Marshal(geminiResp) require.NoError(t, err) withInlineData, _ := convertGeminiToClaudeMessage(geminiResp, "gemini-test", rawData, true) contentWithInlineData, ok := withInlineData["content"].([]any) require.True(t, ok) require.Len(t, contentWithInlineData, 4) require.Equal(t, map[string]any{"type": "text", "text": "before"}, contentWithInlineData[0]) require.Equal(t, map[string]any{"type": "text", "text": "![image](data:image/png;base64,aW1hZ2U=)"}, contentWithInlineData[1]) toolUse, ok := contentWithInlineData[2].(map[string]any) require.True(t, ok) require.Equal(t, "tool_use", toolUse["type"]) require.Equal(t, "get_weather", toolUse["name"]) require.Equal(t, map[string]any{"type": "text", "text": "after"}, contentWithInlineData[3]) withoutInlineData, _ := convertGeminiToClaudeMessage(geminiResp, "gemini-test", rawData, false) contentWithoutInlineData, ok := withoutInlineData["content"].([]any) require.True(t, ok) require.Len(t, contentWithoutInlineData, 3) require.Equal(t, map[string]any{"type": "text", "text": "before"}, contentWithoutInlineData[0]) toolUseWithoutInlineData, ok := contentWithoutInlineData[1].(map[string]any) require.True(t, ok) require.Equal(t, "tool_use", toolUseWithoutInlineData["type"]) require.Equal(t, "get_weather", toolUseWithoutInlineData["name"]) require.Equal(t, map[string]any{"type": "text", "text": "after"}, contentWithoutInlineData[2]) } func TestGeminiResponseToChatCompletionsRetainsTextAndToolBehavior(t *testing.T) { geminiResp := map[string]any{ "candidates": []any{map[string]any{ "content": map[string]any{"parts": []any{ map[string]any{"text": "checking"}, map[string]any{"functionCall": map[string]any{ "name": "get_weather", "args": map[string]any{"city": "Paris"}, }}, }}, "finishReason": "STOP", }}, } rawData, err := json.Marshal(geminiResp) require.NoError(t, err) got, _, err := geminiResponseToChatCompletions(geminiResp, "gemini-test", rawData, nil) require.NoError(t, err) require.Len(t, got.Choices, 1) choice := got.Choices[0] var content string require.NoError(t, json.Unmarshal(choice.Message.Content, &content)) require.Equal(t, "checking", content) require.Equal(t, "tool_calls", choice.FinishReason) require.Len(t, choice.Message.ToolCalls, 1) require.Equal(t, "get_weather", choice.Message.ToolCalls[0].Function.Name) require.JSONEq(t, `{"city":"Paris"}`, choice.Message.ToolCalls[0].Function.Arguments) }