diff --git a/backend/internal/service/gemini_messages_compat_service.go b/backend/internal/service/gemini_messages_compat_service.go index 5b1abc11..45aa3599 100644 --- a/backend/internal/service/gemini_messages_compat_service.go +++ b/backend/internal/service/gemini_messages_compat_service.go @@ -3169,12 +3169,17 @@ func convertClaudeToolsToGeminiTools(tools any) []any { return nil } + hasWebSearch := false funcDecls := make([]any, 0, len(arr)) for _, t := range arr { tm, ok := t.(map[string]any) if !ok { continue } + if isClaudeWebSearchToolMap(tm) { + hasWebSearch = true + continue + } var name, desc string var params any @@ -3218,13 +3223,35 @@ func convertClaudeToolsToGeminiTools(tools any) []any { }) } - if len(funcDecls) == 0 { + out := make([]any, 0, 2) + if len(funcDecls) > 0 { + out = append(out, map[string]any{ + "functionDeclarations": funcDecls, + }) + } + if hasWebSearch { + out = append(out, map[string]any{ + "googleSearch": map[string]any{}, + }) + } + if len(out) == 0 { return nil } - return []any{ - map[string]any{ - "functionDeclarations": funcDecls, - }, + return out +} + +func isClaudeWebSearchToolMap(tool map[string]any) bool { + toolType, _ := tool["type"].(string) + if strings.HasPrefix(toolType, "web_search") || toolType == "google_search" { + return true + } + + name, _ := tool["name"].(string) + switch strings.TrimSpace(name) { + case "web_search", "google_search", "web_search_20250305": + return true + default: + return false } } diff --git a/backend/internal/service/gemini_messages_compat_service_test.go b/backend/internal/service/gemini_messages_compat_service_test.go index f659f0e6..0d132b48 100644 --- a/backend/internal/service/gemini_messages_compat_service_test.go +++ b/backend/internal/service/gemini_messages_compat_service_test.go @@ -164,6 +164,35 @@ func TestConvertClaudeToolsToGeminiTools_CustomType(t *testing.T) { } } +func TestConvertClaudeToolsToGeminiTools_PreservesWebSearchAlongsideFunctions(t *testing.T) { + tools := []any{ + map[string]any{ + "name": "get_weather", + "description": "Get weather info", + "input_schema": map[string]any{"type": "object"}, + }, + map[string]any{ + "type": "web_search_20250305", + "name": "web_search", + }, + } + + result := convertClaudeToolsToGeminiTools(tools) + require.Len(t, result, 2) + + functionDecl, ok := result[0].(map[string]any) + require.True(t, ok) + funcDecls, ok := functionDecl["functionDeclarations"].([]any) + require.True(t, ok) + require.Len(t, funcDecls, 1) + + searchDecl, ok := result[1].(map[string]any) + require.True(t, ok) + googleSearch, ok := searchDecl["googleSearch"].(map[string]any) + require.True(t, ok) + require.Empty(t, googleSearch) +} + func TestGeminiHandleNativeNonStreamingResponse_DebugDisabledDoesNotEmitHeaderLogs(t *testing.T) { gin.SetMode(gin.TestMode) logSink, restore := captureStructuredLog(t)