use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ProviderCategory {
Llm,
Mcp,
Local,
}
impl fmt::Display for ProviderCategory {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Llm => write!(f, "LLM"),
Self::Mcp => write!(f, "MCP"),
Self::Local => write!(f, "Local"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Provider {
pub id: &'static str,
pub name: &'static str,
pub aliases: &'static [&'static str],
pub env_var: &'static str,
pub key_prefix: Option<&'static str>,
pub category: ProviderCategory,
pub requires_key: bool,
pub description: &'static str,
}
impl Provider {
pub fn has_env_key(&self) -> bool {
std::env::var(self.env_var).is_ok_and(|v| !v.trim().is_empty())
}
}
pub static KNOWN_PROVIDERS: &[Provider] = &[
Provider {
id: "anthropic",
name: "Anthropic Claude",
aliases: &["claude"],
env_var: "ANTHROPIC_API_KEY",
key_prefix: Some("sk-ant-"),
category: ProviderCategory::Llm,
requires_key: true,
description: "Claude models (Opus, Sonnet, Haiku)",
},
Provider {
id: "openai",
name: "OpenAI",
aliases: &["gpt"],
env_var: "OPENAI_API_KEY",
key_prefix: Some("sk-"),
category: ProviderCategory::Llm,
requires_key: true,
description: "GPT-4, GPT-4o, and other OpenAI models",
},
Provider {
id: "mistral",
name: "Mistral AI",
aliases: &[],
env_var: "MISTRAL_API_KEY",
key_prefix: None,
category: ProviderCategory::Llm,
requires_key: true,
description: "Mistral Large, Medium, Small models",
},
Provider {
id: "groq",
name: "Groq",
aliases: &[],
env_var: "GROQ_API_KEY",
key_prefix: Some("gsk_"),
category: ProviderCategory::Llm,
requires_key: true,
description: "Fast inference with Llama, Mixtral models",
},
Provider {
id: "deepseek",
name: "DeepSeek",
aliases: &["deep-seek"],
env_var: "DEEPSEEK_API_KEY",
key_prefix: Some("sk-"),
category: ProviderCategory::Llm,
requires_key: true,
description: "DeepSeek Chat and Coder models",
},
Provider {
id: "gemini",
name: "Google Gemini",
aliases: &["google"],
env_var: "GEMINI_API_KEY",
key_prefix: None,
category: ProviderCategory::Llm,
requires_key: true,
description: "Gemini Pro, Flash, and Ultra models",
},
Provider {
id: "xai",
name: "xAI Grok",
aliases: &["grok"],
env_var: "XAI_API_KEY",
key_prefix: None,
category: ProviderCategory::Llm,
requires_key: true,
description: "Grok models (Grok-3, Grok-4)",
},
Provider {
id: "neo4j",
name: "Neo4j",
aliases: &[],
env_var: "NEO4J_PASSWORD",
key_prefix: None,
category: ProviderCategory::Mcp,
requires_key: true,
description: "Neo4j graph database MCP server",
},
Provider {
id: "github",
name: "GitHub",
aliases: &[],
env_var: "GITHUB_TOKEN",
key_prefix: Some("ghp_"),
category: ProviderCategory::Mcp,
requires_key: true,
description: "GitHub API for repos, issues, PRs",
},
Provider {
id: "slack",
name: "Slack",
aliases: &[],
env_var: "SLACK_BOT_TOKEN",
key_prefix: Some("xoxb-"),
category: ProviderCategory::Mcp,
requires_key: true,
description: "Slack workspace integration",
},
Provider {
id: "perplexity",
name: "Perplexity",
aliases: &[],
env_var: "PERPLEXITY_API_KEY",
key_prefix: Some("pplx-"),
category: ProviderCategory::Mcp,
requires_key: true,
description: "Web search and research MCP server",
},
Provider {
id: "firecrawl",
name: "Firecrawl",
aliases: &[],
env_var: "FIRECRAWL_API_KEY",
key_prefix: Some("fc-"),
category: ProviderCategory::Mcp,
requires_key: true,
description: "Web scraping and crawling MCP server",
},
Provider {
id: "supadata",
name: "Supadata",
aliases: &[],
env_var: "SUPADATA_API_KEY",
key_prefix: None,
category: ProviderCategory::Mcp,
requires_key: true,
description: "Video transcription MCP server",
},
Provider {
id: "dataforseo",
name: "DataForSEO",
aliases: &[],
env_var: "DATAFORSEO_API_KEY",
key_prefix: None,
category: ProviderCategory::Mcp,
requires_key: true,
description: "SEO data and keyword research",
},
Provider {
id: "ahrefs",
name: "Ahrefs",
aliases: &[],
env_var: "AHREFS_API_KEY",
key_prefix: None,
category: ProviderCategory::Mcp,
requires_key: true,
description: "Backlink and SEO analysis",
},
Provider {
id: "postgres",
name: "PostgreSQL",
aliases: &[],
env_var: "POSTGRES_URL",
key_prefix: None,
category: ProviderCategory::Mcp,
requires_key: true,
description: "PostgreSQL database MCP server",
},
Provider {
id: "filesystem",
name: "Filesystem",
aliases: &[],
env_var: "FILESYSTEM_ALLOWED_PATHS",
key_prefix: None,
category: ProviderCategory::Mcp,
requires_key: false,
description: "Local filesystem access MCP server",
},
Provider {
id: "memory",
name: "Memory",
aliases: &[],
env_var: "MEMORY_STORAGE_PATH",
key_prefix: None,
category: ProviderCategory::Mcp,
requires_key: false,
description: "Persistent memory MCP server",
},
Provider {
id: "native",
name: "Native Inference",
aliases: &["local"],
env_var: "NIKA_NATIVE_MODEL_PATH",
key_prefix: None,
category: ProviderCategory::Local,
requires_key: false,
description: "Local GGUF models via mistral.rs",
},
];
pub fn find_provider(name: &str) -> Option<&'static Provider> {
let lower = name.to_lowercase();
KNOWN_PROVIDERS
.iter()
.find(|p| p.id == lower || p.aliases.iter().any(|a| *a == lower))
}
pub fn provider_to_env_var(id: &str) -> Option<&'static str> {
find_provider(id).map(|p| p.env_var)
}
pub fn providers_by_category(category: ProviderCategory) -> Vec<&'static Provider> {
KNOWN_PROVIDERS
.iter()
.filter(|p| p.category == category)
.collect()
}
pub fn validate_key_format(provider: &Provider, key: &str) -> bool {
match provider.key_prefix {
Some(prefix) => key.starts_with(prefix),
None => true,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_known_providers_count() {
assert_eq!(KNOWN_PROVIDERS.len(), 19);
}
#[test]
fn test_provider_categories() {
let llm = providers_by_category(ProviderCategory::Llm);
let mcp = providers_by_category(ProviderCategory::Mcp);
let local = providers_by_category(ProviderCategory::Local);
assert_eq!(llm.len(), 7);
assert_eq!(mcp.len(), 11);
assert_eq!(local.len(), 1);
}
#[test]
fn test_find_provider() {
let anthropic = find_provider("anthropic").unwrap();
assert_eq!(anthropic.id, "anthropic");
assert_eq!(anthropic.env_var, "ANTHROPIC_API_KEY");
assert_eq!(anthropic.key_prefix, Some("sk-ant-"));
assert!(anthropic.requires_key);
let native = find_provider("native").unwrap();
assert!(!native.requires_key);
assert!(find_provider("ollama").is_none());
assert!(find_provider("nonexistent").is_none());
}
#[test]
fn test_find_provider_by_alias() {
let p = find_provider("claude").unwrap();
assert_eq!(p.id, "anthropic");
let p = find_provider("gpt").unwrap();
assert_eq!(p.id, "openai");
let p = find_provider("deep-seek").unwrap();
assert_eq!(p.id, "deepseek");
let p = find_provider("google").unwrap();
assert_eq!(p.id, "gemini");
let p = find_provider("local").unwrap();
assert_eq!(p.id, "native");
let p = find_provider("Claude").unwrap();
assert_eq!(p.id, "anthropic");
}
#[test]
fn test_provider_to_env_var() {
assert_eq!(provider_to_env_var("anthropic"), Some("ANTHROPIC_API_KEY"));
assert_eq!(provider_to_env_var("openai"), Some("OPENAI_API_KEY"));
assert_eq!(provider_to_env_var("neo4j"), Some("NEO4J_PASSWORD"));
assert_eq!(provider_to_env_var("unknown"), None);
}
#[test]
fn test_validate_key_format() {
let anthropic = find_provider("anthropic").unwrap();
assert!(validate_key_format(anthropic, "sk-ant-abc123"));
assert!(!validate_key_format(anthropic, "sk-proj-abc123"));
assert!(!validate_key_format(anthropic, "abc123"));
let groq = find_provider("groq").unwrap();
assert!(validate_key_format(groq, "gsk_abc123"));
assert!(!validate_key_format(groq, "sk-abc123"));
let mistral = find_provider("mistral").unwrap();
assert!(validate_key_format(mistral, "any-key-format"));
}
#[test]
fn test_provider_category_display() {
assert_eq!(ProviderCategory::Llm.to_string(), "LLM");
assert_eq!(ProviderCategory::Mcp.to_string(), "MCP");
assert_eq!(ProviderCategory::Local.to_string(), "Local");
}
#[test]
fn test_all_llm_providers_have_expected_fields() {
for provider in providers_by_category(ProviderCategory::Llm) {
assert!(!provider.id.is_empty());
assert!(!provider.name.is_empty());
assert!(!provider.env_var.is_empty());
assert!(!provider.description.is_empty());
}
}
#[test]
fn test_all_mcp_providers_have_expected_fields() {
for provider in providers_by_category(ProviderCategory::Mcp) {
assert!(!provider.id.is_empty());
assert!(!provider.name.is_empty());
assert!(!provider.env_var.is_empty());
assert!(!provider.description.is_empty());
}
}
}