diff --git a/provider/geminiprovider/agent.go b/provider/geminiprovider/agent.go index 17443f0e..d8889aad 100644 --- a/provider/geminiprovider/agent.go +++ b/provider/geminiprovider/agent.go @@ -294,9 +294,19 @@ func (a *client) buildParams(messages []*message.Message, opts []agent.Option) ( fc.Mode = genai.FunctionCallingConfigModeAny fc.AllowedFunctionNames = mode.Required() } - cfg.ToolConfig = &genai.ToolConfig{ - FunctionCallingConfig: fc, + // Merge into any caller-supplied ToolConfig (e.g. a RetrievalConfig for + // Vertex grounding passed through GenerateContentConfig) rather than + // replacing it, so only the function-calling mode is overridden. Shallow- + // clone the struct first so overriding FunctionCallingConfig preserves the + // caller's other fields without mutating their ToolConfig pointer (which is + // aliased via the shallow *cfg = p copy above). + if cfg.ToolConfig == nil { + cfg.ToolConfig = &genai.ToolConfig{} + } else { + tc := *cfg.ToolConfig + cfg.ToolConfig = &tc } + cfg.ToolConfig.FunctionCallingConfig = fc } // Build a map of CallID → function name by scanning all messages first. diff --git a/provider/geminiprovider/agent_test.go b/provider/geminiprovider/agent_test.go index 33847256..3ad4e8d9 100644 --- a/provider/geminiprovider/agent_test.go +++ b/provider/geminiprovider/agent_test.go @@ -1792,6 +1792,63 @@ func TestGenerateContentConfigOption(t *testing.T) { } } +// TestToolModeMergesCallerToolConfig verifies that applying a tool mode only +// overrides the FunctionCallingConfig and preserves other fields a caller set on +// ToolConfig via the GenerateContentConfig escape hatch (e.g. a RetrievalConfig +// used for Vertex grounding). +func TestToolModeMergesCallerToolConfig(t *testing.T) { + weatherTool := functool.MustNew(functool.Config{ + Name: "get_weather", + Description: "Get the weather for a city.", + }, func(_ context.Context, args struct{ City string }) (string, error) { + return "sunny", nil + }) + + bodyCh := make(chan []byte, 1) + server := httptest.NewServer(captureAndRespond(t, bodyCh, "application/json", minimalTextResponse("ok"))) + defer server.Close() + + a := newTestClient(t, server) + + _, err := a.RunText(t.Context(), "what's the weather?", + geminiprovider.GenerateContentConfig(genai.GenerateContentConfig{ + ToolConfig: &genai.ToolConfig{ + RetrievalConfig: &genai.RetrievalConfig{LanguageCode: "en-US"}, + }, + }), + agent.WithTool(weatherTool), + agent.WithToolMode(tool.ToolModeRequired), + ).Collect() + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + var req map[string]any + if err := json.Unmarshal(<-bodyCh, &req); err != nil { + t.Fatalf("unmarshal request body: %v", err) + } + toolConfig, ok := req["toolConfig"].(map[string]any) + if !ok { + t.Fatalf("request missing toolConfig, got %T", req["toolConfig"]) + } + // The requested tool mode must be applied. + fcc, ok := toolConfig["functionCallingConfig"].(map[string]any) + if !ok { + t.Fatalf("toolConfig missing functionCallingConfig, got %T", toolConfig["functionCallingConfig"]) + } + if mode, _ := fcc["mode"].(string); mode != string(genai.FunctionCallingConfigModeAny) { + t.Errorf("functionCallingConfig.mode = %q, want %q", mode, genai.FunctionCallingConfigModeAny) + } + // The caller-supplied RetrievalConfig must be preserved, not dropped. + retrieval, ok := toolConfig["retrievalConfig"].(map[string]any) + if !ok { + t.Fatalf("toolConfig dropped caller retrievalConfig, got %T", toolConfig["retrievalConfig"]) + } + if lang, _ := retrieval["languageCode"].(string); lang != "en-US" { + t.Errorf("retrievalConfig.languageCode = %q, want %q", lang, "en-US") + } +} + // findErrorContent returns the first *message.ErrorContent across all messages // of a response, or nil if none is present. func findErrorContent(resp *agent.Response) *message.ErrorContent {