use crate::PreviewError;
pub struct ApiKeyValidator;
impl ApiKeyValidator {
pub fn validate_openai_key(api_key: &str) -> Result<(), PreviewError> {
if api_key.is_empty() {
return Err(PreviewError::InvalidConfiguration("OpenAI API key cannot be empty".to_string()));
}
if !api_key.starts_with("sk-") {
return Err(PreviewError::InvalidConfiguration(
"OpenAI API key must start with 'sk-'".to_string()
));
}
if api_key.len() < 20 {
return Err(PreviewError::InvalidConfiguration(
"OpenAI API key appears to be too short".to_string()
));
}
Ok(())
}
pub fn validate_anthropic_key(api_key: &str) -> Result<(), PreviewError> {
if api_key.is_empty() {
return Err(PreviewError::InvalidConfiguration("Anthropic API key cannot be empty".to_string()));
}
if !api_key.starts_with("sk-ant-") {
return Err(PreviewError::InvalidConfiguration(
"Anthropic API key must start with 'sk-ant-'".to_string()
));
}
if api_key.len() < 20 {
return Err(PreviewError::InvalidConfiguration(
"Anthropic API key appears to be too short".to_string()
));
}
Ok(())
}
pub fn validate_model_name(provider: &str, model: &str) -> Result<(), PreviewError> {
match provider.to_lowercase().as_str() {
"openai" => {
let valid_models = [
"gpt-4", "gpt-4-turbo", "gpt-4o", "gpt-4o-mini",
"gpt-3.5-turbo", "gpt-3.5-turbo-16k"
];
if !valid_models.iter().any(|&m| model.starts_with(m)) {
return Err(PreviewError::InvalidConfiguration(
format!("Unknown OpenAI model: {}. Valid models: {}",
model, valid_models.join(", "))
));
}
}
"anthropic" => {
let valid_models = [
"claude-3-opus", "claude-3-sonnet", "claude-3-haiku",
"claude-3-5-sonnet", "claude-3-5-haiku"
];
if !valid_models.iter().any(|&m| model.starts_with(m)) {
return Err(PreviewError::InvalidConfiguration(
format!("Unknown Anthropic model: {}. Valid models: {}",
model, valid_models.join(", "))
));
}
}
_ => {
}
}
Ok(())
}
}
pub struct LLMConfig;
impl LLMConfig {
pub fn openai_from_env() -> Result<crate::OpenAIProvider, PreviewError> {
let api_key = std::env::var("OPENAI_API_KEY")
.map_err(|_| PreviewError::InvalidConfiguration(
"OPENAI_API_KEY environment variable not set".to_string()
))?;
ApiKeyValidator::validate_openai_key(&api_key)?;
let model = std::env::var("OPENAI_MODEL")
.unwrap_or_else(|_| "gpt-4o-mini".to_string());
ApiKeyValidator::validate_model_name("openai", &model)?;
Ok(crate::OpenAIProvider::new(api_key).with_model(model))
}
pub fn anthropic_from_env() -> Result<crate::AnthropicProvider, PreviewError> {
let api_key = std::env::var("ANTHROPIC_API_KEY")
.map_err(|_| PreviewError::InvalidConfiguration(
"ANTHROPIC_API_KEY environment variable not set".to_string()
))?;
ApiKeyValidator::validate_anthropic_key(&api_key)?;
let model = std::env::var("ANTHROPIC_MODEL")
.unwrap_or_else(|_| "claude-3-5-sonnet-20241022".to_string());
ApiKeyValidator::validate_model_name("anthropic", &model)?;
Ok(crate::AnthropicProvider::new(api_key).with_model(model))
}
pub fn claude_code_from_env() -> Result<crate::OpenAIProvider, PreviewError> {
let base_url = std::env::var("CLAUDE_CODE_API_URL")
.unwrap_or_else(|_| "http://localhost:8080/v1".to_string());
let model = std::env::var("CLAUDE_CODE_MODEL")
.unwrap_or_else(|_| "claude-3-5-haiku-20241022".to_string());
let config = async_openai::config::OpenAIConfig::new()
.with_api_base(base_url)
.with_api_key("not-needed");
Ok(crate::OpenAIProvider::from_config(config, model))
}
pub fn local_from_env() -> Result<crate::LocalProvider, PreviewError> {
let endpoint = std::env::var("LOCAL_LLM_ENDPOINT")
.unwrap_or_else(|_| "http://localhost:11434".to_string());
let model = std::env::var("LOCAL_LLM_MODEL")
.unwrap_or_else(|_| "llama2".to_string());
Ok(crate::LocalProvider::new(endpoint, model))
}
pub async fn auto_detect_provider() -> Result<std::sync::Arc<dyn crate::LLMProvider>, PreviewError> {
if let Ok(provider) = Self::openai_from_env() {
return Ok(std::sync::Arc::new(provider));
}
if let Ok(provider) = Self::anthropic_from_env() {
return Ok(std::sync::Arc::new(provider));
}
if let Ok(provider) = Self::claude_code_from_env() {
if Self::test_claude_code_api().await {
return Ok(std::sync::Arc::new(provider));
}
}
if let Ok(provider) = Self::local_from_env() {
if Self::test_local_api(&provider).await {
return Ok(std::sync::Arc::new(provider));
}
}
Ok(std::sync::Arc::new(crate::MockProvider::new()))
}
async fn test_claude_code_api() -> bool {
let base_url = std::env::var("CLAUDE_CODE_API_URL")
.unwrap_or_else(|_| "http://localhost:8080".to_string());
if let Ok(response) = reqwest::get(&format!("{}/health", base_url)).await {
response.status().is_success()
} else {
false
}
}
async fn test_local_api(_provider: &crate::LocalProvider) -> bool {
false
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_openai_key_validation() {
assert!(ApiKeyValidator::validate_openai_key("sk-1234567890abcdefghij").is_ok());
assert!(ApiKeyValidator::validate_openai_key("").is_err());
assert!(ApiKeyValidator::validate_openai_key("invalid").is_err());
assert!(ApiKeyValidator::validate_openai_key("sk-short").is_err());
}
#[test]
fn test_anthropic_key_validation() {
assert!(ApiKeyValidator::validate_anthropic_key("sk-ant-1234567890abcdefghij").is_ok());
assert!(ApiKeyValidator::validate_anthropic_key("").is_err());
assert!(ApiKeyValidator::validate_anthropic_key("sk-1234567890").is_err());
assert!(ApiKeyValidator::validate_anthropic_key("sk-ant-short").is_err());
}
#[test]
fn test_model_validation() {
assert!(ApiKeyValidator::validate_model_name("openai", "gpt-4").is_ok());
assert!(ApiKeyValidator::validate_model_name("openai", "gpt-4o-mini").is_ok());
assert!(ApiKeyValidator::validate_model_name("openai", "invalid-model").is_err());
assert!(ApiKeyValidator::validate_model_name("anthropic", "claude-3-opus-20240229").is_ok());
assert!(ApiKeyValidator::validate_model_name("anthropic", "claude-3-5-sonnet-20241022").is_ok());
assert!(ApiKeyValidator::validate_model_name("anthropic", "invalid-model").is_err());
assert!(ApiKeyValidator::validate_model_name("local", "any-model").is_ok());
}
}