From 1ba92449c7372493b2c94d42e290c0959f4cf760 Mon Sep 17 00:00:00 2001 From: yaxin Date: Tue, 18 Aug 2026 15:05:54 +0800 Subject: [PATCH] fix(gemini): wire includeServerSideToolInvocations into the typed transform path The struct field alone never reached the wire: the raw passthrough pipeline is covered by enableMixedGeminiToolInvocations (#5711), but TransformClaudeToGeminiWithOptions builds GeminiToolConfig from scratch and never set the flag, so gemini-* models entering through the Claude format gateway could still hit the upstream 400 from issue #5709. - Set IncludeServerSideToolInvocations=true when the built tool declarations mix functionDeclarations with googleSearch, matching the raw-path injection semantics. - Replace the marshal-roundtrip-only test with behavior tests that drive TransformClaudeToGeminiWithOptions: mixed tools set the flag, function-only and web-search-only requests leave it unset. --- .../pkg/antigravity/request_transformer.go | 22 +++++++ .../antigravity/request_transformer_test.go | 66 ++++++++++++++----- 2 files changed, 71 insertions(+), 17 deletions(-) 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 008e54724a..05764ef7ff 100644 --- a/backend/internal/pkg/antigravity/request_transformer_test.go +++ b/backend/internal/pkg/antigravity/request_transformer_test.go @@ -567,25 +567,57 @@ func TestTransformClaudeToGeminiWithOptions_PreservesWebSearchAlongsideFunctions } func TestGeminiToolConfig_IncludeServerSideToolInvocations(t *testing.T) { - t.Run("serialize and deserialize toolConfig with includeServerSideToolInvocations", func(t *testing.T) { - trueVal := true - cfg := GeminiToolConfig{ - FunctionCallingConfig: &GeminiFunctionCallingConfig{ - Mode: "VALIDATED", + 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"}]`), + }, }, - IncludeServerSideToolInvocations: &trueVal, - } - - data, err := json.Marshal(cfg) + Tools: tools, + }, "project-1", "gemini-2.5-flash", DefaultTransformOptions()) require.NoError(t, err) - require.Contains(t, string(data), `"includeServerSideToolInvocations":true`) - var decoded GeminiToolConfig - err = json.Unmarshal(data, &decoded) - require.NoError(t, err) - require.NotNil(t, decoded.IncludeServerSideToolInvocations) - require.True(t, *decoded.IncludeServerSideToolInvocations) - require.Equal(t, "VALIDATED", decoded.FunctionCallingConfig.Mode) + 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") }) } -