use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LlmProviderConfig {
pub api_key: String,
pub base_url: Option<String>,
pub default_model: Option<String>,
pub default_temperature: Option<f32>,
pub timeout_seconds: Option<u64>,
pub max_retries: Option<u32>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LlmConfig {
pub default_provider: Option<String>,
pub openai: Option<LlmProviderConfig>,
pub deepseek: Option<LlmProviderConfig>,
pub anthropic: Option<LlmProviderConfig>,
}
impl Default for LlmConfig {
fn default() -> Self {
Self {
default_provider: Some("openai".to_string()),
openai: None,
deepseek: None,
anthropic: None,
}
}
}
impl LlmConfig {
pub fn validate(&self) -> Result<(), String> {
if let Some(default) = &self.default_provider {
match default.as_str() {
"openai" if self.openai.is_none() => {
return Err(
"Default provider is 'openai' but openai config is not present".to_string(),
);
}
"deepseek" if self.deepseek.is_none() => {
return Err(
"Default provider is 'deepseek' but deepseek config is not present"
.to_string(),
);
}
"anthropic" if self.anthropic.is_none() => {
return Err(
"Default provider is 'anthropic' but anthropic config is not present"
.to_string(),
);
}
"openai" | "deepseek" | "anthropic" => {}
_ => {
return Err(format!(
"Invalid default provider: {}. Must be 'openai', 'deepseek', or 'anthropic'",
default
));
}
}
}
if let Some(openai) = &self.openai
&& openai.api_key.is_empty()
{
return Err("OpenAI API key cannot be empty".to_string());
}
if let Some(deepseek) = &self.deepseek
&& deepseek.api_key.is_empty()
{
return Err("DeepSeek API key cannot be empty".to_string());
}
if let Some(anthropic) = &self.anthropic
&& anthropic.api_key.is_empty()
{
return Err("Anthropic API key cannot be empty".to_string());
}
Ok(())
}
pub fn get_provider_config(&self, provider_name: &str) -> Option<&LlmProviderConfig> {
match provider_name.to_lowercase().as_str() {
"openai" => self.openai.as_ref(),
"deepseek" => self.deepseek.as_ref(),
"anthropic" => self.anthropic.as_ref(),
_ => None,
}
}
pub fn get_default_provider_name(&self) -> Option<String> {
self.default_provider.clone()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_llm_config_default() {
let config = LlmConfig::default();
assert_eq!(config.default_provider, Some("openai".to_string()));
assert!(config.openai.is_none());
assert!(config.deepseek.is_none());
assert!(config.anthropic.is_none());
}
#[test]
fn test_llm_config_validate_default_provider_must_be_configured() {
let config = LlmConfig {
default_provider: Some("openai".to_string()),
openai: None,
deepseek: None,
anthropic: None,
};
assert!(config.validate().is_err());
let config = LlmConfig {
default_provider: Some("deepseek".to_string()),
openai: None,
deepseek: None,
anthropic: None,
};
assert!(config.validate().is_err());
let config = LlmConfig {
default_provider: Some("anthropic".to_string()),
openai: None,
deepseek: None,
anthropic: None,
};
assert!(config.validate().is_err());
}
#[test]
fn test_llm_config_validate_invalid_provider_name() {
let config = LlmConfig {
default_provider: Some("invalid_provider".to_string()),
openai: Some(LlmProviderConfig {
api_key: "key".to_string(),
base_url: None,
default_model: None,
default_temperature: None,
timeout_seconds: None,
max_retries: None,
}),
deepseek: None,
anthropic: None,
};
assert!(config.validate().is_err());
}
#[test]
fn test_llm_config_validate_empty_api_key() {
let config = LlmConfig {
default_provider: Some("openai".to_string()),
openai: Some(LlmProviderConfig {
api_key: "".to_string(),
base_url: None,
default_model: None,
default_temperature: None,
timeout_seconds: None,
max_retries: None,
}),
deepseek: None,
anthropic: None,
};
assert!(config.validate().is_err());
}
#[test]
fn test_llm_config_validate_success() {
let config = LlmConfig {
default_provider: Some("deepseek".to_string()),
openai: None,
deepseek: Some(LlmProviderConfig {
api_key: "test-key".to_string(),
base_url: Some("https://api.deepseek.com/v1".to_string()),
default_model: Some("deepseek-chat".to_string()),
default_temperature: Some(0.7),
timeout_seconds: Some(300),
max_retries: Some(3),
}),
anthropic: None,
};
assert!(config.validate().is_ok());
}
#[test]
fn test_llm_config_get_provider_config() {
let openai_config = LlmProviderConfig {
api_key: "openai-key".to_string(),
base_url: None,
default_model: None,
default_temperature: None,
timeout_seconds: None,
max_retries: None,
};
let config = LlmConfig {
default_provider: Some("openai".to_string()),
openai: Some(openai_config),
deepseek: None,
anthropic: None,
};
let retrieved = config.get_provider_config("openai");
assert!(retrieved.is_some());
assert_eq!(retrieved.unwrap().api_key, "openai-key");
let not_found = config.get_provider_config("deepseek");
assert!(not_found.is_none());
}
#[test]
fn test_llm_config_get_provider_config_case_insensitive() {
let deepseek_config = LlmProviderConfig {
api_key: "deepseek-key".to_string(),
base_url: None,
default_model: None,
default_temperature: None,
timeout_seconds: None,
max_retries: None,
};
let config = LlmConfig {
default_provider: Some("deepseek".to_string()),
openai: None,
deepseek: Some(deepseek_config),
anthropic: None,
};
assert!(config.get_provider_config("DeepSeek").is_some());
assert!(config.get_provider_config("DEEPSEEK").is_some());
assert!(config.get_provider_config("deepseek").is_some());
}
#[test]
fn test_llm_config_get_default_provider_name() {
let config = LlmConfig {
default_provider: Some("anthropic".to_string()),
openai: None,
deepseek: None,
anthropic: Some(LlmProviderConfig {
api_key: "key".to_string(),
base_url: None,
default_model: None,
default_temperature: None,
timeout_seconds: None,
max_retries: None,
}),
};
assert_eq!(
config.get_default_provider_name(),
Some("anthropic".to_string())
);
}
}