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