use std::sync::Arc;
use llm_trait::{LlmConfig, LlmError, LlmProvider, Protocol};
use crate::generic::{GenericProvider, ProfiledProvider};
use crate::model_registry::{ModelProfile, ModelRegistry};
use crate::protocol::anthropic::AnthropicProtocol;
use crate::protocol::openai::OpenAiProtocol;
static MODEL_REGISTRY: std::sync::LazyLock<ModelRegistry> =
std::sync::LazyLock::new(ModelRegistry::builtin);
pub fn create_provider(config: &LlmConfig) -> Result<Arc<dyn LlmProvider>, LlmError> {
if config.api_key.is_empty() {
return Err(LlmError::config("LLM_API_KEY is required"));
}
if config.model.is_empty() {
return Err(LlmError::config("LLM_MODEL is required"));
}
if config.base_url.is_empty() {
return Err(LlmError::config("LLM_BASE_URL is required"));
}
if let Some(protocol) = config.protocol
&& !matches!(protocol, Protocol::OpenAi | Protocol::Anthropic)
{
return Err(LlmError::config(format!(
"Unsupported protocol '{}'. Use 'openai' or 'anthropic'.",
protocol.as_str()
)));
}
let profile = MODEL_REGISTRY.lookup(&config.model, Some(&config.base_url), config.protocol);
tracing::info!(
model = %config.model,
base_url = %config.base_url,
protocol = ?profile.protocol,
provider = %profile.provider_name,
reasoning_mode = ?profile.reasoning_mode,
max_output_tokens = ?profile.capabilities.max_output_tokens,
"provider created from registry profile"
);
let protocol_impl = build_protocol(&profile, config)?;
let provider = ProfiledProvider::new(GenericProvider::new(protocol_impl), profile);
Ok(Arc::new(provider))
}
fn build_protocol(
profile: &ModelProfile,
config: &LlmConfig,
) -> Result<Box<dyn llm_trait::RawAdapter>, LlmError> {
match profile.protocol {
Protocol::OpenAi => Ok(Box::new(
OpenAiProtocol::from_config(config).with_model_profile(profile.clone()),
)),
Protocol::Anthropic => Ok(Box::new(
AnthropicProtocol::from_config(config).with_model_profile(profile.clone()),
)),
Protocol::OpenAiResponses => Err(LlmError::config(
"OpenAI Responses API is not yet supported. Use protocol 'openai' or 'anthropic'.",
)),
}
}
pub fn create(
api_key: &str,
model: &str,
base_url: &str,
) -> Result<Arc<dyn LlmProvider>, LlmError> {
let config = LlmConfig {
protocol: None,
api_key: api_key.to_string(),
model: model.to_string(),
base_url: base_url.to_string(),
options: std::collections::HashMap::new(),
};
create_provider(&config)
}
pub fn from_env() -> Result<Arc<dyn LlmProvider>, LlmError> {
let config = LlmConfig::from_env()?;
create_provider(&config)
}
#[cfg(test)]
mod tests {
use super::*;
use llm_trait::ReasoningMode;
#[test]
fn create_provider_openai() {
let config = LlmConfig {
protocol: None,
api_key: "sk-test".to_string(),
model: "gpt-4o".to_string(),
base_url: "https://api.openai.com/v1".to_string(),
options: Default::default(),
};
let provider = create_provider(&config).unwrap();
let info = provider.info();
assert_eq!(info.name, "openai");
assert_eq!(info.model, "gpt-4o");
}
#[test]
fn create_provider_anthropic() {
let config = LlmConfig {
protocol: None,
api_key: "sk-test".to_string(),
model: "claude-sonnet".to_string(),
base_url: "https://api.anthropic.com".to_string(),
options: Default::default(),
};
let provider = create_provider(&config).unwrap();
let info = provider.info();
assert_eq!(info.name, "anthropic");
assert_eq!(info.model, "claude-sonnet");
}
#[test]
fn create_provider_explicit_protocol() {
let config = LlmConfig {
protocol: Some(Protocol::Anthropic),
api_key: "sk-test".to_string(),
model: "test-model".to_string(),
base_url: "https://custom.api.com".to_string(),
options: Default::default(),
};
let provider = create_provider(&config).unwrap();
let info = provider.info();
assert_eq!(info.name, "anthropic");
}
#[test]
fn create_provider_deepseek() {
let config = LlmConfig {
protocol: None,
api_key: "sk-test".to_string(),
model: "deepseek-chat".to_string(),
base_url: "https://api.deepseek.com/v1".to_string(),
options: Default::default(),
};
let provider = create_provider(&config).unwrap();
let info = provider.info();
assert_eq!(info.name, "deepseek");
}
#[test]
fn create_provider_mimo_no_reasoning() {
let config = LlmConfig {
protocol: None,
api_key: "tp-test".to_string(),
model: "mimo-v2.5-pro".to_string(),
base_url: "https://api.example-mimo.com/v1".to_string(),
options: Default::default(),
};
let provider = create_provider(&config).unwrap();
let info = provider.info();
assert_eq!(info.name, "mimo");
let profile = MODEL_REGISTRY.lookup(
"mimo-v2.5-pro",
Some("https://api.example-mimo.com/v1"),
None,
);
assert_eq!(profile.reasoning_mode, ReasoningMode::None);
}
#[test]
fn create_provider_qwen() {
let config = LlmConfig {
protocol: None,
api_key: "sk-test".to_string(),
model: "qwen-plus".to_string(),
base_url: "https://dashscope.aliyuncs.com/compatible-mode/v1".to_string(),
options: Default::default(),
};
let provider = create_provider(&config).unwrap();
let info = provider.info();
assert_eq!(info.name, "qwen");
}
#[test]
fn create_provider_openai_responses_returns_error() {
let config = LlmConfig {
protocol: Some(Protocol::OpenAiResponses),
api_key: "sk-test".to_string(),
model: "gpt-4o".to_string(),
base_url: "https://api.openai.com/v1".to_string(),
options: Default::default(),
};
match create_provider(&config) {
Ok(_) => panic!("Expected error for OpenAiResponses protocol"),
Err(e) => assert!(
e.to_string().contains("Unsupported protocol"),
"Expected clear error about unsupported protocol, got: {}",
e
),
}
}
#[test]
fn create_convenience() {
let provider = create("sk-test", "gpt-4o", "https://api.openai.com/v1").unwrap();
let info = provider.info();
assert_eq!(info.name, "openai");
}
#[test]
fn create_provider_options_max_tokens_ignored() {
use std::collections::HashMap;
let mut options = HashMap::new();
options.insert("max_tokens".to_string(), serde_json::json!(42));
let config = LlmConfig {
protocol: None,
api_key: "sk-test".to_string(),
model: "gpt-4o".to_string(),
base_url: "https://api.openai.com/v1".to_string(),
options,
};
let provider = create_provider(&config).unwrap();
assert_eq!(provider.info().name, "openai");
}
}