llm_connector/
config.rs

1//! Configuration management for LLM providers
2//!
3//! This module provides simple configuration for LLM providers.
4//!
5//! # Direct API Key Configuration
6//!
7//! The simplest way to configure a provider:
8//!
9//! ```rust
10//! use llm_connector::config::ProviderConfig;
11//!
12//! let config = ProviderConfig::new("your-api-key");
13//! ```
14//!
15//! # Advanced Configuration
16//!
17//! For custom settings:
18//!
19//! ```rust
20//! use llm_connector::config::{ProviderConfig, RetryConfig};
21//!
22//! let config = ProviderConfig::new("your-api-key")
23//!     .with_base_url("https://api.example.com/v1")
24//!     .with_timeout_ms(30000)
25//!     .with_retry(RetryConfig {
26//!         max_retries: 3,
27//!         initial_backoff_ms: 1000,
28//!         backoff_multiplier: 2.0,
29//!         max_backoff_ms: 30000,
30//!     })
31//!     .with_header("X-Custom-Header", "value");
32//! ```
33
34use serde::{Deserialize, Serialize};
35use std::collections::HashMap;
36use std::sync::Arc;
37
38/// Configuration for retry behavior
39#[derive(Debug, Clone, Serialize, Deserialize)]
40pub struct RetryConfig {
41    /// Maximum number of retry attempts
42    pub max_retries: u32,
43
44    /// Initial backoff delay in milliseconds
45    pub initial_backoff_ms: u64,
46
47    /// Backoff multiplier for exponential backoff
48    pub backoff_multiplier: f32,
49
50    /// Maximum backoff delay in milliseconds
51    pub max_backoff_ms: u64,
52}
53
54impl Default for RetryConfig {
55    fn default() -> Self {
56        Self {
57            max_retries: 3,
58            initial_backoff_ms: 1000,
59            backoff_multiplier: 2.0,
60            max_backoff_ms: 30000,
61        }
62    }
63}
64
65/// Configuration for a single provider
66///
67/// This is the unified configuration structure used across all protocols.
68/// It contains all the necessary information to create and configure a provider.
69#[derive(Debug, Clone, Serialize, Deserialize)]
70pub struct ProviderConfig {
71    /// API key for authentication
72    pub api_key: String,
73
74    /// Optional base URL override
75    /// If not provided, the protocol's default URL will be used
76    #[serde(skip_serializing_if = "Option::is_none")]
77    pub base_url: Option<String>,
78
79    /// Request timeout in milliseconds
80    #[serde(skip_serializing_if = "Option::is_none")]
81    pub timeout_ms: Option<u64>,
82
83    /// Optional HTTP proxy URL
84    #[serde(skip_serializing_if = "Option::is_none")]
85    pub proxy: Option<String>,
86
87    /// Retry configuration
88    #[serde(skip_serializing_if = "Option::is_none")]
89    pub retry: Option<RetryConfig>,
90
91    /// Custom HTTP headers
92    #[serde(skip_serializing_if = "Option::is_none")]
93    pub headers: Option<HashMap<String, String>>,
94
95    /// Maximum concurrent requests (for connection pooling)
96    #[serde(skip_serializing_if = "Option::is_none")]
97    pub max_concurrent_requests: Option<usize>,
98}
99
100impl ProviderConfig {
101    /// Create a new provider configuration with just an API key
102    pub fn new(api_key: impl Into<String>) -> Self {
103        Self {
104            api_key: api_key.into(),
105            base_url: None,
106            timeout_ms: None,
107            proxy: None,
108            retry: None,
109            headers: None,
110            max_concurrent_requests: None,
111        }
112    }
113
114    /// Set the base URL
115    pub fn with_base_url(mut self, base_url: impl Into<String>) -> Self {
116        self.base_url = Some(base_url.into());
117        self
118    }
119
120    /// Set the timeout in milliseconds
121    pub fn with_timeout_ms(mut self, timeout_ms: u64) -> Self {
122        self.timeout_ms = Some(timeout_ms);
123        self
124    }
125
126    /// Set the proxy URL
127    pub fn with_proxy(mut self, proxy: impl Into<String>) -> Self {
128        self.proxy = Some(proxy.into());
129        self
130    }
131
132    /// Set the retry configuration
133    pub fn with_retry(mut self, retry: RetryConfig) -> Self {
134        self.retry = Some(retry);
135        self
136    }
137
138    /// Add a custom header
139    pub fn with_header(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
140        self.headers
141            .get_or_insert_with(HashMap::new)
142            .insert(key.into(), value.into());
143        self
144    }
145
146    /// Set custom headers
147    pub fn with_headers(mut self, headers: HashMap<String, String>) -> Self {
148        self.headers = Some(headers);
149        self
150    }
151
152    /// Set maximum concurrent requests
153    pub fn with_max_concurrent_requests(mut self, max: usize) -> Self {
154        self.max_concurrent_requests = Some(max);
155        self
156    }
157
158    /// Get the timeout duration
159    pub fn timeout(&self) -> std::time::Duration {
160        std::time::Duration::from_millis(self.timeout_ms.unwrap_or(30000))
161    }
162
163    /// Get the retry configuration, or default if not set
164    pub fn retry_config(&self) -> RetryConfig {
165        self.retry.clone().unwrap_or_default()
166    }
167}
168
169/// Shared provider configuration
170///
171/// This is an Arc-wrapped version of ProviderConfig for efficient sharing
172/// across multiple components without cloning.
173#[derive(Debug, Clone)]
174pub struct SharedProviderConfig {
175    inner: Arc<ProviderConfig>,
176}
177
178impl SharedProviderConfig {
179    /// Create a new shared configuration
180    pub fn new(config: ProviderConfig) -> Self {
181        Self {
182            inner: Arc::new(config),
183        }
184    }
185
186    /// Get a reference to the inner configuration
187    pub fn get(&self) -> &ProviderConfig {
188        &self.inner
189    }
190}
191
192impl From<ProviderConfig> for SharedProviderConfig {
193    fn from(config: ProviderConfig) -> Self {
194        Self::new(config)
195    }
196}
197
198impl std::ops::Deref for SharedProviderConfig {
199    type Target = ProviderConfig;
200
201    fn deref(&self) -> &Self::Target {
202        &self.inner
203    }
204}
205
206#[cfg(test)]
207mod tests {
208    use super::*;
209
210    #[test]
211    fn test_provider_config_builder() {
212        let config = ProviderConfig::new("test-key")
213            .with_base_url("https://api.example.com")
214            .with_timeout_ms(5000)
215            .with_header("X-Custom", "value")
216            .with_retry(RetryConfig::default());
217
218        assert_eq!(config.api_key, "test-key");
219        assert_eq!(config.base_url, Some("https://api.example.com".to_string()));
220        assert_eq!(config.timeout_ms, Some(5000));
221        assert!(config.headers.is_some());
222        assert!(config.retry.is_some());
223    }
224
225    #[test]
226    fn test_retry_config_default() {
227        let retry = RetryConfig::default();
228        assert_eq!(retry.max_retries, 3);
229        assert_eq!(retry.initial_backoff_ms, 1000);
230        assert_eq!(retry.backoff_multiplier, 2.0);
231        assert_eq!(retry.max_backoff_ms, 30000);
232    }
233
234    #[test]
235    fn test_shared_config() {
236        let config = ProviderConfig::new("test-key");
237        let shared1 = SharedProviderConfig::new(config.clone());
238        let shared2 = shared1.clone();
239
240        assert_eq!(shared1.api_key, shared2.api_key);
241        assert_eq!(Arc::strong_count(&shared1.inner), 2);
242    }
243}