fix(tools): improve web search provider fallback (#2629)

- centralize web search provider readiness and resolution logic
- fall back when the configured provider is unavailable or invalid
- allow native-search-capable models to use built-in search without the client tool
- simplify the tools page and add direct access to web search settings
- add backend, agent, and integration tests for the new selection behavior
This commit is contained in:
wenjie
2026-04-23 15:39:16 +08:00
committed by GitHub
parent 451db2f5d8
commit cac4f21746
16 changed files with 633 additions and 222 deletions
+37 -78
View File
@@ -261,6 +261,8 @@ func buildToolSupport(cfg *config.Config) []toolSupportItem {
status, reasonCode = resolveDiscoveryToolSupport(cfg, cfg.Tools.MCP.Discovery.UseRegex)
case "tool_search_tool_bm25":
status, reasonCode = resolveDiscoveryToolSupport(cfg, cfg.Tools.MCP.Discovery.UseBM25)
case "web_search":
status, reasonCode = resolveWebSearchToolSupport(cfg)
case "i2c", "spi":
status, reasonCode = resolveHardwareToolSupport(cfg.Tools.IsToolEnabled(entry.ConfigKey))
default:
@@ -304,6 +306,13 @@ func resolveDiscoveryToolSupport(cfg *config.Config, methodEnabled bool) (string
return "enabled", ""
}
func resolveWebSearchToolSupport(cfg *config.Config) (string, string) {
if !cfg.Tools.IsToolEnabled("web") {
return "disabled", ""
}
return "enabled", ""
}
func applyToolState(cfg *config.Config, toolName string, enabled bool) error {
switch toolName {
case "read_file":
@@ -507,6 +516,7 @@ func normalizeWebSearchAPIKeys(apiKeys []string, apiKey string) ([]string, bool)
}
func buildWebSearchConfigResponse(cfg *config.Config) webSearchConfigResponse {
opts := picotools.WebSearchToolOptionsFromConfig(cfg)
current := resolveCurrentWebSearchProvider(cfg)
settings := map[string]webSearchProviderConfig{
"sogou": {
@@ -563,59 +573,53 @@ func buildWebSearchConfigResponse(cfg *config.Config) webSearchConfigResponse {
{
ID: "sogou",
Label: "Sogou",
Configured: cfg.Tools.Web.Sogou.Enabled,
Configured: picotools.WebSearchProviderReady(opts, "sogou"),
Current: current == "sogou",
},
{
ID: "duckduckgo",
Label: "DuckDuckGo",
Configured: cfg.Tools.Web.DuckDuckGo.Enabled,
Configured: picotools.WebSearchProviderReady(opts, "duckduckgo"),
Current: current == "duckduckgo",
},
{
ID: "brave",
Label: "Brave Search",
Configured: cfg.Tools.Web.Brave.Enabled &&
len(cfg.Tools.Web.Brave.APIKeys.Values()) > 0,
ID: "brave",
Label: "Brave Search",
Configured: picotools.WebSearchProviderReady(opts, "brave"),
Current: current == "brave",
RequiresAuth: true,
},
{
ID: "tavily",
Label: "Tavily",
Configured: cfg.Tools.Web.Tavily.Enabled &&
len(cfg.Tools.Web.Tavily.APIKeys.Values()) > 0,
ID: "tavily",
Label: "Tavily",
Configured: picotools.WebSearchProviderReady(opts, "tavily"),
Current: current == "tavily",
RequiresAuth: true,
},
{
ID: "perplexity",
Label: "Perplexity",
Configured: cfg.Tools.Web.Perplexity.Enabled &&
len(cfg.Tools.Web.Perplexity.APIKeys.Values()) > 0,
ID: "perplexity",
Label: "Perplexity",
Configured: picotools.WebSearchProviderReady(opts, "perplexity"),
Current: current == "perplexity",
RequiresAuth: true,
},
{
ID: "searxng",
Label: "SearXNG",
Configured: cfg.Tools.Web.SearXNG.Enabled &&
strings.TrimSpace(cfg.Tools.Web.SearXNG.BaseURL) != "",
Current: current == "searxng",
ID: "searxng",
Label: "SearXNG",
Configured: picotools.WebSearchProviderReady(opts, "searxng"),
Current: current == "searxng",
},
{
ID: "glm_search",
Label: "GLM Search",
Configured: cfg.Tools.Web.GLMSearch.Enabled &&
cfg.Tools.Web.GLMSearch.APIKey.String() != "",
ID: "glm_search",
Label: "GLM Search",
Configured: picotools.WebSearchProviderReady(opts, "glm_search"),
Current: current == "glm_search",
RequiresAuth: true,
},
{
ID: "baidu_search",
Label: "Baidu Search",
Configured: cfg.Tools.Web.BaiduSearch.Enabled &&
cfg.Tools.Web.BaiduSearch.APIKey.String() != "",
ID: "baidu_search",
Label: "Baidu Search",
Configured: picotools.WebSearchProviderReady(opts, "baidu_search"),
Current: current == "baidu_search",
RequiresAuth: true,
},
@@ -637,57 +641,12 @@ func buildWebSearchConfigResponse(cfg *config.Config) webSearchConfigResponse {
}
func resolveCurrentWebSearchProvider(cfg *config.Config) string {
selected := normalizeWebSearchProvider(cfg.Tools.Web.Provider)
if selected != "" && selected != "auto" && webSearchProviderConfigured(cfg, selected) {
return selected
if cfg == nil || !cfg.Tools.IsToolEnabled("web") {
return ""
}
for _, name := range []string{"perplexity", "brave", "searxng", "tavily"} {
if webSearchProviderConfigured(cfg, name) {
return name
}
}
if webSearchProviderConfigured(cfg, "sogou") && webSearchProviderConfigured(cfg, "duckduckgo") {
if picotools.GetPreferredWebSearchLanguage() == "en" {
return "duckduckgo"
}
return "sogou"
}
if webSearchProviderConfigured(cfg, "sogou") {
return "sogou"
}
if webSearchProviderConfigured(cfg, "duckduckgo") {
return "duckduckgo"
}
for _, name := range []string{"baidu_search", "glm_search"} {
if webSearchProviderConfigured(cfg, name) {
return name
}
}
return ""
}
func webSearchProviderConfigured(cfg *config.Config, name string) bool {
switch name {
case "sogou":
return cfg.Tools.Web.Sogou.Enabled
case "duckduckgo":
return cfg.Tools.Web.DuckDuckGo.Enabled
case "brave":
return cfg.Tools.Web.Brave.Enabled && len(cfg.Tools.Web.Brave.APIKeys.Values()) > 0
case "tavily":
return cfg.Tools.Web.Tavily.Enabled && len(cfg.Tools.Web.Tavily.APIKeys.Values()) > 0
case "perplexity":
return cfg.Tools.Web.Perplexity.Enabled && len(cfg.Tools.Web.Perplexity.APIKeys.Values()) > 0
case "searxng":
return cfg.Tools.Web.SearXNG.Enabled && strings.TrimSpace(cfg.Tools.Web.SearXNG.BaseURL) != ""
case "glm_search":
return cfg.Tools.Web.GLMSearch.Enabled && cfg.Tools.Web.GLMSearch.APIKey.String() != ""
case "baidu_search":
return cfg.Tools.Web.BaiduSearch.Enabled && cfg.Tools.Web.BaiduSearch.APIKey.String() != ""
default:
return false
selected, err := picotools.ResolveWebSearchProviderName(picotools.WebSearchToolOptionsFromConfig(cfg), "")
if err != nil {
return ""
}
return selected
}