feat(providers): add gemini web search provider (#2763)

* add gemini web search provider

* fix(web): prefer free providers before Gemini in auto mode

* fix(web): expose gemini api key and model settings

* fix(web): prefer configured providers before Gemini in auto mode

* fix(web): satisfy gemini lint checks

* fix(web): address gemini provider review feedback

* test(web): align auto-provider expectations

* fix(web): let gemini ignore search range
This commit is contained in:
Anton Bogdanovich
2026-05-13 18:50:47 -07:00
committed by GitHub
parent eb0653074b
commit 794eb04f32
12 changed files with 431 additions and 15 deletions
+23 -1
View File
@@ -49,6 +49,7 @@ type webSearchProviderConfig struct {
BaseURL string `json:"base_url,omitempty"`
APIKey string `json:"api_key,omitempty"`
APIKeys []string `json:"api_keys,omitempty"`
Model string `json:"model,omitempty"`
APIKeySet bool `json:"api_key_set,omitempty"`
}
@@ -446,6 +447,14 @@ func (h *Handler) handleUpdateWebSearchConfig(w http.ResponseWriter, r *http.Req
cfg.Tools.Web.DuckDuckGo.Enabled = settings.Enabled
cfg.Tools.Web.DuckDuckGo.MaxResults = settings.MaxResults
}
if settings, ok := req.Settings["gemini"]; ok {
cfg.Tools.Web.Gemini.Enabled = settings.Enabled
cfg.Tools.Web.Gemini.MaxResults = settings.MaxResults
cfg.Tools.Web.Gemini.Model = strings.TrimSpace(settings.Model)
if key := strings.TrimSpace(settings.APIKey); key != "" {
cfg.Tools.Web.Gemini.APIKey = *config.NewSecureString(key)
}
}
if settings, ok := req.Settings["brave"]; ok {
cfg.Tools.Web.Brave.Enabled = settings.Enabled
cfg.Tools.Web.Brave.MaxResults = settings.MaxResults
@@ -505,7 +514,7 @@ func normalizeWebSearchProvider(provider string) string {
switch strings.ToLower(strings.TrimSpace(provider)) {
case "", "auto":
return "auto"
case "sogou", "brave", "tavily", "duckduckgo", "perplexity", "searxng", "glm_search", "baidu_search":
case "sogou", "brave", "tavily", "duckduckgo", "gemini", "perplexity", "searxng", "glm_search", "baidu_search":
return strings.ToLower(strings.TrimSpace(provider))
default:
return ""
@@ -549,6 +558,12 @@ func buildWebSearchConfigResponse(cfg *config.Config) webSearchConfigResponse {
Enabled: cfg.Tools.Web.DuckDuckGo.Enabled,
MaxResults: cfg.Tools.Web.DuckDuckGo.MaxResults,
},
"gemini": {
Enabled: cfg.Tools.Web.Gemini.Enabled,
MaxResults: cfg.Tools.Web.Gemini.MaxResults,
Model: cfg.Tools.Web.Gemini.Model,
APIKeySet: cfg.Tools.Web.Gemini.APIKey.String() != "",
},
"brave": {
Enabled: cfg.Tools.Web.Brave.Enabled,
MaxResults: cfg.Tools.Web.Brave.MaxResults,
@@ -604,6 +619,13 @@ func buildWebSearchConfigResponse(cfg *config.Config) webSearchConfigResponse {
Configured: picotools.WebSearchProviderReady(opts, "duckduckgo"),
Current: current == "duckduckgo",
},
{
ID: "gemini",
Label: "Gemini (Google Search)",
Configured: picotools.WebSearchProviderReady(opts, "gemini"),
Current: current == "gemini",
RequiresAuth: true,
},
{
ID: "brave",
Label: "Brave Search",
+1 -1
View File
@@ -540,7 +540,7 @@ func TestHandleUpdateWebSearchConfig_PreservesAndReplacesMultiKeys(t *testing.T)
}
}
func TestResolveCurrentWebSearchProvider_PrefersConfiguredProvidersBeforeSogou(t *testing.T) {
func TestResolveCurrentWebSearchProvider_PrefersConfiguredProvidersInAutoMode(t *testing.T) {
cfg := config.DefaultConfig()
cfg.Tools.Web.Provider = "auto"
cfg.Tools.Web.Sogou.Enabled = true
+1
View File
@@ -30,6 +30,7 @@ export interface WebSearchProviderConfig {
max_results: number
base_url?: string
api_key?: string
model?: string
api_key_set?: boolean
}
@@ -30,10 +30,13 @@ const apiKeyProviders = new Set([
"brave",
"tavily",
"perplexity",
"gemini",
"glm_search",
"baidu_search",
])
const modelProviders = new Set(["gemini"])
export function WebSearchProviderSettings({
providerLabelMap,
settings,
@@ -226,6 +229,27 @@ function ProviderCard({
/>
</ProviderField>
)}
{modelProviders.has(providerId) && (
<ProviderField
label={t("pages.agent.tools.web_search.model", "Model")}
>
<Input
value={settings.model ?? ""}
onChange={(event) =>
updateSettings((current) => ({
...current,
model: event.target.value,
}))
}
placeholder={t(
"pages.agent.tools.web_search.model_placeholder",
"Optional model override",
)}
className="bg-muted/40 hover:bg-muted/60 focus:bg-background focus:ring-primary/20 h-10 rounded-xl border-transparent shadow-none transition-colors"
/>
</ProviderField>
)}
</div>
</div>
)}