mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-07 14:58:23 +08:00
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:
@@ -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")
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user