diff --git a/backend/internal/pkg/antigravity/request_transformer.go b/backend/internal/pkg/antigravity/request_transformer.go index 1750848348..5f9e74bc6f 100644 --- a/backend/internal/pkg/antigravity/request_transformer.go +++ b/backend/internal/pkg/antigravity/request_transformer.go @@ -148,21 +148,20 @@ func TransformClaudeToGeminiWithOptions(claudeReq *ClaudeRequest, projectID, map } // 针对 Gemini Reasoning 模型(如 gemini-3.1-pro-high等)过滤强制空 ToolConfig - isReasoning := IsGeminiReasoningModel(targetModel) - if !isReasoning || len(tools) > 0 { - // 总是设置 toolConfig,与官方客户端一致 - innerRequest.ToolConfig = &GeminiToolConfig{ - FunctionCallingConfig: &GeminiFunctionCallingConfig{ - Mode: "VALIDATED", - }, - } - // 内置工具(googleSearch)与函数调用混用时,上游要求显式开启 - // includeServerSideToolInvocations,否则返回 400(issue #5709)。 - // 与 raw 透传路的 enableMixedGeminiToolInvocations 注入保持同一语义。 - if hasMixedToolInvocations(tools) { - enabled := true - innerRequest.ToolConfig.IncludeServerSideToolInvocations = &enabled - } + // toolConfig must always be present: upstream rejects requests without it, + // including reasoning models called without any tools. + // 总是设置 toolConfig,与官方客户端一致 + innerRequest.ToolConfig = &GeminiToolConfig{ + FunctionCallingConfig: &GeminiFunctionCallingConfig{ + Mode: "VALIDATED", + }, + } + // 内置工具(googleSearch)与函数调用混用时,上游要求显式开启 + // includeServerSideToolInvocations,否则返回 400(issue #5709)。 + // 与 raw 透传路的 enableMixedGeminiToolInvocations 注入保持同一语义。 + if hasMixedToolInvocations(tools) { + enabled := true + innerRequest.ToolConfig.IncludeServerSideToolInvocations = &enabled } if systemInstruction != nil { diff --git a/backend/internal/pkg/antigravity/request_transformer_test.go b/backend/internal/pkg/antigravity/request_transformer_test.go index 05764ef7ff..62d038640c 100644 --- a/backend/internal/pkg/antigravity/request_transformer_test.go +++ b/backend/internal/pkg/antigravity/request_transformer_test.go @@ -621,3 +621,38 @@ func TestGeminiToolConfig_IncludeServerSideToolInvocations(t *testing.T) { require.NotContains(t, raw, "includeServerSideToolInvocations") }) } + +// TestToolConfigAlwaysPresent ensures toolConfig is always emitted, including for +// reasoning models without any tools: upstream rejects requests that omit it. +func TestToolConfigAlwaysPresent(t *testing.T) { + cases := []struct { + name string + model string + tools []ClaudeTool + }{ + {name: "reasoning model without tools", model: "gemini-3.1-pro-high"}, + {name: "reasoning model with tools", model: "gemini-3.1-pro-high", tools: []ClaudeTool{{ + Name: "web_search", + Description: "Search the web", + InputSchema: map[string]any{"type": "object", "properties": map[string]any{}}, + }}}, + {name: "non-reasoning model without tools", model: "gemini-3.1-pro"}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + claudeReq := &ClaudeRequest{ + Model: tc.model, + Messages: []ClaudeMessage{{Role: "user", Content: json.RawMessage(`"Hello"`)}}, + Tools: tc.tools, + } + body, err := TransformClaudeToGeminiWithOptions(claudeReq, "project-1", tc.model, DefaultTransformOptions()) + require.NoError(t, err) + + var req V1InternalRequest + require.NoError(t, json.Unmarshal(body, &req)) + require.NotNil(t, req.Request.ToolConfig, "toolConfig must be present") + require.NotNil(t, req.Request.ToolConfig.FunctionCallingConfig) + require.Equal(t, "VALIDATED", req.Request.ToolConfig.FunctionCallingConfig.Mode) + }) + } +}