lc/search/
providers.rs

1use serde::{Deserialize, Serialize};
2use std::collections::HashMap;
3
4#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
5#[serde(rename_all = "lowercase")]
6pub enum SearchProviderType {
7    Brave,
8    Exa,
9    Serper,
10    SerpApi,
11    DuckDuckGo,
12    Jina,
13    Tavily,
14}
15
16#[derive(Debug, Serialize, Deserialize, Clone)]
17pub struct SearchProviderConfig {
18    pub url: String,
19    pub provider_type: SearchProviderType,
20    #[serde(default)]
21    pub headers: HashMap<String, String>,
22}
23
24impl SearchProviderConfig {
25    #[allow(dead_code)]
26    pub fn new(url: String, provider_type: SearchProviderType) -> Self {
27        Self {
28            url,
29            provider_type,
30            headers: HashMap::new(),
31        }
32    }
33}
34
35impl SearchProviderType {
36    /// Auto-detect provider type from URL
37    pub fn detect_from_url(url: &str) -> anyhow::Result<Self> {
38        let url_lower = url.to_lowercase();
39
40        if url_lower.contains("api.search.brave.com") {
41            Ok(SearchProviderType::Brave)
42        } else if url_lower.contains("api.exa.ai") || url_lower.contains("exa.ai") {
43            Ok(SearchProviderType::Exa)
44        } else if url_lower.contains("google.serper.dev") || url_lower.contains("serper.dev") {
45            Ok(SearchProviderType::Serper)
46        } else if url_lower.contains("serpapi.com") {
47            Ok(SearchProviderType::SerpApi)
48        } else if url_lower.contains("duckduckgo.com") || url_lower.contains("api.duckduckgo.com") {
49            Ok(SearchProviderType::DuckDuckGo)
50        } else if url_lower.contains("jina.ai") || url_lower.contains("s.jina.ai") {
51            Ok(SearchProviderType::Jina)
52        } else if url_lower.contains("api.tavily.com") || url_lower.contains("tavily.com") {
53            Ok(SearchProviderType::Tavily)
54        } else {
55            anyhow::bail!(
56                "Cannot auto-detect provider type from URL '{}'. \
57                Supported providers:\n\
58                - Brave: api.search.brave.com\n\
59                - Exa: api.exa.ai\n\
60                - Serper: google.serper.dev\n\
61                - SerpApi: serpapi.com\n\
62                - DuckDuckGo: api.duckduckgo.com\n\
63                - Jina: s.jina.ai\n\
64                - Tavily: api.tavily.com",
65                url
66            )
67        }
68    }
69
70    /// Get the correct API key header name for this provider type
71    pub fn api_key_header(&self) -> &'static str {
72        match self {
73            SearchProviderType::Brave => "X-Subscription-Token",
74            SearchProviderType::Exa => "x-api-key",
75            SearchProviderType::Serper => "X-API-KEY",
76            SearchProviderType::SerpApi => "api_key",
77            SearchProviderType::DuckDuckGo => "", // No API key required
78            SearchProviderType::Jina => "Authorization",
79            SearchProviderType::Tavily => "Authorization",
80        }
81    }
82}
83
84#[allow(dead_code)]
85pub trait SearchProvider {
86    fn name(&self) -> &str;
87    fn search(
88        &self,
89        query: &str,
90        count: Option<usize>,
91    ) -> impl std::future::Future<Output = anyhow::Result<super::SearchResults>> + Send;
92}