pub mod providers;
pub mod models;
use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use tokio::sync::mpsc;
use crate::core::error::ProviderError;
use crate::core::TokenUsage;
use crate::storage::Message;
use crate::tools::Tool;
#[async_trait]
pub trait Provider: Send + Sync {
async fn send_messages(
&self,
messages: Vec<Message>,
tools: Vec<Box<dyn Tool>>,
) -> Result<ProviderResponse, ProviderError>;
async fn stream_response(
&self,
messages: Vec<Message>,
tools: Vec<Box<dyn Tool>>,
) -> Result<mpsc::Receiver<Result<ProviderEvent, ProviderError>>, ProviderError>;
fn model(&self) -> &Model;
fn name(&self) -> &str;
async fn is_available(&self) -> bool;
}
#[derive(Debug, Clone)]
pub struct ProviderResponse {
pub content: String,
pub tool_calls: Vec<ProviderToolCall>,
pub token_usage: Option<TokenUsage>,
pub metadata: serde_json::Value,
}
#[derive(Debug, Clone)]
pub struct ProviderToolCall {
pub id: String,
pub name: String,
pub parameters: serde_json::Value,
}
#[derive(Debug, Clone)]
pub enum ProviderEvent {
ContentChunk {
content: String,
},
ToolCall {
name: String,
parameters: serde_json::Value,
},
Complete {
token_usage: Option<TokenUsage>,
},
Error {
error: String,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Model {
pub id: String,
pub name: String,
pub provider: String,
pub context_length: u32,
pub max_output_tokens: u32,
pub supports_tools: bool,
pub supports_streaming: bool,
pub supports_vision: bool,
pub cost_per_input_token: f64,
pub cost_per_output_token: f64,
pub capabilities: ModelCapabilities,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ModelCapabilities {
pub programming_languages: Vec<String>,
pub code_generation: bool,
pub code_explanation: bool,
pub code_debugging: bool,
pub code_refactoring: bool,
pub function_calling: bool,
pub file_operations: bool,
}
impl Default for ModelCapabilities {
fn default() -> Self {
Self {
programming_languages: vec![
"rust".to_string(),
"python".to_string(),
"javascript".to_string(),
"typescript".to_string(),
"go".to_string(),
"java".to_string(),
"cpp".to_string(),
"c".to_string(),
],
code_generation: true,
code_explanation: true,
code_debugging: true,
code_refactoring: true,
function_calling: true,
file_operations: true,
}
}
}
impl Model {
pub fn calculate_cost(&self, token_usage: &TokenUsage) -> f64 {
let input_cost = token_usage.input_tokens as f64 * self.cost_per_input_token;
let output_cost = token_usage.output_tokens as f64 * self.cost_per_output_token;
input_cost + output_cost
}
pub fn supports_capability(&self, capability: &str) -> bool {
match capability {
"tools" => self.supports_tools,
"streaming" => self.supports_streaming,
"vision" => self.supports_vision,
"code_generation" => self.capabilities.code_generation,
"code_explanation" => self.capabilities.code_explanation,
"code_debugging" => self.capabilities.code_debugging,
"code_refactoring" => self.capabilities.code_refactoring,
"function_calling" => self.capabilities.function_calling,
"file_operations" => self.capabilities.file_operations,
_ => false,
}
}
pub fn supports_language(&self, language: &str) -> bool {
self.capabilities.programming_languages
.iter()
.any(|lang| lang.eq_ignore_ascii_case(language))
}
}
pub struct ProviderFactory;
impl ProviderFactory {
pub async fn create_provider(
provider_name: &str,
config: &crate::core::config::ProviderConfig,
) -> Result<Box<dyn Provider>, ProviderError> {
match provider_name {
"mock" => {
Ok(Box::new(providers::MockProvider::new()))
}
"openai" => {
let api_key = config.api_key.clone()
.ok_or_else(|| ProviderError::Configuration("OpenAI API key required".into()))?;
let model = models::find_model_by_id(&config.default_model)
.ok_or_else(|| ProviderError::ModelNotFound(config.default_model.clone()))?;
let base_url = config.settings.get("base_url")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let provider = providers::OpenAIProvider::new(
api_key,
model,
base_url,
config.max_tokens,
Some(0.7), )?;
Ok(Box::new(provider))
}
"anthropic" => {
let api_key = config.api_key.clone()
.ok_or_else(|| ProviderError::Configuration("Anthropic API key required".into()))?;
let model = models::find_model_by_id(&config.default_model)
.ok_or_else(|| ProviderError::ModelNotFound(config.default_model.clone()))?;
let base_url = config.settings.get("base_url")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let provider = providers::AnthropicProvider::new(
api_key,
model,
base_url,
config.max_tokens,
Some(0.7), )?;
Ok(Box::new(provider))
}
"groq" => {
let api_key = config.api_key.clone()
.ok_or_else(|| ProviderError::Configuration("Groq API key required".into()))?;
let model = models::find_model_by_id(&config.default_model)
.ok_or_else(|| ProviderError::ModelNotFound(config.default_model.clone()))?;
let provider = providers::OpenAICompatibleProvider::groq(api_key, model)?;
Ok(Box::new(provider))
}
"cohere" => {
let api_key = config.api_key.clone()
.ok_or_else(|| ProviderError::Configuration("Cohere API key required".into()))?;
let model = models::find_model_by_id(&config.default_model)
.ok_or_else(|| ProviderError::ModelNotFound(config.default_model.clone()))?;
let provider = providers::OpenAICompatibleProvider::cohere(api_key, model)?;
Ok(Box::new(provider))
}
"sambanova" => {
let api_key = config.api_key.clone()
.ok_or_else(|| ProviderError::Configuration("SambaNova API key required".into()))?;
let model = models::find_model_by_id(&config.default_model)
.ok_or_else(|| ProviderError::ModelNotFound(config.default_model.clone()))?;
let provider = providers::OpenAICompatibleProvider::sambanova(api_key, model)?;
Ok(Box::new(provider))
}
"together" => {
let api_key = config.api_key.clone()
.ok_or_else(|| ProviderError::Configuration("Together API key required".into()))?;
let model = models::find_model_by_id(&config.default_model)
.ok_or_else(|| ProviderError::ModelNotFound(config.default_model.clone()))?;
let provider = providers::OpenAICompatibleProvider::together(api_key, model)?;
Ok(Box::new(provider))
}
"perplexity" => {
let api_key = config.api_key.clone()
.ok_or_else(|| ProviderError::Configuration("Perplexity API key required".into()))?;
let model = models::find_model_by_id(&config.default_model)
.ok_or_else(|| ProviderError::ModelNotFound(config.default_model.clone()))?;
let provider = providers::OpenAICompatibleProvider::perplexity(api_key, model)?;
Ok(Box::new(provider))
}
"gemini" => {
let api_key = config.api_key.clone()
.ok_or_else(|| ProviderError::Configuration("Gemini API key required".into()))?;
let model = models::find_model_by_id(&config.default_model)
.ok_or_else(|| ProviderError::ModelNotFound(config.default_model.clone()))?;
let provider = providers::GeminiProvider::new(api_key, model)?;
Ok(Box::new(provider))
}
"azure" => {
let api_key = config.api_key.clone()
.ok_or_else(|| ProviderError::Configuration("Azure API key required".into()))?;
let endpoint = config.base_url.clone()
.ok_or_else(|| ProviderError::Configuration("Azure endpoint required".into()))?;
let deployment_name = config.settings.get("deployment_name")
.and_then(|v| v.as_str())
.ok_or_else(|| ProviderError::Configuration("Azure deployment name required".into()))?;
let model = models::find_model_by_id(&config.default_model)
.ok_or_else(|| ProviderError::ModelNotFound(config.default_model.clone()))?;
let provider = providers::AzureProvider::new(
api_key,
endpoint,
deployment_name.to_string(),
config.settings.get("api_version").and_then(|v| v.as_str()).map(|s| s.to_string()),
model,
config.max_tokens,
config.settings.get("temperature").and_then(|v| v.as_f64()).map(|f| f as f32),
)?;
Ok(Box::new(provider))
}
"vertex" => {
let project_id = config.settings.get("project_id")
.and_then(|v| v.as_str())
.ok_or_else(|| ProviderError::Configuration("Vertex AI project ID required".into()))?;
let location = config.settings.get("location")
.and_then(|v| v.as_str())
.unwrap_or("us-central1");
let access_token = config.api_key.clone()
.ok_or_else(|| ProviderError::Configuration("Vertex AI access token required".into()))?;
let model = models::find_model_by_id(&config.default_model)
.ok_or_else(|| ProviderError::ModelNotFound(config.default_model.clone()))?;
let provider = providers::VertexProvider::new(
project_id.to_string(),
location.to_string(),
access_token,
model,
config.max_tokens,
config.settings.get("temperature").and_then(|v| v.as_f64()).map(|f| f as f32),
)?;
Ok(Box::new(provider))
}
"openrouter" => {
let api_key = config.api_key.clone()
.ok_or_else(|| ProviderError::Configuration("OpenRouter API key required".into()))?;
let model = models::find_model_by_id(&config.default_model)
.ok_or_else(|| ProviderError::ModelNotFound(config.default_model.clone()))?;
let provider = providers::OpenRouterProvider::new(
api_key,
model,
config.max_tokens,
config.settings.get("temperature").and_then(|v| v.as_f64()).map(|f| f as f32),
config.settings.get("app_name").and_then(|v| v.as_str()).map(|s| s.to_string()),
config.settings.get("site_url").and_then(|v| v.as_str()).map(|s| s.to_string()),
)?;
Ok(Box::new(provider))
}
"xai" => {
let api_key = config.api_key.clone()
.ok_or_else(|| ProviderError::Configuration("xAI API key required".into()))?;
let model = models::find_model_by_id(&config.default_model)
.ok_or_else(|| ProviderError::ModelNotFound(config.default_model.clone()))?;
let provider = providers::XaiProvider::new(
api_key,
model,
config.max_tokens,
config.settings.get("temperature").and_then(|v| v.as_f64()).map(|f| f as f32),
)?;
Ok(Box::new(provider))
}
"local" => {
let local_config = providers::local::LocalProviderConfig {
endpoint: config.base_url.clone()
.unwrap_or_else(|| std::env::var("LOCAL_ENDPOINT")
.unwrap_or_else(|_| "http://localhost:1234".to_string())),
api_key: config.api_key.clone(),
timeout_seconds: 60,
discovery_paths: vec![
"v1/models".to_string(),
"api/v0/models".to_string(),
"api/v1/models".to_string(),
"v1/internal/model/list".to_string(),
],
};
let provider = if let Some(model_id) = config.settings.get("model_id").and_then(|v| v.as_str()) {
providers::LocalProvider::with_model(local_config, model_id).await?
} else {
providers::LocalProvider::new(local_config).await?
};
Ok(Box::new(provider))
}
_ => Err(ProviderError::ModelNotFound(format!("Unknown provider: {}", provider_name))),
}
}
pub fn available_providers() -> Vec<&'static str> {
vec![
"mock",
"openai",
"anthropic",
"groq",
"cohere",
"sambanova",
"together",
"perplexity",
"gemini",
"azure",
"vertex",
"openrouter",
"xai",
"local",
]
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_model_cost_calculation() {
let model = Model {
id: "test-model".to_string(),
name: "Test Model".to_string(),
provider: "test".to_string(),
context_length: 4000,
max_output_tokens: 1000,
supports_tools: true,
supports_streaming: true,
supports_vision: false,
cost_per_input_token: 0.00001,
cost_per_output_token: 0.00003,
capabilities: ModelCapabilities::default(),
};
let token_usage = TokenUsage {
input_tokens: 1000,
output_tokens: 500,
total_tokens: 1500,
cache_creation_tokens: 0,
cache_read_tokens: 0,
};
let cost = model.calculate_cost(&token_usage);
assert_eq!(cost, 0.025); }
#[test]
fn test_model_capabilities() {
let model = Model {
id: "test-model".to_string(),
name: "Test Model".to_string(),
provider: "test".to_string(),
context_length: 4000,
max_output_tokens: 1000,
supports_tools: true,
supports_streaming: true,
supports_vision: false,
cost_per_input_token: 0.00001,
cost_per_output_token: 0.00003,
capabilities: ModelCapabilities::default(),
};
assert!(model.supports_capability("tools"));
assert!(model.supports_capability("streaming"));
assert!(!model.supports_capability("vision"));
assert!(model.supports_capability("code_generation"));
assert!(model.supports_language("rust"));
assert!(model.supports_language("Python")); assert!(!model.supports_language("cobol"));
}
#[test]
fn test_provider_factory_available_providers() {
let providers = ProviderFactory::available_providers();
assert!(!providers.is_empty());
}
}