Skip to main content

spn_core/
providers.rs

1//! Provider definitions for LLM and MCP services.
2//!
3//! This module is the **single source of truth** for all provider metadata
4//! across the SuperNovae ecosystem.
5
6/// Category of provider service.
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
8pub enum ProviderCategory {
9    /// LLM inference providers (Anthropic, OpenAI, etc.)
10    Llm,
11    /// MCP service providers (Neo4j, GitHub, etc.)
12    Mcp,
13    /// Local model runners (Ollama)
14    Local,
15}
16
17/// Provider metadata.
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
19pub struct Provider {
20    /// Unique identifier (e.g., "anthropic", "openai")
21    pub id: &'static str,
22    /// Human-readable name (e.g., "Anthropic Claude")
23    pub name: &'static str,
24    /// Environment variable name (e.g., "ANTHROPIC_API_KEY")
25    pub env_var: &'static str,
26    /// Expected key prefix for validation (e.g., "sk-ant-")
27    pub key_prefix: Option<&'static str>,
28    /// Provider category
29    pub category: ProviderCategory,
30    /// Whether this provider requires an API key
31    pub requires_key: bool,
32    /// Description of the provider
33    pub description: &'static str,
34}
35
36/// All known providers in the SuperNovae ecosystem.
37///
38/// This constant is the **single source of truth** for provider definitions.
39/// It replaces the duplicated PROVIDERS arrays in nika and spn.
40pub static KNOWN_PROVIDERS: &[Provider] = &[
41    // ==================== LLM Providers ====================
42    Provider {
43        id: "anthropic",
44        name: "Anthropic Claude",
45        env_var: "ANTHROPIC_API_KEY",
46        key_prefix: Some("sk-ant-"),
47        category: ProviderCategory::Llm,
48        requires_key: true,
49        description: "Claude models (Opus, Sonnet, Haiku)",
50    },
51    Provider {
52        id: "openai",
53        name: "OpenAI GPT",
54        env_var: "OPENAI_API_KEY",
55        key_prefix: Some("sk-"),
56        category: ProviderCategory::Llm,
57        requires_key: true,
58        description: "GPT-4, GPT-3.5, and other OpenAI models",
59    },
60    Provider {
61        id: "mistral",
62        name: "Mistral AI",
63        env_var: "MISTRAL_API_KEY",
64        key_prefix: None,
65        category: ProviderCategory::Llm,
66        requires_key: true,
67        description: "Mistral and Mixtral models",
68    },
69    Provider {
70        id: "groq",
71        name: "Groq",
72        env_var: "GROQ_API_KEY",
73        key_prefix: Some("gsk_"),
74        category: ProviderCategory::Llm,
75        requires_key: true,
76        description: "Ultra-fast inference with Groq LPU",
77    },
78    Provider {
79        id: "deepseek",
80        name: "DeepSeek",
81        env_var: "DEEPSEEK_API_KEY",
82        key_prefix: Some("sk-"),
83        category: ProviderCategory::Llm,
84        requires_key: true,
85        description: "DeepSeek Coder and Chat models",
86    },
87    Provider {
88        id: "gemini",
89        name: "Google Gemini",
90        env_var: "GEMINI_API_KEY",
91        key_prefix: None,
92        category: ProviderCategory::Llm,
93        requires_key: true,
94        description: "Gemini Pro and Ultra models",
95    },
96    Provider {
97        id: "ollama",
98        name: "Ollama",
99        env_var: "OLLAMA_API_BASE_URL",
100        key_prefix: None,
101        category: ProviderCategory::Local,
102        requires_key: false,
103        description: "Local model runner (llama, mistral, etc.)",
104    },
105    // ==================== MCP Service Providers ====================
106    Provider {
107        id: "neo4j",
108        name: "Neo4j Graph Database",
109        env_var: "NEO4J_PASSWORD",
110        key_prefix: None,
111        category: ProviderCategory::Mcp,
112        requires_key: true,
113        description: "Graph database for knowledge storage",
114    },
115    Provider {
116        id: "github",
117        name: "GitHub API",
118        env_var: "GITHUB_TOKEN",
119        key_prefix: Some("ghp_"),
120        category: ProviderCategory::Mcp,
121        requires_key: true,
122        description: "GitHub API access",
123    },
124    Provider {
125        id: "slack",
126        name: "Slack API",
127        env_var: "SLACK_BOT_TOKEN",
128        key_prefix: Some("xoxb-"),
129        category: ProviderCategory::Mcp,
130        requires_key: true,
131        description: "Slack workspace integration",
132    },
133    Provider {
134        id: "perplexity",
135        name: "Perplexity AI",
136        env_var: "PERPLEXITY_API_KEY",
137        key_prefix: Some("pplx-"),
138        category: ProviderCategory::Mcp,
139        requires_key: true,
140        description: "AI-powered web search",
141    },
142    Provider {
143        id: "firecrawl",
144        name: "Firecrawl",
145        env_var: "FIRECRAWL_API_KEY",
146        key_prefix: Some("fc-"),
147        category: ProviderCategory::Mcp,
148        requires_key: true,
149        description: "Web scraping and crawling",
150    },
151    Provider {
152        id: "supadata",
153        name: "Supadata API",
154        env_var: "SUPADATA_API_KEY",
155        key_prefix: None,
156        category: ProviderCategory::Mcp,
157        requires_key: true,
158        description: "Video transcription and web scraping",
159    },
160];
161
162/// Find a provider by ID (case-insensitive).
163///
164/// # Example
165///
166/// ```
167/// use spn_core::find_provider;
168///
169/// let provider = find_provider("anthropic").unwrap();
170/// assert_eq!(provider.env_var, "ANTHROPIC_API_KEY");
171///
172/// let provider = find_provider("OPENAI").unwrap();
173/// assert_eq!(provider.id, "openai");
174/// ```
175#[must_use]
176pub fn find_provider(id: &str) -> Option<&'static Provider> {
177    KNOWN_PROVIDERS.iter().find(|p| p.id.eq_ignore_ascii_case(id))
178}
179
180/// Get the environment variable name for a provider.
181///
182/// # Example
183///
184/// ```
185/// use spn_core::provider_to_env_var;
186///
187/// assert_eq!(provider_to_env_var("anthropic"), Some("ANTHROPIC_API_KEY"));
188/// assert_eq!(provider_to_env_var("unknown"), None);
189/// ```
190pub fn provider_to_env_var(id: &str) -> Option<&'static str> {
191    find_provider(id).map(|p| p.env_var)
192}
193
194/// Get all providers in a specific category.
195///
196/// # Example
197///
198/// ```
199/// use spn_core::{providers_by_category, ProviderCategory};
200///
201/// let llm_providers: Vec<_> = providers_by_category(ProviderCategory::Llm).collect();
202/// assert!(llm_providers.iter().any(|p| p.id == "anthropic"));
203/// ```
204pub fn providers_by_category(
205    category: ProviderCategory,
206) -> impl Iterator<Item = &'static Provider> {
207    KNOWN_PROVIDERS.iter().filter(move |p| p.category == category)
208}
209
210#[cfg(test)]
211mod tests {
212    use super::*;
213
214    #[test]
215    fn test_find_provider() {
216        assert!(find_provider("anthropic").is_some());
217        assert!(find_provider("ANTHROPIC").is_some());
218        assert!(find_provider("unknown").is_none());
219    }
220
221    #[test]
222    fn test_provider_to_env_var() {
223        assert_eq!(provider_to_env_var("anthropic"), Some("ANTHROPIC_API_KEY"));
224        assert_eq!(provider_to_env_var("github"), Some("GITHUB_TOKEN"));
225        assert_eq!(provider_to_env_var("unknown"), None);
226    }
227
228    #[test]
229    fn test_providers_by_category() {
230        let llm: Vec<_> = providers_by_category(ProviderCategory::Llm).collect();
231        assert!(llm.len() >= 6);
232        assert!(llm.iter().all(|p| p.category == ProviderCategory::Llm));
233
234        let mcp: Vec<_> = providers_by_category(ProviderCategory::Mcp).collect();
235        assert!(mcp.len() >= 5);
236        assert!(mcp.iter().all(|p| p.category == ProviderCategory::Mcp));
237    }
238
239    #[test]
240    fn test_all_providers_have_env_var() {
241        for provider in KNOWN_PROVIDERS {
242            assert!(!provider.env_var.is_empty(), "Provider {} missing env_var", provider.id);
243        }
244    }
245
246    #[test]
247    fn test_provider_count() {
248        // Ensure we have at least 13 providers (7 LLM + 6 MCP)
249        assert!(KNOWN_PROVIDERS.len() >= 13);
250    }
251}