Skip to content

Commit 181e19e

Browse files
authored
Merge pull request #2276 from dgageot/google-grounding
Google grounding
2 parents f99a708 + 489af91 commit 181e19e

7 files changed

Lines changed: 210 additions & 2 deletions

File tree

agent-schema.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -545,7 +545,7 @@
545545
},
546546
"provider_opts": {
547547
"type": "object",
548-
"description": "Provider-specific options. Sampling parameters: top_k (integer, supported by anthropic, google, amazon-bedrock, and custom OpenAI-compatible providers like vLLM/Ollama), repetition_penalty (float, forwarded to custom OpenAI-compatible providers), min_p (float, forwarded to custom providers), seed (integer, forwarded to OpenAI). Infrastructure options: dmr: runtime_flags. anthropic/amazon-bedrock (Claude): interleaved_thinking (boolean, default true). openai: transport ('sse' or 'websocket') to choose between SSE and WebSocket streaming for the Responses API. openai/anthropic/google: rerank_prompt (string) to fully override the system prompt used for RAG reranking (advanced - prefer using results.reranking.criteria for domain-specific guidance).",
548+
"description": "Provider-specific options. Sampling parameters: top_k (integer, supported by anthropic, google, amazon-bedrock, and custom OpenAI-compatible providers like vLLM/Ollama), repetition_penalty (float, forwarded to custom OpenAI-compatible providers), min_p (float, forwarded to custom providers), seed (integer, forwarded to OpenAI). Infrastructure options: dmr: runtime_flags. anthropic/amazon-bedrock (Claude): interleaved_thinking (boolean, default true). openai: transport ('sse' or 'websocket') to choose between SSE and WebSocket streaming for the Responses API. openai/anthropic/google: rerank_prompt (string) to fully override the system prompt used for RAG reranking (advanced - prefer using results.reranking.criteria for domain-specific guidance). Google: google_search (boolean) enables Google Search grounding, google_maps (boolean) enables Google Maps grounding, code_execution (boolean) enables server-side code execution.",
549549
"additionalProperties": true
550550
},
551551
"track_usage": {

docs/providers/google/index.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,3 +89,25 @@ models:
8989
model: gemini-3-flash
9090
thinking_budget: medium # default for Flash: minimal | low | medium | high
9191
```
92+
93+
## Built-in Tools (Grounding)
94+
95+
Gemini models support built-in tools that let the model access Google Search and Google Maps
96+
directly during generation. Enable them via `provider_opts`:
97+
98+
```yaml
99+
models:
100+
gemini-grounded:
101+
provider: google
102+
model: gemini-2.5-flash
103+
provider_opts:
104+
google_search: true
105+
google_maps: true
106+
code_execution: true
107+
```
108+
109+
| Option | Description |
110+
| ---------------- | ---------------------------------------------------- |
111+
| `google_search` | Enables Google Search grounding for up-to-date info |
112+
| `google_maps` | Enables Google Maps grounding for location queries |
113+
| `code_execution` | Enables server-side code execution for computations |
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
#!/usr/bin/env docker agent run
2+
3+
models:
4+
gemini:
5+
provider: google
6+
model: gemini-3.1-flash-lite-preview
7+
provider_opts:
8+
google_search: true
9+
10+
agents:
11+
root:
12+
model: gemini
13+
description: Gemini with Google Search
14+
instruction: |
15+
You are a helpful assistant with access to the latest information via Google Search.
16+
Use grounded search results to provide accurate, up-to-date answers.

pkg/model/provider/gemini/client.go

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -442,6 +442,28 @@ func (c *Client) applyGemini25ThinkingBudget(config *genai.GenerateContentConfig
442442
slog.Debug("Gemini request using thinking_budget", "budget_tokens", tokens)
443443
}
444444

445+
// builtInTools returns Gemini built-in tools (Google Search, Google Maps,
446+
// Code Execution) enabled via provider_opts.
447+
func (c *Client) builtInTools() []*genai.Tool {
448+
entries := []struct {
449+
key string
450+
tool *genai.Tool
451+
}{
452+
{"google_search", &genai.Tool{GoogleSearch: &genai.GoogleSearch{}}},
453+
{"google_maps", &genai.Tool{GoogleMaps: &genai.GoogleMaps{}}},
454+
{"code_execution", &genai.Tool{CodeExecution: &genai.ToolCodeExecution{}}},
455+
}
456+
457+
var builtIn []*genai.Tool
458+
for _, e := range entries {
459+
if enabled, ok := providerutil.GetProviderOptBool(c.ModelConfig.ProviderOpts, e.key); ok && enabled {
460+
builtIn = append(builtIn, e.tool)
461+
slog.Debug("Gemini built-in tool enabled", "key", e.key)
462+
}
463+
}
464+
return builtIn
465+
}
466+
445467
// convertToolsToGemini converts tools to Gemini format
446468
func convertToolsToGemini(requestTools []tools.Tool) ([]*genai.Tool, error) {
447469
if len(requestTools) == 0 {
@@ -533,6 +555,9 @@ func (c *Client) CreateChatCompletionStream(
533555

534556
config := c.buildConfig()
535557

558+
// Start with Google built-in tools (search, maps, code execution) from provider_opts
559+
config.Tools = c.builtInTools()
560+
536561
// Add tools to config if provided
537562
if len(requestTools) > 0 {
538563
allTools, err := convertToolsToGemini(requestTools)
@@ -541,7 +566,7 @@ func (c *Client) CreateChatCompletionStream(
541566
return nil, err
542567
}
543568

544-
config.Tools = allTools
569+
config.Tools = append(config.Tools, allTools...)
545570

546571
// Enable function calling
547572
config.ToolConfig = &genai.ToolConfig{
@@ -550,6 +575,11 @@ func (c *Client) CreateChatCompletionStream(
550575
},
551576
}
552577

578+
// When mixing built-in tools with function calling, Gemini requires this flag
579+
if len(config.Tools) > len(allTools) {
580+
config.ToolConfig.IncludeServerSideToolInvocations = new(true)
581+
}
582+
553583
// Debug: Log the tools we're sending
554584
slog.Debug("Gemini tools config", "tools", config.Tools)
555585
for _, tool := range config.Tools {

pkg/model/provider/gemini/client_test.go

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -379,6 +379,98 @@ func TestConvertMessagesToGemini_ThoughtSignature(t *testing.T) {
379379
}
380380
}
381381

382+
func TestBuiltInTools(t *testing.T) {
383+
t.Parallel()
384+
385+
tests := []struct {
386+
name string
387+
providerOpts map[string]any
388+
wantCount int
389+
wantSearch bool
390+
wantMaps bool
391+
wantCodeExec bool
392+
}{
393+
{
394+
name: "no built-in tools by default",
395+
providerOpts: nil,
396+
wantCount: 0,
397+
},
398+
{
399+
name: "google_search enabled",
400+
providerOpts: map[string]any{"google_search": true},
401+
wantCount: 1,
402+
wantSearch: true,
403+
},
404+
{
405+
name: "google_maps enabled",
406+
providerOpts: map[string]any{"google_maps": true},
407+
wantCount: 1,
408+
wantMaps: true,
409+
},
410+
{
411+
name: "both enabled",
412+
providerOpts: map[string]any{"google_search": true, "google_maps": true},
413+
wantCount: 2,
414+
wantSearch: true,
415+
wantMaps: true,
416+
},
417+
{
418+
name: "explicitly disabled",
419+
providerOpts: map[string]any{"google_search": false, "google_maps": false},
420+
wantCount: 0,
421+
},
422+
{
423+
name: "code_execution enabled",
424+
providerOpts: map[string]any{"code_execution": true},
425+
wantCount: 1,
426+
wantCodeExec: true,
427+
},
428+
{
429+
name: "all three enabled",
430+
providerOpts: map[string]any{"google_search": true, "google_maps": true, "code_execution": true},
431+
wantCount: 3,
432+
wantSearch: true,
433+
wantMaps: true,
434+
wantCodeExec: true,
435+
},
436+
}
437+
438+
for _, tt := range tests {
439+
t.Run(tt.name, func(t *testing.T) {
440+
t.Parallel()
441+
442+
client := &Client{
443+
Config: base.Config{
444+
ModelConfig: latest.ModelConfig{
445+
Provider: "google",
446+
Model: "gemini-2.5-flash",
447+
ProviderOpts: tt.providerOpts,
448+
},
449+
},
450+
}
451+
452+
result := client.builtInTools()
453+
assert.Len(t, result, tt.wantCount)
454+
455+
var hasSearch, hasMaps, hasCodeExec bool
456+
for _, tool := range result {
457+
if tool.GoogleSearch != nil {
458+
hasSearch = true
459+
}
460+
if tool.GoogleMaps != nil {
461+
hasMaps = true
462+
}
463+
if tool.CodeExecution != nil {
464+
hasCodeExec = true
465+
}
466+
}
467+
assert.Equal(t, tt.wantSearch, hasSearch, "GoogleSearch")
468+
assert.Equal(t, tt.wantMaps, hasMaps, "GoogleMaps")
469+
assert.Equal(t, tt.wantCodeExec, hasCodeExec, "CodeExecution")
470+
})
471+
}
472+
}
473+
382474
func TestBuildConfig_ThinkingFromBudget(t *testing.T) {
383475
t.Parallel()
384476

pkg/model/provider/providerutil/provider_opts.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,28 @@ func GetProviderOptInt64(opts map[string]any, key string) (int64, bool) {
6767
}
6868
}
6969

70+
// GetProviderOptBool extracts a bool value from provider opts.
71+
func GetProviderOptBool(opts map[string]any, key string) (bool, bool) {
72+
if opts == nil {
73+
return false, false
74+
}
75+
v, ok := opts[key]
76+
if !ok {
77+
return false, false
78+
}
79+
switch b := v.(type) {
80+
case bool:
81+
return b, true
82+
default:
83+
slog.Debug("provider_opts type mismatch, ignoring",
84+
"key", key,
85+
"expected_type", "bool",
86+
"actual_type", fmt.Sprintf("%T", v),
87+
"value", v)
88+
return false, false
89+
}
90+
}
91+
7092
// samplingProviderOptsKeys lists the provider_opts keys that are
7193
// treated as sampling parameters and forwarded to provider APIs.
7294
// Provider-specific infrastructure keys (api_type, transport, region, etc.)

pkg/model/provider/providerutil/provider_opts_test.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,32 @@ func TestGetProviderOptFloat64(t *testing.T) {
3333
}
3434
}
3535

36+
func TestGetProviderOptBool(t *testing.T) {
37+
tests := []struct {
38+
name string
39+
opts map[string]any
40+
key string
41+
want bool
42+
wantOK bool
43+
}{
44+
{"nil opts", nil, "google_search", false, false},
45+
{"missing key", map[string]any{}, "google_search", false, false},
46+
{"true value", map[string]any{"google_search": true}, "google_search", true, true},
47+
{"false value", map[string]any{"google_search": false}, "google_search", false, true},
48+
{"string value", map[string]any{"google_search": "true"}, "google_search", false, false},
49+
{"int value", map[string]any{"google_search": 1}, "google_search", false, false},
50+
}
51+
for _, tt := range tests {
52+
t.Run(tt.name, func(t *testing.T) {
53+
got, ok := GetProviderOptBool(tt.opts, tt.key)
54+
assert.Equal(t, tt.wantOK, ok)
55+
if ok {
56+
assert.Equal(t, tt.want, got)
57+
}
58+
})
59+
}
60+
}
61+
3662
func TestGetProviderOptInt64(t *testing.T) {
3763
tests := []struct {
3864
name string

0 commit comments

Comments
 (0)