返回 CodeWhale
search.rs
根目录 / crates / tui / src / config / search.rs
1 //! Web-search provider configuration types.
2 //!
3 //! Self-contained `[search]` table types extracted verbatim from `config.rs`.
4 //! Re-exported from `crate::config` via `pub use search::*;` so existing
5 //! `crate::config::SearchProvider` (and sibling) paths resolve unchanged
6 //! (#3311).
7
8 use serde::{Deserialize, Serialize};
9
10 /// Search provider enumeration — selects the first backend `web_search` uses.
11 /// API-backed providers may visibly degrade through DuckDuckGo → Bing after
12 /// runtime failure or an empty result. Configuration and
13 /// network-policy errors fail closed without crossing providers.
14 #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Deserialize, Serialize)]
15 #[serde(rename_all = "snake_case")]
16 pub enum SearchProvider {
17 /// Bing HTML scraping. No API key needed.
18 Bing,
19 /// DuckDuckGo HTML scraping with Bing fallback. No API key needed.
20 #[serde(alias = "duckduckgo")]
21 DuckDuckGo,
22 /// Firecrawl Search API. Works keyless on Firecrawl Cloud with a bounded
23 /// per-IP quota; `[search] api_key` or `FIRECRAWL_API_KEY` raises limits.
24 #[default]
25 Firecrawl,
26 /// Tavily AI Search API (<https://tavily.com>). Requires api_key.
27 Tavily,
28 /// Bocha AI Search API (<https://bochaai.com>). Requires api_key.
29 Bocha,
30 /// Metaso AI Search API (<https://metaso.cn>). Requires `[search] api_key`
31 /// or the `METASO_API_KEY` env var.
32 #[serde(alias = "metaso")]
33 Metaso,
34 /// SearXNG JSON search API. Requires a trusted/self-hosted `base_url`.
35 #[serde(alias = "searx", alias = "searx-ng", alias = "searx_ng")]
36 Searxng,
37 /// Baidu AI Search API (<https://qianfan.baidubce.com>). Requires api_key.
38 #[serde(
39 alias = "baidu-search",
40 alias = "baidu_ai_search",
41 alias = "baidu_search",
42 alias = "baidu-ai-search"
43 )]
44 Baidu,
45 /// Volcengine Ark web_search via Responses API. Requires api_key.
46 /// Free tier: 20K queries/month per API key. Falls back to
47 /// `VOLCENGINE_API_KEY` / `VOLCENGINE_ARK_API_KEY` / `ARK_API_KEY`
48 /// env vars when `[search] api_key` is not set.
49 #[serde(
50 alias = "volcengine",
51 alias = "ark",
52 alias = "volc",
53 alias = "volcengine-ark",
54 alias = "volcengine_ark",
55 alias = "volc-ark"
56 )]
57 Volcengine,
58 /// Sofya web search API (<https://sofya.co>). Requires api_key
59 /// (`ay_live_...`). Returns full extracted page content rather than
60 /// snippets; falls back to the `SOFYA_API_KEY` env var when
61 /// `[search] api_key` is not set.
62 Sofya,
63 /// Serply Google search API (<https://serply.io>). Requires api_key;
64 /// returns Google organic results with snippets. Falls back to the
65 /// `SERPLY_API_KEY` env var when `[search] api_key` is not set.
66 Serply,
67 }
68
69 impl SearchProvider {
70 #[must_use]
71 pub fn parse(value: &str) -> Option<Self> {
72 match value.trim().to_ascii_lowercase().as_str() {
73 "bing" => Some(Self::Bing),
74 "duckduckgo" | "duck-duck-go" | "duck_duck_go" | "ddg" => Some(Self::DuckDuckGo),
75 "firecrawl" | "fire-crawl" | "fire_crawl" => Some(Self::Firecrawl),
76 "tavily" => Some(Self::Tavily),
77 "bocha" => Some(Self::Bocha),
78 "metaso" => Some(Self::Metaso),
79 "searxng" | "searx" | "searx-ng" | "searx_ng" => Some(Self::Searxng),
80 "baidu" | "baidu-search" | "baidu_search" | "baidu-ai-search" | "baidu_ai_search" => {
81 Some(Self::Baidu)
82 }
83 "volcengine" | "ark" | "volc" | "volcengine-ark" => Some(Self::Volcengine),
84 "sofya" => Some(Self::Sofya),
85 "serply" => Some(Self::Serply),
86 _ => None,
87 }
88 }
89
90 #[must_use]
91 pub fn as_str(self) -> &'static str {
92 match self {
93 Self::Bing => "bing",
94 Self::DuckDuckGo => "duckduckgo",
95 Self::Firecrawl => "firecrawl",
96 Self::Tavily => "tavily",
97 Self::Bocha => "bocha",
98 Self::Metaso => "metaso",
99 Self::Searxng => "searxng",
100 Self::Baidu => "baidu",
101 Self::Volcengine => "volcengine",
102 Self::Sofya => "sofya",
103 Self::Serply => "serply",
104 }
105 }
106
107 #[must_use]
108 pub fn names_hint() -> &'static str {
109 "bing, duckduckgo, firecrawl, tavily, bocha, metaso, searxng, baidu, volcengine, sofya, serply"
110 }
111 }
112
113 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
114 pub enum SearchProviderSource {
115 Default,
116 Config,
117 EnvOverride,
118 /// Autodetected from a Tavily key signal: `TAVILY_API_KEY`, or a generic
119 /// `[search] api_key` / `CODEWHALE_SEARCH_API_KEY` value in the `tvly-`
120 /// family. Runtime-only — resolution never writes `[search] provider`.
121 TavilyKey,
122 }
123
124 impl SearchProviderSource {
125 /// One honest source token for doctor, `/config`, and the runtime GET
126 /// route. `tavily key` names where the signal came from without claiming a
127 /// disk pin or naming `TAVILY_API_KEY` when the winner was a `tvly-`
128 /// generic key.
129 #[must_use]
130 pub fn as_str(self) -> &'static str {
131 match self {
132 Self::Default => "default",
133 Self::Config => "config",
134 Self::EnvOverride => "env override",
135 Self::TavilyKey => "tavily key",
136 }
137 }
138 }
139
140 /// Tavily issues keys in the `tvly-` family; the Tavily error copy already
141 /// says so. Applied to **generic** keys only — a dedicated `TAVILY_API_KEY`
142 /// is honored as-is, however it is shaped.
143 #[must_use]
144 pub fn looks_like_tavily_key(value: &str) -> bool {
145 value.trim().starts_with("tvly-")
146 }
147
148 /// `TAVILY_API_KEY`, read at request time. Deliberately never merged into
149 /// [`SearchConfig::api_key`]: that generic slot is shared by every provider,
150 /// so merging would hand a Tavily key to Firecrawl (or the reverse) the moment
151 /// the operator pins a different provider.
152 #[must_use]
153 pub fn tavily_env_key() -> Option<String> {
154 std::env::var("TAVILY_API_KEY")
155 .ok()
156 .map(|value| value.trim().to_string())
157 .filter(|value| !value.is_empty())
158 }
159
160 /// The key a Tavily request should send.
161 ///
162 /// Dedicated env wins over the generic `[search] api_key` so resolution and
163 /// the request agree on one key: `[search] api_key` is shared, and a
164 /// Firecrawl `fc-` / sentinel generic value must never be POSTed to
165 /// `api.tavily.com`. The generic fallback is prefix-gated, which is what
166 /// stops `CODEWHALE_SEARCH_API_KEY=doctor-offline-search-sentinel` from
167 /// autodetecting Tavily.
168 #[must_use]
169 pub fn tavily_key_from(search_api_key: Option<&str>) -> Option<String> {
170 if let Some(env_key) = tavily_env_key() {
171 return Some(env_key);
172 }
173 search_api_key
174 .map(str::trim)
175 .filter(|value| !value.is_empty() && looks_like_tavily_key(value))
176 .map(str::to_string)
177 }
178
179 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
180 pub struct SearchProviderResolution {
181 pub provider: SearchProvider,
182 pub source: SearchProviderSource,
183 }
184
185 /// Web search provider configuration (`[search]` table in config.toml).
186 #[derive(Debug, Clone, Deserialize, Default)]
187 pub struct SearchConfig {
188 /// Search provider. Default: keyless `firecrawl`.
189 #[serde(default)]
190 pub provider: Option<SearchProvider>,
191 /// Optional search endpoint. With `duckduckgo`, this is a
192 /// DuckDuckGo-compatible HTML endpoint. With `searxng`, this is the trusted
193 /// SearXNG instance root or `/search` endpoint.
194 #[serde(default)]
195 pub base_url: Option<String>,
196 /// Optional for Firecrawl; required for Tavily, Bocha, Metaso, Baidu, Volcengine, Sofya, or Serply.
197 /// Metaso also falls back to the `METASO_API_KEY` env var.
198 /// Baidu also falls back to `BAIDU_SEARCH_API_KEY` env var.
199 /// Serply also falls back to the `SERPLY_API_KEY` env var.
200 /// Volcengine also falls back to `VOLCENGINE_API_KEY` / `VOLCENGINE_ARK_API_KEY` / `ARK_API_KEY` env vars.
201 ///
202 /// This slot is shared across providers. `TAVILY_API_KEY` is **not**
203 /// merged into it — Tavily reads its dedicated env at request time
204 /// ([`tavily_key_from`]) so pinning another provider never forwards a
205 /// Tavily key, and a `tvly-` value here can autodetect Tavily without a
206 /// disk write.
207 #[serde(default)]
208 pub api_key: Option<String>,
209 }
210
210 lines RUST