use std::env;
use std::sync::Arc;
use agent_contracts::runtime::RuntimeView;
use crate::error::LlmError;
use crate::provider_registry::{
normalize_api_base, resolve_provider_profile, ProtocolFamily, ProviderProfile,
};
use crate::providers::{
AnthropicProvider, GeminiProvider, OllamaProvider, OpenAiFamilyAuthStyle, OpenAiFamilyProvider,
ZhipuProvider,
};
use crate::resolver::ResolvedConfig;
use super::config::LlmProviderConfig;
use super::wrapper::LlmProviderWrapper;
pub fn create_llm_provider(
config: &LlmProviderConfig,
agent_id: Option<String>,
runtime_view: Option<Arc<dyn RuntimeView>>,
) -> Result<LlmProviderWrapper, LlmError> {
let profile = resolve_provider_profile(&config.provider).ok_or_else(|| {
LlmError::ProviderNotFound(format!(
"Unknown provider: {}. Supported: {}",
config.provider,
crate::provider_registry::supported_providers().join(", ")
))
})?;
let api_base = resolve_api_base(config, &profile)?;
let api_key = resolve_api_key(config, &profile)?;
create_provider_by_protocol(
profile.protocol_family,
api_key,
api_base,
config.model.clone(),
&profile,
agent_id,
runtime_view,
config.api_key_provider.clone(),
)
}
pub fn create_llm_provider_from_resolved(
config: &ResolvedConfig,
model: String,
agent_id: Option<String>,
runtime_view: Option<Arc<dyn RuntimeView>>,
) -> Result<LlmProviderWrapper, LlmError> {
let profile = config
.provider
.as_deref()
.and_then(resolve_provider_profile);
create_provider_by_protocol(
config.protocol,
config.api_key.clone(),
config.base_url.clone(),
model,
&profile.unwrap_or_else(|| default_profile(config.protocol)),
agent_id,
runtime_view,
None,
)
}
fn create_provider_by_protocol(
protocol: ProtocolFamily,
api_key: Option<String>,
api_base: String,
model: String,
profile: &ProviderProfile,
agent_id: Option<String>,
runtime_view: Option<Arc<dyn RuntimeView>>,
api_key_provider: Option<crate::factory::ApiKeyProviderFn>,
) -> Result<LlmProviderWrapper, LlmError> {
match protocol {
ProtocolFamily::OpenAiCompatible => {
if profile.api_key_required && api_key_provider.is_none() && api_key.is_none() {
return Err(LlmError::ConfigError(format!(
"{} API key required",
profile.display_name
)));
}
Ok(LlmProviderWrapper::new(
Arc::new(OpenAiFamilyProvider::new(
api_key,
api_base,
model,
OpenAiFamilyAuthStyle::Bearer,
vec![],
api_key_provider,
)),
agent_id,
runtime_view,
))
}
ProtocolFamily::Anthropic => {
if profile.api_key_required && api_key_provider.is_none() && api_key.is_none() {
return Err(LlmError::ConfigError(format!(
"{} API key required",
profile.display_name
)));
}
Ok(LlmProviderWrapper::new(
Arc::new(AnthropicProvider::new(
api_key,
api_base,
model,
api_key_provider,
)),
agent_id,
runtime_view,
))
}
ProtocolFamily::Gemini => {
if profile.api_key_required && api_key_provider.is_none() && api_key.is_none() {
return Err(LlmError::ConfigError(format!(
"{} API key required",
profile.display_name
)));
}
Ok(LlmProviderWrapper::new(
Arc::new(GeminiProvider::new(
api_key,
api_base,
model,
api_key_provider,
)),
agent_id,
runtime_view,
))
}
ProtocolFamily::Ollama => Ok(LlmProviderWrapper::new(
Arc::new(OllamaProvider::new(api_base, model)),
agent_id,
runtime_view,
)),
ProtocolFamily::Zhipu => {
if profile.api_key_required && api_key_provider.is_none() && api_key.is_none() {
return Err(LlmError::ConfigError(format!(
"{} API key required",
profile.display_name
)));
}
Ok(LlmProviderWrapper::new(
Arc::new(ZhipuProvider::new(
api_key,
api_base,
model,
api_key_provider,
)),
agent_id,
runtime_view,
))
}
}
}
fn resolve_api_base(
config: &LlmProviderConfig,
profile: &ProviderProfile,
) -> Result<String, LlmError> {
let api_base = match &config.api_base {
Some(api_base) => api_base.clone(),
None => profile
.default_base_url
.ok_or_else(|| {
LlmError::ConfigError(format!("{} API base required", profile.display_name))
})?
.to_string(),
};
let api_base = api_base.trim();
if api_base.is_empty() {
return Err(LlmError::ConfigError(format!(
"{} API base required",
profile.display_name
)));
}
Ok(normalize_api_base(api_base))
}
fn resolve_api_key(
config: &LlmProviderConfig,
profile: &ProviderProfile,
) -> Result<Option<String>, LlmError> {
if let Some(ref api_key) = config.api_key {
return Ok(Some(api_key.clone()));
}
if let Some(env_name) = profile.default_api_key_env {
match env::var(env_name) {
Ok(value) if !value.trim().is_empty() => return Ok(Some(value)),
Ok(_) | Err(env::VarError::NotPresent) => {}
Err(env::VarError::NotUnicode(_)) => {
return Err(LlmError::ConfigError(format!(
"environment variable is not valid unicode: {env_name}"
)));
}
}
}
match profile.api_key_required {
true => Err(LlmError::ConfigError(format!(
"{} API key required",
profile.display_name
))),
false => Ok(None),
}
}
fn default_profile(protocol: ProtocolFamily) -> ProviderProfile {
ProviderProfile {
provider_name: "custom",
display_name: "Custom",
protocol_family: protocol,
default_base_url: None,
default_api_key_env: None,
api_key_required: true,
supports_model_catalog: false,
}
}