From 9d28a254d8ea601d11658ebda26ea27545383e16 Mon Sep 17 00:00:00 2001 From: Nanook Date: Mon, 25 May 2026 13:40:11 +0000 Subject: [PATCH] fix: show search provider in doctor output Signed-off-by: Nanook --- crates/tui/src/config.rs | 137 ++++++++++++++++++++++++++++++ crates/tui/src/main.rs | 120 ++++++++++++++++++++++++-- crates/tui/src/runtime_threads.rs | 7 +- crates/tui/src/tui/ui.rs | 6 +- 4 files changed, 254 insertions(+), 16 deletions(-) diff --git a/crates/tui/src/config.rs b/crates/tui/src/config.rs index b41712557..f1346314c 100644 --- a/crates/tui/src/config.rs +++ b/crates/tui/src/config.rs @@ -616,6 +616,17 @@ pub enum SearchProvider { } impl SearchProvider { + #[must_use] + pub fn parse(value: &str) -> Option { + match value.trim().to_ascii_lowercase().as_str() { + "bing" => Some(Self::Bing), + "duckduckgo" | "duck-duck-go" | "duck_duck_go" | "ddg" => Some(Self::DuckDuckGo), + "tavily" => Some(Self::Tavily), + "bocha" => Some(Self::Bocha), + _ => None, + } + } + #[must_use] pub fn as_str(self) -> &'static str { match self { @@ -627,6 +638,30 @@ impl SearchProvider { } } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum SearchProviderSource { + Default, + Config, + EnvOverride, +} + +impl SearchProviderSource { + #[must_use] + pub fn as_str(self) -> &'static str { + match self { + Self::Default => "default", + Self::Config => "config", + Self::EnvOverride => "env override", + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct SearchProviderResolution { + pub provider: SearchProvider, + pub source: SearchProviderSource, +} + /// Web search provider configuration (`[search]` table in config.toml). #[derive(Debug, Clone, Deserialize, Default)] pub struct SearchConfig { @@ -1258,6 +1293,35 @@ struct RequirementsFile { // === Config Loading === impl Config { + #[must_use] + pub fn search_provider_resolution(&self) -> SearchProviderResolution { + if let Ok(raw) = std::env::var("DEEPSEEK_SEARCH_PROVIDER") + && let Some(provider) = SearchProvider::parse(&raw) + { + return SearchProviderResolution { + provider, + source: SearchProviderSource::EnvOverride, + }; + } + + if let Some(provider) = self.search.as_ref().and_then(|search| search.provider) { + return SearchProviderResolution { + provider, + source: SearchProviderSource::Config, + }; + } + + SearchProviderResolution { + provider: SearchProvider::default(), + source: SearchProviderSource::Default, + } + } + + #[must_use] + pub fn search_provider(&self) -> SearchProvider { + self.search_provider_resolution().provider + } + /// Return `true` if the `[auto] cost_saving = true` opt-in is set /// (#1207). When true, the auto-mode router biases toward /// `deepseek-v4-flash` for ambiguous requests instead of escalating to @@ -3698,6 +3762,79 @@ mod tests { ); } + #[test] + fn search_provider_resolution_reports_default_source() { + let _guard = lock_test_env(); + let prev = env::var_os("DEEPSEEK_SEARCH_PROVIDER"); + unsafe { env::remove_var("DEEPSEEK_SEARCH_PROVIDER") }; + + let resolution = Config::default().search_provider_resolution(); + + unsafe { EnvGuard::restore_var("DEEPSEEK_SEARCH_PROVIDER", prev) }; + assert_eq!(resolution.provider, SearchProvider::Bing); + assert_eq!(resolution.source, SearchProviderSource::Default); + } + + #[test] + fn search_provider_resolution_reports_config_source() { + let _guard = lock_test_env(); + let prev = env::var_os("DEEPSEEK_SEARCH_PROVIDER"); + unsafe { env::remove_var("DEEPSEEK_SEARCH_PROVIDER") }; + let config: Config = toml::from_str( + r#" + [search] + provider = "tavily" + "#, + ) + .expect("search config"); + + let resolution = config.search_provider_resolution(); + + unsafe { EnvGuard::restore_var("DEEPSEEK_SEARCH_PROVIDER", prev) }; + assert_eq!(resolution.provider, SearchProvider::Tavily); + assert_eq!(resolution.source, SearchProviderSource::Config); + } + + #[test] + fn search_provider_resolution_reports_env_override_source() { + let _guard = lock_test_env(); + let prev = env::var_os("DEEPSEEK_SEARCH_PROVIDER"); + unsafe { env::set_var("DEEPSEEK_SEARCH_PROVIDER", "bocha") }; + let config: Config = toml::from_str( + r#" + [search] + provider = "duckduckgo" + "#, + ) + .expect("search config"); + + let resolution = config.search_provider_resolution(); + + unsafe { EnvGuard::restore_var("DEEPSEEK_SEARCH_PROVIDER", prev) }; + assert_eq!(resolution.provider, SearchProvider::Bocha); + assert_eq!(resolution.source, SearchProviderSource::EnvOverride); + } + + #[test] + fn search_provider_resolution_ignores_invalid_env_override() { + let _guard = lock_test_env(); + let prev = env::var_os("DEEPSEEK_SEARCH_PROVIDER"); + unsafe { env::set_var("DEEPSEEK_SEARCH_PROVIDER", "not-a-provider") }; + let config: Config = toml::from_str( + r#" + [search] + provider = "tavily" + "#, + ) + .expect("search config"); + + let resolution = config.search_provider_resolution(); + + unsafe { EnvGuard::restore_var("DEEPSEEK_SEARCH_PROVIDER", prev) }; + assert_eq!(resolution.provider, SearchProvider::Tavily); + assert_eq!(resolution.source, SearchProviderSource::Config); + } + struct EnvGuard { home: Option, userprofile: Option, diff --git a/crates/tui/src/main.rs b/crates/tui/src/main.rs index 34cb7fce7..e1147cdc1 100644 --- a/crates/tui/src/main.rs +++ b/crates/tui/src/main.rs @@ -2072,6 +2072,7 @@ async fn run_doctor(config: &Config, workspace: &Path, config_path_override: Opt ); } println!(" workspace: {}", crate::utils::display_path(workspace)); + println!(" {}", doctor_search_provider_line(config)); // State root (v0.8.44) println!(); @@ -3031,6 +3032,7 @@ fn run_doctor_json( "message": strict_tool_mode.message, "recommended_base_url": strict_tool_mode.recommended_base_url, }, + "search_provider": doctor_search_provider_json(config), "memory": memory_summary, "mcp": mcp_summary, "skills": { @@ -3140,6 +3142,38 @@ fn provider_capability_report(config: &Config) -> serde_json::Value { }) } +fn doctor_search_provider_line(config: &Config) -> String { + let search_provider = config.search_provider_resolution(); + let switch_hint = if matches!( + (search_provider.provider, search_provider.source), + ( + crate::config::SearchProvider::Bing, + crate::config::SearchProviderSource::Default + ) + ) { + "; set [search] provider = \"duckduckgo\" | \"tavily\" | \"bocha\" to switch" + } else { + "" + }; + + format!( + "search_provider: {} (source: {}{})", + search_provider.provider.as_str(), + search_provider.source.as_str(), + switch_hint + ) +} + +fn doctor_search_provider_json(config: &Config) -> serde_json::Value { + use serde_json::json; + + let search_provider = config.search_provider_resolution(); + json!({ + "provider": search_provider.provider.as_str(), + "source": search_provider.source.as_str(), + }) +} + #[derive(Debug, Clone, PartialEq, Eq)] struct DoctorApiTarget { provider: &'static str, @@ -5130,11 +5164,7 @@ async fn run_exec_agent( .tag() .to_string(), workshop: config.workshop.clone(), - search_provider: config - .search - .as_ref() - .and_then(|s| s.provider) - .unwrap_or_default(), + search_provider: config.search_provider(), search_api_key: config.search.as_ref().and_then(|s| s.api_key.clone()), }; @@ -5630,6 +5660,86 @@ mod doctor_endpoint_tests { assert!(report["alias_deprecation"].is_null()); } + #[test] + fn doctor_search_provider_line_includes_default_source_and_switch_hint() { + let _guard = crate::test_support::lock_test_env(); + let prev = std::env::var_os("DEEPSEEK_SEARCH_PROVIDER"); + unsafe { std::env::remove_var("DEEPSEEK_SEARCH_PROVIDER") }; + + let line = doctor_search_provider_line(&Config::default()); + + match prev { + Some(value) => unsafe { std::env::set_var("DEEPSEEK_SEARCH_PROVIDER", value) }, + None => unsafe { std::env::remove_var("DEEPSEEK_SEARCH_PROVIDER") }, + } + assert!(line.contains("search_provider: bing")); + assert!(line.contains("source: default")); + assert!(line.contains("[search] provider")); + } + + #[test] + fn doctor_search_provider_json_reports_config_source() { + let _guard = crate::test_support::lock_test_env(); + let prev = std::env::var_os("DEEPSEEK_SEARCH_PROVIDER"); + unsafe { std::env::remove_var("DEEPSEEK_SEARCH_PROVIDER") }; + let config = Config { + search: Some(crate::config::SearchConfig { + provider: Some(crate::config::SearchProvider::DuckDuckGo), + api_key: None, + }), + ..Default::default() + }; + + let report = doctor_search_provider_json(&config); + + match prev { + Some(value) => unsafe { std::env::set_var("DEEPSEEK_SEARCH_PROVIDER", value) }, + None => unsafe { std::env::remove_var("DEEPSEEK_SEARCH_PROVIDER") }, + } + assert_eq!(report["provider"], "duckduckgo"); + assert_eq!(report["source"], "config"); + } + + #[test] + fn doctor_search_provider_json_reports_env_override_source() { + let _guard = crate::test_support::lock_test_env(); + let prev = std::env::var_os("DEEPSEEK_SEARCH_PROVIDER"); + unsafe { std::env::set_var("DEEPSEEK_SEARCH_PROVIDER", "tavily") }; + + let report = doctor_search_provider_json(&Config::default()); + + match prev { + Some(value) => unsafe { std::env::set_var("DEEPSEEK_SEARCH_PROVIDER", value) }, + None => unsafe { std::env::remove_var("DEEPSEEK_SEARCH_PROVIDER") }, + } + assert_eq!(report["provider"], "tavily"); + assert_eq!(report["source"], "env override"); + } + + #[test] + fn doctor_search_provider_line_omits_switch_hint_when_bing_is_configured() { + let _guard = crate::test_support::lock_test_env(); + let prev = std::env::var_os("DEEPSEEK_SEARCH_PROVIDER"); + unsafe { std::env::remove_var("DEEPSEEK_SEARCH_PROVIDER") }; + let config = Config { + search: Some(crate::config::SearchConfig { + provider: Some(crate::config::SearchProvider::Bing), + api_key: None, + }), + ..Default::default() + }; + + let line = doctor_search_provider_line(&config); + + match prev { + Some(value) => unsafe { std::env::set_var("DEEPSEEK_SEARCH_PROVIDER", value) }, + None => unsafe { std::env::remove_var("DEEPSEEK_SEARCH_PROVIDER") }, + } + assert!(line.contains("search_provider: bing")); + assert!(line.contains("source: config")); + assert!(!line.contains("[search] provider")); + } + #[test] fn timeout_recovery_keeps_default_deepseek_users_on_default_endpoint() { let config = Config::default(); diff --git a/crates/tui/src/runtime_threads.rs b/crates/tui/src/runtime_threads.rs index 1a08473d6..9d57edc26 100644 --- a/crates/tui/src/runtime_threads.rs +++ b/crates/tui/src/runtime_threads.rs @@ -1987,12 +1987,7 @@ impl RuntimeThreadManager { .tag() .to_string(), workshop: self.config.workshop.clone(), - search_provider: self - .config - .search - .as_ref() - .and_then(|s| s.provider) - .unwrap_or_default(), + search_provider: self.config.search_provider(), search_api_key: self.config.search.as_ref().and_then(|s| s.api_key.clone()), }; diff --git a/crates/tui/src/tui/ui.rs b/crates/tui/src/tui/ui.rs index 25e1fdb11..4d8c7b7fa 100644 --- a/crates/tui/src/tui/ui.rs +++ b/crates/tui/src/tui/ui.rs @@ -722,11 +722,7 @@ fn build_engine_config(app: &App, config: &Config) -> EngineConfig { goal_objective: app.goal.goal_objective.clone(), locale_tag: app.ui_locale.tag().to_string(), workshop: config.workshop.clone(), - search_provider: config - .search - .as_ref() - .and_then(|s| s.provider) - .unwrap_or_default(), + search_provider: config.search_provider(), search_api_key: config.search.as_ref().and_then(|s| s.api_key.clone()), } }