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:
@@ -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
@@ -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();
|
||||
|
||||
@@ -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(),
|
||||
};
|
||||
|
||||
@@ -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(),
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user