Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions config.example.toml
Original file line number Diff line number Diff line change
Expand Up @@ -995,12 +995,12 @@ max_subagents = 10 # optional (default 64, clamped to 1-128)
# Choose which backend the Web tool's search uses. Default is keyless Firecrawl
# (bounded per-IP quota; `[search] api_key` or FIRECRAWL_API_KEY raises it).
# Bing and DuckDuckGo HTML scraping need no API key. Tavily, Bocha, Metaso,
# Baidu, Volcengine and Sofya need an api_key; SearXNG needs base_url.
# Baidu, Volcengine, Sofya and Serply need an api_key; SearXNG needs base_url.
# API runtime failures and empty responses visibly degrade through DuckDuckGo
# then Bing. Missing configuration and network-policy denials fail closed.
#
# [search]
# provider = "firecrawl" # firecrawl | bing | duckduckgo | tavily | bocha | metaso | searxng | baidu | volcengine | sofya
# provider = "firecrawl" # firecrawl | bing | duckduckgo | tavily | bocha | metaso | searxng | baidu | volcengine | sofya | serply
# # firecrawl: Firecrawl Cloud search, keyless with a bounded per-IP quota
# # duckduckgo: HTML scrape with Bing fallback
# # bing: HTML scrape, no API key
Expand All @@ -1016,9 +1016,12 @@ max_subagents = 10 # optional (default 64, clamped to 1-128)
# # sofya: https://sofya.co — AI search returning full page
# # content (not snippets), needs api_key (ay_live_...);
# # also falls back to the SOFYA_API_KEY env var
# # serply: https://serply.io Google organic results with
# # snippets, needs api_key;
# # also falls back to the SERPLY_API_KEY env var
# base_url = "https://search.example/" # optional DuckDuckGo-compatible HTML endpoint;
# # required SearXNG root or /search endpoint
# api_key = "YOUR_SEARCH_KEY" # required for tavily, bocha, metaso, baidu, volcengine, and sofya; optional for firecrawl (raises the keyless quota); unused by searxng
# api_key = "YOUR_SEARCH_KEY" # required for tavily, bocha, metaso, baidu, volcengine, sofya, and serply; optional for firecrawl (raises the keyless quota); unused by searxng
# # WARNING: treat config.toml like a secret file when
# # storing API keys. Prefer env vars for local smoke tests.
#
Expand All @@ -1034,6 +1037,7 @@ max_subagents = 10 # optional (default 64, clamped to 1-128)
# BAIDU_SEARCH_API_KEY → baidu key fallback
# VOLCENGINE_API_KEY / VOLCENGINE_ARK_API_KEY / ARK_API_KEY → volcengine key fallback
# SOFYA_API_KEY → sofya key fallback
# SERPLY_API_KEY → serply key fallback

# ─────────────────────────────────────────────────────────────────────────────────
# Network Policy (#135)
Expand Down
11 changes: 9 additions & 2 deletions crates/tui/src/config/search.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,10 @@ pub enum SearchProvider {
/// snippets; falls back to the `SOFYA_API_KEY` env var when
/// `[search] api_key` is not set.
Sofya,
/// Serply Google search API (<https://serply.io>). Requires api_key;
/// returns Google organic results with snippets. Falls back to the
/// `SERPLY_API_KEY` env var when `[search] api_key` is not set.
Serply,
}

impl SearchProvider {
Expand All @@ -78,6 +82,7 @@ impl SearchProvider {
}
"volcengine" | "ark" | "volc" | "volcengine-ark" => Some(Self::Volcengine),
"sofya" => Some(Self::Sofya),
"serply" => Some(Self::Serply),
_ => None,
}
}
Expand All @@ -95,12 +100,13 @@ impl SearchProvider {
Self::Baidu => "baidu",
Self::Volcengine => "volcengine",
Self::Sofya => "sofya",
Self::Serply => "serply",
}
}

#[must_use]
pub fn names_hint() -> &'static str {
"bing, duckduckgo, firecrawl, tavily, bocha, metaso, searxng, baidu, volcengine, sofya"
"bing, duckduckgo, firecrawl, tavily, bocha, metaso, searxng, baidu, volcengine, sofya, serply"
}
}

Expand Down Expand Up @@ -139,9 +145,10 @@ pub struct SearchConfig {
/// SearXNG instance root or `/search` endpoint.
#[serde(default)]
pub base_url: Option<String>,
/// Optional for Firecrawl; required for Tavily, Bocha, Metaso, Baidu, Volcengine, or Sofya.
/// Optional for Firecrawl; required for Tavily, Bocha, Metaso, Baidu, Volcengine, Sofya, or Serply.
/// Metaso also falls back to the `METASO_API_KEY` env var.
/// Baidu also falls back to `BAIDU_SEARCH_API_KEY` env var.
/// Serply also falls back to the `SERPLY_API_KEY` env var.
/// Volcengine also falls back to `VOLCENGINE_API_KEY` / `VOLCENGINE_ARK_API_KEY` / `ARK_API_KEY` env vars.
#[serde(default)]
pub api_key: Option<String>,
Expand Down
29 changes: 29 additions & 0 deletions crates/tui/src/config/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2008,6 +2008,35 @@ fn sofya_search_provider_parses_and_round_trips() {
assert_eq!(SearchProvider::Sofya.as_str(), "sofya");
}

#[test]
fn explicit_serply_search_provider_is_preserved() {
let config: Config = toml::from_str(
r#"
[search]
provider = "serply"
"#,
)
.expect("serply search config");

assert_eq!(
config.search.and_then(|search| search.provider),
Some(SearchProvider::Serply)
);
}

#[test]
fn serply_search_provider_parses_and_round_trips() {
assert_eq!(
SearchProvider::parse("serply"),
Some(SearchProvider::Serply)
);
assert_eq!(
SearchProvider::parse("Serply"),
Some(SearchProvider::Serply)
);
assert_eq!(SearchProvider::Serply.as_str(), "serply");
}

#[test]
fn live_search_provider_update_preserves_environment_precedence() {
let _guard = lock_test_env();
Expand Down
1 change: 1 addition & 0 deletions crates/tui/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2831,6 +2831,7 @@ fn is_workspace_dotenv_credential_key(key: &str) -> bool {
key,
"DEEPSEEK_SEARCH_API_KEY"
| "SOFYA_API_KEY"
| "SERPLY_API_KEY"
| "METASO_API_KEY"
| "BAIDU_SEARCH_API_KEY"
| "DEEPSEEK_SANDBOX_API_KEY"
Expand Down
8 changes: 7 additions & 1 deletion crates/tui/src/tools/web/backend.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ pub(crate) enum ConfiguredSearchBackend<'a> {
Baidu(BackendContext<'a>),
Volcengine(BackendContext<'a>),
Sofya(BackendContext<'a>),
Serply(BackendContext<'a>),
}

#[derive(Clone, Copy)]
Expand All @@ -61,6 +62,7 @@ impl<'a> ConfiguredSearchBackend<'a> {
SearchProvider::Baidu => Self::Baidu(backend),
SearchProvider::Volcengine => Self::Volcengine(backend),
SearchProvider::Sofya => Self::Sofya(backend),
SearchProvider::Serply => Self::Serply(backend),
}
}

Expand All @@ -76,6 +78,7 @@ impl<'a> ConfiguredSearchBackend<'a> {
Self::Baidu(_) => SearchProvider::Baidu,
Self::Volcengine(_) => SearchProvider::Volcengine,
Self::Sofya(_) => SearchProvider::Sofya,
Self::Serply(_) => SearchProvider::Serply,
}
}

Expand All @@ -90,7 +93,8 @@ impl<'a> ConfiguredSearchBackend<'a> {
| Self::Searxng(context)
| Self::Baidu(context)
| Self::Volcengine(context)
| Self::Sofya(context) => context,
| Self::Sofya(context)
| Self::Serply(context) => context,
}
}
}
Expand Down Expand Up @@ -288,6 +292,7 @@ impl SearchBackend for ConfiguredSearchBackend<'_> {
SearchProvider::Baidu => BackendId::Baidu,
SearchProvider::Volcengine => BackendId::Volcengine,
SearchProvider::Sofya => BackendId::Sofya,
SearchProvider::Serply => BackendId::Serply,
}
}

Expand Down Expand Up @@ -516,6 +521,7 @@ mod tests {
(SearchProvider::Baidu, BackendId::Baidu),
(SearchProvider::Volcengine, BackendId::Volcengine),
(SearchProvider::Sofya, BackendId::Sofya),
(SearchProvider::Serply, BackendId::Serply),
];

for (provider, expected) in cases {
Expand Down
2 changes: 2 additions & 0 deletions crates/tui/src/tools/web/contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ pub(crate) enum BackendId {
Baidu,
Volcengine,
Sofya,
Serply,
}

impl BackendId {
Expand All @@ -47,6 +48,7 @@ impl BackendId {
Self::Baidu => "baidu",
Self::Volcengine => "volcengine",
Self::Sofya => "sofya",
Self::Serply => "serply",
}
}
}
Expand Down
Loading