use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum IntegrationProvider {
OpenAI,
Anthropic,
Google,
DeepSeek,
ElevenLabs,
OpenRouter,
Custom,
}
impl IntegrationProvider {
pub fn api_key_env_var(&self) -> &'static str {
match self {
IntegrationProvider::OpenAI => "OPENAI_API_KEY",
IntegrationProvider::Anthropic => "ANTHROPIC_API_KEY",
IntegrationProvider::Google => "GOOGLE_API_KEY",
IntegrationProvider::DeepSeek => "DEEPSEEK_API_KEY",
IntegrationProvider::ElevenLabs => "ELEVENLABS_API_KEY",
IntegrationProvider::OpenRouter => "OPENROUTER_API_KEY",
IntegrationProvider::Custom => "CUSTOM_API_KEY",
}
}
pub fn default_base_url(&self) -> &'static str {
match self {
IntegrationProvider::OpenAI => "https://api.openai.com/v1",
IntegrationProvider::Anthropic => "https://api.anthropic.com/v1",
IntegrationProvider::Google => "https://generativelanguage.googleapis.com/v1beta",
IntegrationProvider::DeepSeek => "https://api.deepseek.com/v1",
IntegrationProvider::ElevenLabs => "https://api.elevenlabs.io/v1",
IntegrationProvider::OpenRouter => "https://openrouter.ai/api/v1",
IntegrationProvider::Custom => "",
}
}
pub fn as_str(&self) -> &'static str {
match self {
IntegrationProvider::OpenAI => "openai",
IntegrationProvider::Anthropic => "anthropic",
IntegrationProvider::Google => "google",
IntegrationProvider::DeepSeek => "deepseek",
IntegrationProvider::ElevenLabs => "elevenlabs",
IntegrationProvider::OpenRouter => "openrouter",
IntegrationProvider::Custom => "custom",
}
}
}
impl std::fmt::Display for IntegrationProvider {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.as_str())
}
}
impl std::str::FromStr for IntegrationProvider {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"openai" => Ok(IntegrationProvider::OpenAI),
"anthropic" | "claude" => Ok(IntegrationProvider::Anthropic),
"google" | "gemini" => Ok(IntegrationProvider::Google),
"deepseek" | "deep_seek" => Ok(IntegrationProvider::DeepSeek),
"elevenlabs" | "eleven" | "eleven_labs" => Ok(IntegrationProvider::ElevenLabs),
"openrouter" | "open_router" => Ok(IntegrationProvider::OpenRouter),
"custom" => Ok(IntegrationProvider::Custom),
_ => Err(format!(
"Unknown provider: '{}'. Valid values: openai, anthropic, google, deepseek, elevenlabs, openrouter, custom",
s
)),
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ProviderConfig {
pub provider: IntegrationProvider,
#[serde(default)]
pub base_url: Option<String>,
#[serde(default)]
pub api_key: Option<String>,
#[serde(default = "default_timeout_ms")]
pub timeout_ms: u32,
#[serde(default)]
pub options: HashMap<String, serde_json::Value>,
}
fn default_timeout_ms() -> u32 {
30000
}
impl ProviderConfig {
pub fn new(provider: IntegrationProvider) -> Self {
Self {
provider,
base_url: None,
api_key: None,
timeout_ms: default_timeout_ms(),
options: HashMap::new(),
}
}
pub fn effective_base_url(&self) -> &str {
self.base_url
.as_deref()
.unwrap_or_else(|| self.provider.default_base_url())
}
pub fn resolve_api_key(&self) -> Option<String> {
if let Some(key) = &self.api_key {
if let Some(env_var) = key.strip_prefix('$') {
return std::env::var(env_var).ok();
}
if key.starts_with("xybrid://secrets/") {
return None;
}
return Some(key.clone());
}
std::env::var(self.provider.api_key_env_var()).ok()
}
pub fn with_option(mut self, key: &str, value: serde_json::Value) -> Self {
self.options.insert(key.to_string(), value);
self
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct OpenAIOptions {
#[serde(default = "default_temperature")]
pub temperature: f32,
#[serde(default)]
pub max_tokens: Option<u32>,
#[serde(default)]
pub system_prompt: Option<String>,
#[serde(default)]
pub top_p: Option<f32>,
#[serde(default)]
pub frequency_penalty: Option<f32>,
#[serde(default)]
pub presence_penalty: Option<f32>,
}
fn default_temperature() -> f32 {
0.7
}
impl Default for OpenAIOptions {
fn default() -> Self {
Self {
temperature: default_temperature(),
max_tokens: None,
system_prompt: None,
top_p: None,
frequency_penalty: None,
presence_penalty: None,
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct AnthropicOptions {
#[serde(default = "default_anthropic_max_tokens")]
pub max_tokens: u32,
#[serde(default = "default_temperature")]
pub temperature: f32,
#[serde(default)]
pub system: Option<String>,
#[serde(default)]
pub top_p: Option<f32>,
#[serde(default)]
pub top_k: Option<u32>,
}
fn default_anthropic_max_tokens() -> u32 {
4096
}
impl Default for AnthropicOptions {
fn default() -> Self {
Self {
max_tokens: default_anthropic_max_tokens(),
temperature: default_temperature(),
system: None,
top_p: None,
top_k: None,
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
pub struct ElevenLabsOptions {
#[serde(default)]
pub voice_id: Option<String>,
#[serde(default)]
pub model_id: Option<String>,
#[serde(default)]
pub stability: Option<f32>,
#[serde(default)]
pub similarity_boost: Option<f32>,
#[serde(default)]
pub output_format: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct GoogleOptions {
#[serde(default = "default_temperature")]
pub temperature: f32,
#[serde(default)]
pub max_output_tokens: Option<u32>,
#[serde(default)]
pub top_p: Option<f32>,
#[serde(default)]
pub top_k: Option<u32>,
#[serde(default)]
pub safety_level: Option<String>,
}
impl Default for GoogleOptions {
fn default() -> Self {
Self {
temperature: default_temperature(),
max_output_tokens: None,
top_p: None,
top_k: None,
safety_level: None,
}
}
}
#[derive(Debug, Clone)]
pub struct ProviderValidation {
pub valid: bool,
pub errors: Vec<String>,
pub warnings: Vec<String>,
}
impl ProviderConfig {
pub fn validate(&self) -> ProviderValidation {
let mut errors = Vec::new();
let mut warnings = Vec::new();
if self.resolve_api_key().is_none() {
warnings.push(format!(
"No API key found for {}. Set {} environment variable or provide api_key in config.",
self.provider,
self.provider.api_key_env_var()
));
}
if self.provider == IntegrationProvider::Custom && self.base_url.is_none() {
errors.push("Custom provider requires a base_url".to_string());
}
if self.timeout_ms < 1000 {
warnings.push(format!(
"Timeout {}ms is very short, consider increasing to at least 5000ms",
self.timeout_ms
));
}
ProviderValidation {
valid: errors.is_empty(),
errors,
warnings,
}
}
pub fn is_ready(&self) -> bool {
self.resolve_api_key().is_some()
}
pub fn auth_headers(&self) -> Option<Vec<(String, String)>> {
let api_key = self.resolve_api_key()?;
let headers = match self.provider {
IntegrationProvider::OpenAI
| IntegrationProvider::OpenRouter
| IntegrationProvider::DeepSeek => {
vec![("Authorization".to_string(), format!("Bearer {}", api_key))]
}
IntegrationProvider::Anthropic => {
vec![
("x-api-key".to_string(), api_key),
("anthropic-version".to_string(), "2023-06-01".to_string()),
]
}
IntegrationProvider::Google => {
vec![("x-goog-api-key".to_string(), api_key)]
}
IntegrationProvider::ElevenLabs => {
vec![("xi-api-key".to_string(), api_key)]
}
IntegrationProvider::Custom => {
vec![("Authorization".to_string(), format!("Bearer {}", api_key))]
}
};
Some(headers)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_provider_from_str() {
assert_eq!(
"openai".parse::<IntegrationProvider>().unwrap(),
IntegrationProvider::OpenAI
);
assert_eq!(
"anthropic".parse::<IntegrationProvider>().unwrap(),
IntegrationProvider::Anthropic
);
assert_eq!(
"claude".parse::<IntegrationProvider>().unwrap(),
IntegrationProvider::Anthropic
);
assert_eq!(
"elevenlabs".parse::<IntegrationProvider>().unwrap(),
IntegrationProvider::ElevenLabs
);
}
#[test]
fn test_provider_env_var() {
assert_eq!(
IntegrationProvider::OpenAI.api_key_env_var(),
"OPENAI_API_KEY"
);
assert_eq!(
IntegrationProvider::Anthropic.api_key_env_var(),
"ANTHROPIC_API_KEY"
);
}
#[test]
fn test_provider_config_base_url() {
let config = ProviderConfig::new(IntegrationProvider::OpenAI);
assert_eq!(config.effective_base_url(), "https://api.openai.com/v1");
let config = ProviderConfig {
provider: IntegrationProvider::OpenAI,
base_url: Some("https://custom.openai.azure.com".to_string()),
api_key: None,
timeout_ms: 30000,
options: HashMap::new(),
};
assert_eq!(
config.effective_base_url(),
"https://custom.openai.azure.com"
);
}
#[test]
fn test_provider_config_serde() {
let config = ProviderConfig::new(IntegrationProvider::OpenAI)
.with_option("temperature", serde_json::json!(0.7));
let yaml = serde_yaml::to_string(&config).unwrap();
let parsed: ProviderConfig = serde_yaml::from_str(&yaml).unwrap();
assert_eq!(config.provider, parsed.provider);
}
#[test]
fn test_provider_validation() {
let config = ProviderConfig::new(IntegrationProvider::Custom);
let validation = config.validate();
assert!(!validation.valid);
assert!(validation.errors.iter().any(|e| e.contains("base_url")));
let config = ProviderConfig::new(IntegrationProvider::OpenAI);
let validation = config.validate();
assert!(validation.valid); }
#[test]
fn test_provider_auth_headers() {
std::env::set_var("OPENAI_API_KEY", "test-key-123");
let config = ProviderConfig::new(IntegrationProvider::OpenAI);
let headers = config.auth_headers().unwrap();
assert_eq!(headers.len(), 1);
assert_eq!(headers[0].0, "Authorization");
assert!(headers[0].1.contains("Bearer test-key-123"));
std::env::remove_var("OPENAI_API_KEY");
}
#[test]
fn test_google_options_default() {
let options = GoogleOptions::default();
assert!((options.temperature - 0.7).abs() < f32::EPSILON);
assert!(options.max_output_tokens.is_none());
}
#[test]
fn test_provider_is_ready() {
std::env::set_var("TEST_API_KEY", "key123");
let mut config = ProviderConfig::new(IntegrationProvider::Custom);
config.api_key = Some("$TEST_API_KEY".to_string());
assert!(config.is_ready());
config.api_key = Some("$NONEXISTENT_KEY".to_string());
assert!(!config.is_ready());
std::env::remove_var("TEST_API_KEY");
}
}