use serde::{Deserialize, Serialize};
use std::env;
use std::time::Duration;
pub mod anthropic;
pub mod anthropic_sdk;
pub mod local;
pub mod openai;
pub mod openai_sdk;
use crate::api::{ContentBlock, Message, ToolDefinition};
use crate::errors::ProviderError;
use crate::types::ModelId;
use reqwest::Client;
pub(crate) struct ProviderHttpClient {
client: Client,
}
impl ProviderHttpClient {
pub fn new(timeout_secs: u64) -> Result<Self, ProviderError> {
let client = Client::builder()
.timeout(Duration::from_secs(timeout_secs))
.build()?;
Ok(Self { client })
}
pub fn default() -> Result<Self, ProviderError> {
Self::new(120)
}
pub fn client(&self) -> &Client {
&self.client
}
}
#[derive(Debug, Clone)]
pub struct InferenceRequest {
pub model: ModelId,
pub messages: Vec<Message>,
pub tools: Vec<ToolDefinition>,
pub max_tokens: u32,
pub temperature: Option<f32>,
pub system: String,
}
#[derive(Debug, Clone)]
pub struct InferenceResponse {
pub content: Vec<ContentBlock>,
pub stop_reason: String,
pub usage: Usage,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Usage {
pub input_tokens: u32,
pub output_tokens: u32,
}
#[async_trait::async_trait]
pub trait LLMProvider: Send + Sync {
async fn infer(&self, req: &InferenceRequest) -> Result<InferenceResponse, ProviderError>;
fn name(&self) -> &str;
fn model(&self) -> &ModelId;
fn validate_config(&self) -> Result<(), ProviderError>;
fn supports_tool_use(&self) -> bool {
true
}
}
#[derive(Debug, Clone, Default)]
pub struct ProviderOverrides {
pub model: Option<ModelId>,
}
pub async fn create_provider() -> Result<Box<dyn LLMProvider>, ProviderError> {
create_provider_with_overrides(ProviderOverrides::default()).await
}
pub async fn create_provider_with_overrides(
overrides: ProviderOverrides,
) -> Result<Box<dyn LLMProvider>, ProviderError> {
let _ = dotenvy::dotenv();
let config_file = crate::config_file::ProviderConfig::load().ok();
if let Ok(provider_name) = env::var("PROVIDER") {
return create_provider_by_name(&provider_name, &config_file, overrides).await;
}
if let Some(config) = config_file.as_ref()
&& let Some(provider_name) = &config.provider
{
return create_provider_by_name(provider_name, &config_file, overrides).await;
}
if env::var("ANTHROPIC_API_KEY").is_ok() {
let key = env::var("ANTHROPIC_API_KEY")
.map_err(|_| ProviderError::MissingApiKey("anthropic".to_string()))?;
let cfg_model = config_file
.as_ref()
.and_then(|c| c.merged_settings("anthropic").model)
.map(ModelId::new);
let model = overrides
.model
.or(env::var("MODEL").ok().map(ModelId::new))
.or(cfg_model);
return Ok(Box::new(anthropic::AnthropicProvider::new_with_model(
key, model,
)?));
}
if env::var("OPENAI_API_KEY").is_ok() {
let key = env::var("OPENAI_API_KEY")
.map_err(|_| ProviderError::MissingApiKey("openai".to_string()))?;
let cfg_model = config_file
.as_ref()
.and_then(|c| c.merged_settings("openai").model)
.map(ModelId::new);
let model = overrides
.model
.or(env::var("MODEL").ok().map(ModelId::new))
.or(cfg_model);
return Ok(Box::new(openai::OpenAIProvider::new_with_model(
key, model,
)?));
}
if local::LocalProvider::is_available().await {
let cfg_model = config_file
.as_ref()
.and_then(|c| c.merged_settings("local").model)
.map(ModelId::new);
let model = overrides
.model
.or(env::var("MODEL").ok().map(ModelId::new))
.or(cfg_model);
return Ok(Box::new(local::LocalProvider::new_with_model(model)?));
}
Err(ProviderError::NoProviderConfigured)
}
async fn create_provider_by_name(
name: &str,
config_file: &Option<crate::config_file::ProviderConfig>,
overrides: ProviderOverrides,
) -> Result<Box<dyn LLMProvider>, ProviderError> {
match name.to_lowercase().as_str() {
"anthropic" => {
let key = env::var("ANTHROPIC_API_KEY")
.map_err(|_| ProviderError::MissingApiKey("anthropic".to_string()))?;
let cfg_model = config_file
.as_ref()
.and_then(|c| c.merged_settings("anthropic").model)
.map(ModelId::new);
let model = overrides
.model
.or(env::var("MODEL").ok().map(ModelId::new))
.or(cfg_model);
Ok(Box::new(anthropic::AnthropicProvider::new_with_model(
key, model,
)?))
}
"anthropic-sdk" | "claude-sdk" => {
let key = env::var("ANTHROPIC_API_KEY")
.map_err(|_| ProviderError::MissingApiKey("anthropic".to_string()))?;
let cfg_model = config_file
.as_ref()
.and_then(|c| c.merged_settings("anthropic").model)
.map(ModelId::new);
let model = overrides
.model
.or(env::var("MODEL").ok().map(ModelId::new))
.or(cfg_model);
Ok(Box::new(
anthropic_sdk::AnthropicSdkProvider::new_with_model(key, model)?,
))
}
"openai" => {
let key = env::var("OPENAI_API_KEY")
.map_err(|_| ProviderError::MissingApiKey("openai".to_string()))?;
let cfg_model = config_file
.as_ref()
.and_then(|c| c.merged_settings("openai").model)
.map(ModelId::new);
let model = overrides
.model
.or(env::var("MODEL").ok().map(ModelId::new))
.or(cfg_model);
Ok(Box::new(openai::OpenAIProvider::new_with_model(
key, model,
)?))
}
"openai-sdk" => {
let key = env::var("OPENAI_API_KEY")
.map_err(|_| ProviderError::MissingApiKey("openai".to_string()))?;
let cfg_model = config_file
.as_ref()
.and_then(|c| c.merged_settings("openai").model)
.map(ModelId::new);
let model = overrides
.model
.or(env::var("MODEL").ok().map(ModelId::new))
.or(cfg_model);
Ok(Box::new(openai_sdk::OpenAISdkProvider::new_with_model(
key, model,
)?))
}
"local" | "ollama" => {
let cfg_model = config_file
.as_ref()
.and_then(|c| c.merged_settings("local").model)
.map(ModelId::new);
let model = overrides
.model
.or(env::var("MODEL").ok().map(ModelId::new))
.or(cfg_model);
Ok(Box::new(local::LocalProvider::new_with_model(model)?))
}
"openrouter" => Err(ProviderError::Config(
"OpenRouter provider not yet implemented".to_string(),
)),
other => Err(ProviderError::Config(format!("Unknown provider: {other}"))),
}
}