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.
This commit is contained in:
yaxin
2026-08-18 15:05:54 +08:00
parent 3c3bb2fa19
commit 1ba92449c7
2 changed files with 71 additions and 17 deletions
@@ -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 {
@@ -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")
})
}