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
+143
View File
@@ -198,6 +198,66 @@ func TestHandleUpdateToolState(t *testing.T) {
}
}
func TestHandleListTools_ReportsWebSearchEnabledWhenToolIsOn(t *testing.T) {
tests := []struct {
name string
preferNative bool
}{
{name: "without prefer_native", preferNative: false},
{name: "with prefer_native", preferNative: true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
configPath, cleanup := setupOAuthTestEnv(t)
defer cleanup()
cfg, err := config.LoadConfig(configPath)
if err != nil {
t.Fatalf("LoadConfig() error = %v", err)
}
cfg.Tools.Web.PreferNative = tt.preferNative
cfg.Tools.Web.Provider = "brave"
cfg.Tools.Web.Sogou.Enabled = false
cfg.Tools.Web.DuckDuckGo.Enabled = false
cfg.Tools.Web.Brave.Enabled = true
cfg.Tools.Web.Brave.SetAPIKeys(nil)
if err := config.SaveConfig(configPath, cfg); err != nil {
t.Fatalf("SaveConfig() error = %v", err)
}
h := NewHandler(configPath)
mux := http.NewServeMux()
h.RegisterRoutes(mux)
rec := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/api/tools", nil)
mux.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("status = %d, want %d, body=%s", rec.Code, http.StatusOK, rec.Body.String())
}
var resp toolSupportResponse
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
t.Fatalf("Unmarshal() error = %v", err)
}
for _, tool := range resp.Tools {
if tool.Name != "web_search" {
continue
}
if tool.Status != "enabled" || tool.ReasonCode != "" {
t.Fatalf("web_search = %#v, want enabled with no reason code", tool)
}
return
}
t.Fatal("expected web_search in response")
})
}
}
func TestHandleGetWebSearchConfig(t *testing.T) {
configPath, cleanup := setupOAuthTestEnv(t)
defer cleanup()
@@ -206,6 +266,7 @@ func TestHandleGetWebSearchConfig(t *testing.T) {
if err != nil {
t.Fatalf("LoadConfig() error = %v", err)
}
cfg.Tools.Web.PreferNative = false
cfg.Tools.Web.Provider = "sogou"
cfg.Tools.Web.Sogou.Enabled = true
cfg.Tools.Web.Sogou.MaxResults = 6
@@ -242,6 +303,48 @@ func TestHandleGetWebSearchConfig(t *testing.T) {
}
}
func TestHandleGetWebSearchConfig_DoesNotExposeNativeAsCurrentService(t *testing.T) {
configPath, cleanup := setupOAuthTestEnv(t)
defer cleanup()
cfg, err := config.LoadConfig(configPath)
if err != nil {
t.Fatalf("LoadConfig() error = %v", err)
}
cfg.Tools.Web.PreferNative = true
cfg.Tools.Web.Provider = "brave"
cfg.Tools.Web.Sogou.Enabled = false
cfg.Tools.Web.DuckDuckGo.Enabled = false
cfg.Tools.Web.Brave.Enabled = true
cfg.Tools.Web.Brave.SetAPIKeys(nil)
if err := config.SaveConfig(configPath, cfg); err != nil {
t.Fatalf("SaveConfig() error = %v", err)
}
h := NewHandler(configPath)
mux := http.NewServeMux()
h.RegisterRoutes(mux)
rec := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/api/tools/web-search-config", nil)
mux.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("status = %d, want %d, body=%s", rec.Code, http.StatusOK, rec.Body.String())
}
var resp webSearchConfigResponse
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
t.Fatalf("Unmarshal() error = %v", err)
}
if !resp.PreferNative {
t.Fatal("prefer_native should remain true in response")
}
if resp.CurrentService != "" {
t.Fatalf("current_service = %q, want empty when no external provider is ready", resp.CurrentService)
}
}
func TestHandleUpdateWebSearchConfig(t *testing.T) {
configPath, cleanup := setupOAuthTestEnv(t)
defer cleanup()
@@ -393,6 +496,27 @@ func TestResolveCurrentWebSearchProvider_PrefersConfiguredProvidersBeforeSogou(t
}
}
func TestResolveCurrentWebSearchProvider_FallsBackWhenExplicitProviderUnavailable(t *testing.T) {
cfg := config.DefaultConfig()
cfg.Tools.Web.Provider = "brave"
cfg.Tools.Web.Brave.Enabled = true
cfg.Tools.Web.Sogou.Enabled = true
if got := resolveCurrentWebSearchProvider(cfg); got != "sogou" {
t.Fatalf("resolveCurrentWebSearchProvider() = %q, want sogou", got)
}
}
func TestResolveCurrentWebSearchProvider_FallsBackWhenProviderIsUnknown(t *testing.T) {
cfg := config.DefaultConfig()
cfg.Tools.Web.Provider = "totally_unknown"
cfg.Tools.Web.Sogou.Enabled = true
if got := resolveCurrentWebSearchProvider(cfg); got != "sogou" {
t.Fatalf("resolveCurrentWebSearchProvider() = %q, want sogou", got)
}
}
func TestResolveCurrentWebSearchProvider_UsesPreferredLanguageForSogouAndDuckDuckGo(t *testing.T) {
cfg := config.DefaultConfig()
cfg.Tools.Web.Provider = "auto"
@@ -413,3 +537,22 @@ func TestResolveCurrentWebSearchProvider_UsesPreferredLanguageForSogouAndDuckDuc
t.Fatalf("resolveCurrentWebSearchProvider() = %q, want sogou", got)
}
}
func TestResolveCurrentWebSearchProvider_IgnoresPreferNativeInConfigView(t *testing.T) {
cfg := config.DefaultConfig()
cfg.ModelList = []*config.ModelConfig{{
ModelName: "custom-default",
Model: "openai/gpt-4o",
APIKeys: config.SimpleSecureStrings("sk-default"),
}}
cfg.Agents.Defaults.ModelName = "custom-default"
cfg.Tools.Web.PreferNative = true
cfg.Tools.Web.Provider = "brave"
cfg.Tools.Web.Sogou.Enabled = false
cfg.Tools.Web.DuckDuckGo.Enabled = false
cfg.Tools.Web.Brave.Enabled = true
if got := resolveCurrentWebSearchProvider(cfg); got != "" {
t.Fatalf("resolveCurrentWebSearchProvider() = %q, want empty when only native search would be available", got)
}
}