diff --git a/backend/internal/pkg/antigravity/gemini_types.go b/backend/internal/pkg/antigravity/gemini_types.go index 033dccbd5d..4fd7882adc 100644 --- a/backend/internal/pkg/antigravity/gemini_types.go +++ b/backend/internal/pkg/antigravity/gemini_types.go @@ -112,7 +112,8 @@ type GeminiImageSearch struct { // GeminiToolConfig Gemini 工具配置 type GeminiToolConfig struct { - FunctionCallingConfig *GeminiFunctionCallingConfig `json:"functionCallingConfig,omitempty"` + FunctionCallingConfig *GeminiFunctionCallingConfig `json:"functionCallingConfig,omitempty"` + IncludeServerSideToolInvocations *bool `json:"includeServerSideToolInvocations,omitempty"` } // GeminiFunctionCallingConfig 函数调用配置 diff --git a/backend/internal/pkg/antigravity/request_transformer.go b/backend/internal/pkg/antigravity/request_transformer.go index 7d0a707959..9da3cc33a2 100644 --- a/backend/internal/pkg/antigravity/request_transformer.go +++ b/backend/internal/pkg/antigravity/request_transformer.go @@ -156,6 +156,13 @@ func TransformClaudeToGeminiWithOptions(claudeReq *ClaudeRequest, projectID, map Mode: "VALIDATED", }, } + // 内置工具(googleSearch)与函数调用混用时,上游要求显式开启 + // includeServerSideToolInvocations,否则返回 400(issue #5709)。 + // 与 raw 透传路的 enableMixedGeminiToolInvocations 注入保持同一语义。 + if hasMixedToolInvocations(tools) { + enabled := true + innerRequest.ToolConfig.IncludeServerSideToolInvocations = &enabled + } } if systemInstruction != nil { @@ -703,6 +710,21 @@ func isWebSearchTool(tool ClaudeTool) bool { } } +// hasMixedToolInvocations 判断构建后的工具声明是否同时包含函数声明与内置工具 +// (googleSearch)。仅在两者并存时需要开启 includeServerSideToolInvocations。 +func hasMixedToolInvocations(declarations []GeminiToolDeclaration) bool { + hasFunc, hasBuiltin := false, false + for _, d := range declarations { + if len(d.FunctionDeclarations) > 0 { + hasFunc = true + } + if d.GoogleSearch != nil { + hasBuiltin = true + } + } + return hasFunc && hasBuiltin +} + // buildTools 构建 tools func buildTools(tools []ClaudeTool) []GeminiToolDeclaration { if len(tools) == 0 { diff --git a/backend/internal/pkg/antigravity/request_transformer_test.go b/backend/internal/pkg/antigravity/request_transformer_test.go index ea95cc7ea6..05764ef7ff 100644 --- a/backend/internal/pkg/antigravity/request_transformer_test.go +++ b/backend/internal/pkg/antigravity/request_transformer_test.go @@ -565,3 +565,59 @@ func TestTransformClaudeToGeminiWithOptions_PreservesWebSearchAlongsideFunctions require.Equal(t, "get_weather", req.Request.Tools[0].FunctionDeclarations[0].Name) require.NotNil(t, req.Request.Tools[1].GoogleSearch) } + +func TestGeminiToolConfig_IncludeServerSideToolInvocations(t *testing.T) { + functionTool := ClaudeTool{ + Name: "get_weather", + Description: "Get weather information", + InputSchema: map[string]any{"type": "object"}, + } + webSearchTool := ClaudeTool{ + Type: "web_search_20250305", + Name: "web_search", + } + + transform := func(t *testing.T, tools []ClaudeTool) (V1InternalRequest, string) { + t.Helper() + body, err := TransformClaudeToGeminiWithOptions(&ClaudeRequest{ + Model: "claude-3-5-sonnet-latest", + Messages: []ClaudeMessage{ + { + Role: "user", + Content: json.RawMessage(`[{"type":"text","text":"hello"}]`), + }, + }, + Tools: tools, + }, "project-1", "gemini-2.5-flash", DefaultTransformOptions()) + require.NoError(t, err) + + var req V1InternalRequest + require.NoError(t, json.Unmarshal(body, &req)) + return req, string(body) + } + + t.Run("mixed builtin and function tools enable server-side tool invocations", func(t *testing.T) { + req, raw := transform(t, []ClaudeTool{functionTool, webSearchTool}) + + require.NotNil(t, req.Request.ToolConfig) + require.NotNil(t, req.Request.ToolConfig.IncludeServerSideToolInvocations) + require.True(t, *req.Request.ToolConfig.IncludeServerSideToolInvocations) + require.Contains(t, raw, `"includeServerSideToolInvocations":true`) + }) + + t.Run("function tools only leave the flag unset", func(t *testing.T) { + req, raw := transform(t, []ClaudeTool{functionTool}) + + require.NotNil(t, req.Request.ToolConfig) + require.Nil(t, req.Request.ToolConfig.IncludeServerSideToolInvocations) + require.NotContains(t, raw, "includeServerSideToolInvocations") + }) + + t.Run("web search only leaves the flag unset", func(t *testing.T) { + req, raw := transform(t, []ClaudeTool{webSearchTool}) + + require.NotNil(t, req.Request.ToolConfig) + require.Nil(t, req.Request.ToolConfig.IncludeServerSideToolInvocations) + require.NotContains(t, raw, "includeServerSideToolInvocations") + }) +}