fix: show search provider in doctor output (#2135)

Signed-off-by: Nanook <nanookclaw@users.noreply.github.com>
Co-authored-by: Nanook <nanookclaw@users.noreply.github.com>
This commit is contained in:
nanookclaw
2026-05-26 15:30:20 +00:00
committed by GitHub
parent 5e53866cc9
commit d022d2b293
4 changed files with 254 additions and 16 deletions
+137
View File
@@ -660,6 +660,17 @@ pub enum SearchProvider {
}
impl SearchProvider {
#[must_use]
pub fn parse(value: &str) -> Option<Self> {
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 {
@@ -671,6 +682,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 {
@@ -1324,6 +1359,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
@@ -4170,6 +4234,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<OsString>,
userprofile: Option<OsString>,
+115 -5
View File
@@ -2083,6 +2083,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!();
@@ -3047,6 +3048,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": {
@@ -3156,6 +3158,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,
@@ -5155,11 +5189,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()),
tools_always_load: config.tools_always_load(),
};
@@ -5656,6 +5686,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();
+1 -6
View File
@@ -1988,12 +1988,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()),
tools_always_load: self.config.tools_always_load(),
};
+1 -5
View File
@@ -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()),
tools_always_load: config.tools_always_load(),
}