Skip to main content

litellm_rs/config/models/
file_storage.rs

1//! File storage configuration
2
3use serde::{Deserialize, Serialize};
4
5/// File storage configuration
6#[derive(Debug, Clone, Serialize, Deserialize)]
7#[serde(deny_unknown_fields)]
8pub struct FileStorageConfig {
9    /// Storage type (local, s3, etc.)
10    #[serde(default = "default_file_storage_type")]
11    pub storage_type: String,
12    /// Local storage path
13    #[serde(default)]
14    pub local_path: Option<String>,
15    /// S3 configuration
16    #[serde(default)]
17    pub s3: Option<S3Config>,
18}
19
20impl Default for FileStorageConfig {
21    fn default() -> Self {
22        Self {
23            storage_type: "local".to_string(),
24            local_path: None,
25            s3: None,
26        }
27    }
28}
29
30fn default_file_storage_type() -> String {
31    "local".to_string()
32}
33
34impl FileStorageConfig {
35    /// Merge file storage configurations.
36    pub fn merge(mut self, other: Self) -> Self {
37        let default = Self::default();
38
39        if !other.storage_type.is_empty() && other.storage_type != default.storage_type {
40            self.storage_type = other.storage_type;
41        }
42        if other.local_path.is_some() {
43            self.local_path = other.local_path;
44        }
45        if other.s3.is_some() {
46            self.s3 = other.s3;
47        }
48
49        self
50    }
51}
52
53/// S3 configuration
54#[derive(Clone, Serialize, Deserialize)]
55#[serde(deny_unknown_fields)]
56pub struct S3Config {
57    /// S3 bucket name
58    pub bucket: String,
59    /// AWS region
60    pub region: String,
61    /// Access key ID
62    pub access_key_id: String,
63    /// Secret access key
64    pub secret_access_key: String,
65    /// Endpoint URL (for S3-compatible services)
66    pub endpoint: Option<String>,
67}
68
69impl std::fmt::Debug for S3Config {
70    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
71        f.debug_struct("S3Config")
72            .field("bucket", &self.bucket)
73            .field("region", &self.region)
74            .field("access_key_id", &"***REDACTED***")
75            .field("secret_access_key", &"***REDACTED***")
76            .field("endpoint", &self.endpoint)
77            .finish()
78    }
79}
80
81/// Vector database configuration
82#[derive(Clone, Serialize, Deserialize)]
83#[serde(deny_unknown_fields)]
84pub struct VectorDbConfig {
85    /// Vector DB type (pinecone, weaviate, etc.)
86    pub db_type: String,
87    /// Connection URL
88    pub url: String,
89    /// API key
90    pub api_key: String,
91    /// Index name
92    pub index_name: String,
93    /// When the vector DB section is configured and init fails, allow the gateway
94    /// to keep running without a vector backend instead of failing startup.
95    ///
96    /// Defaults to `false` so a configured-but-broken vector DB is surfaced at
97    /// startup. Set to `true` only when the vector DB is best-effort (e.g.
98    /// optional semantic cache) and silent degradation is acceptable.
99    #[serde(default)]
100    pub allow_degraded: bool,
101}
102
103impl std::fmt::Debug for VectorDbConfig {
104    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
105        f.debug_struct("VectorDbConfig")
106            .field("db_type", &self.db_type)
107            .field("url", &self.url)
108            .field("api_key", &"***REDACTED***")
109            .field("index_name", &self.index_name)
110            .field("allow_degraded", &self.allow_degraded)
111            .finish()
112    }
113}
114
115impl Default for VectorDbConfig {
116    fn default() -> Self {
117        Self {
118            db_type: "pinecone".to_string(),
119            url: String::new(),
120            api_key: String::new(),
121            index_name: "default".to_string(),
122            allow_degraded: false,
123        }
124    }
125}
126
127/// Alerting configuration
128#[derive(Clone, Serialize, Deserialize, Default)]
129#[serde(deny_unknown_fields)]
130pub struct AlertingConfig {
131    /// Enable alerting
132    #[serde(default)]
133    pub enabled: bool,
134    /// Slack webhook URL
135    pub slack_webhook: Option<String>,
136    /// Email configuration
137    pub email: Option<EmailConfig>,
138}
139
140impl std::fmt::Debug for AlertingConfig {
141    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
142        f.debug_struct("AlertingConfig")
143            .field("enabled", &self.enabled)
144            .field(
145                "slack_webhook",
146                &self.slack_webhook.as_ref().map(|_| "***REDACTED***"),
147            )
148            .field("email", &self.email)
149            .finish()
150    }
151}
152
153/// Email configuration
154#[derive(Clone, Serialize, Deserialize)]
155#[serde(deny_unknown_fields)]
156pub struct EmailConfig {
157    /// SMTP server
158    pub smtp_server: String,
159    /// SMTP port
160    pub smtp_port: u16,
161    /// Username
162    pub username: String,
163    /// Password
164    pub password: String,
165    /// From address
166    pub from_address: String,
167    /// To addresses
168    pub to_addresses: Vec<String>,
169}
170
171impl std::fmt::Debug for EmailConfig {
172    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
173        f.debug_struct("EmailConfig")
174            .field("smtp_server", &self.smtp_server)
175            .field("smtp_port", &self.smtp_port)
176            .field("username", &self.username)
177            .field("password", &"***REDACTED***")
178            .field("from_address", &self.from_address)
179            .field("to_addresses", &self.to_addresses)
180            .finish()
181    }
182}
183
184#[cfg(test)]
185mod tests {
186    use super::*;
187
188    // ==================== FileStorageConfig Tests ====================
189
190    #[test]
191    fn test_file_storage_config_default() {
192        let config = FileStorageConfig::default();
193        assert_eq!(config.storage_type, "local");
194        // local_path is None by default; resolved to absolute path at runtime
195        assert!(config.local_path.is_none());
196        assert!(config.s3.is_none());
197    }
198
199    #[test]
200    fn test_file_storage_config_local() {
201        let config = FileStorageConfig {
202            storage_type: "local".to_string(),
203            local_path: Some("/var/data".to_string()),
204            s3: None,
205        };
206        assert_eq!(config.storage_type, "local");
207        assert_eq!(config.local_path, Some("/var/data".to_string()));
208    }
209
210    #[test]
211    fn test_file_storage_config_s3() {
212        let s3 = S3Config {
213            bucket: "my-bucket".to_string(),
214            region: "us-east-1".to_string(),
215            access_key_id: "AKIAIOSFODNN7EXAMPLE".to_string(),
216            secret_access_key: "secret".to_string(),
217            endpoint: None,
218        };
219
220        let config = FileStorageConfig {
221            storage_type: "s3".to_string(),
222            local_path: None,
223            s3: Some(s3),
224        };
225        assert_eq!(config.storage_type, "s3");
226        assert!(config.s3.is_some());
227    }
228
229    #[test]
230    fn test_file_storage_config_serialization() {
231        let config = FileStorageConfig::default();
232        let json = serde_json::to_value(&config).unwrap();
233        assert_eq!(json["storage_type"], "local");
234        assert!(json["local_path"].is_null());
235    }
236
237    #[test]
238    fn test_file_storage_config_deserializes_partial_local_config() {
239        let json = r#"{"local_path": "/configured/files"}"#;
240        let config: FileStorageConfig = serde_json::from_str(json).unwrap();
241
242        assert_eq!(config.storage_type, "local");
243        assert_eq!(config.local_path, Some("/configured/files".to_string()));
244        assert!(config.s3.is_none());
245    }
246
247    #[test]
248    fn test_file_storage_config_clone() {
249        let config = FileStorageConfig::default();
250        let cloned = config.clone();
251        assert_eq!(config.storage_type, cloned.storage_type);
252    }
253
254    #[test]
255    fn test_file_storage_config_merge_local_path() {
256        let base = FileStorageConfig::default();
257        let other = FileStorageConfig {
258            storage_type: "local".to_string(),
259            local_path: Some("/configured/files".to_string()),
260            s3: None,
261        };
262
263        let merged = base.merge(other);
264
265        assert_eq!(merged.storage_type, "local");
266        assert_eq!(merged.local_path, Some("/configured/files".to_string()));
267    }
268
269    // ==================== S3Config Tests ====================
270
271    #[test]
272    fn test_s3_config_structure() {
273        let config = S3Config {
274            bucket: "test-bucket".to_string(),
275            region: "eu-west-1".to_string(),
276            access_key_id: "access".to_string(),
277            secret_access_key: "secret".to_string(),
278            endpoint: Some("https://s3.custom.com".to_string()),
279        };
280        assert_eq!(config.bucket, "test-bucket");
281        assert_eq!(config.region, "eu-west-1");
282        assert!(config.endpoint.is_some());
283    }
284
285    #[test]
286    fn test_s3_config_serialization() {
287        let config = S3Config {
288            bucket: "bucket".to_string(),
289            region: "us-west-2".to_string(),
290            access_key_id: "key".to_string(),
291            secret_access_key: "secret".to_string(),
292            endpoint: None,
293        };
294        let json = serde_json::to_value(&config).unwrap();
295        assert_eq!(json["bucket"], "bucket");
296        assert_eq!(json["region"], "us-west-2");
297    }
298
299    #[test]
300    fn test_s3_config_deserialization() {
301        let json = r#"{
302            "bucket": "my-bucket",
303            "region": "ap-southeast-1",
304            "access_key_id": "AKIA...",
305            "secret_access_key": "secret123"
306        }"#;
307        let config: S3Config = serde_json::from_str(json).unwrap();
308        assert_eq!(config.bucket, "my-bucket");
309        assert!(config.endpoint.is_none());
310    }
311
312    #[test]
313    fn test_s3_config_clone() {
314        let config = S3Config {
315            bucket: "clone-test".to_string(),
316            region: "us-east-1".to_string(),
317            access_key_id: "key".to_string(),
318            secret_access_key: "secret".to_string(),
319            endpoint: None,
320        };
321        let cloned = config.clone();
322        assert_eq!(config.bucket, cloned.bucket);
323    }
324
325    // ==================== VectorDbConfig Tests ====================
326
327    #[test]
328    fn test_vector_db_config_default() {
329        let config = VectorDbConfig::default();
330        assert_eq!(config.db_type, "pinecone");
331        assert!(config.url.is_empty());
332        assert!(config.api_key.is_empty());
333        assert_eq!(config.index_name, "default");
334    }
335
336    #[test]
337    fn test_vector_db_config_structure() {
338        let config = VectorDbConfig {
339            db_type: "weaviate".to_string(),
340            url: "http://weaviate:8080".to_string(),
341            api_key: "weaviate-key".to_string(),
342            index_name: "embeddings".to_string(),
343            allow_degraded: false,
344        };
345        assert_eq!(config.db_type, "weaviate");
346        assert_eq!(config.index_name, "embeddings");
347    }
348
349    #[test]
350    fn test_vector_db_config_serialization() {
351        let config = VectorDbConfig {
352            db_type: "qdrant".to_string(),
353            url: "http://qdrant:6333".to_string(),
354            api_key: "qdrant-key".to_string(),
355            index_name: "vectors".to_string(),
356            allow_degraded: false,
357        };
358        let json = serde_json::to_value(&config).unwrap();
359        assert_eq!(json["db_type"], "qdrant");
360        assert_eq!(json["url"], "http://qdrant:6333");
361    }
362
363    #[test]
364    fn test_vector_db_config_allow_degraded_default_false() {
365        let config = VectorDbConfig::default();
366        assert!(
367            !config.allow_degraded,
368            "allow_degraded must default to false so explicit failures are surfaced"
369        );
370    }
371
372    #[test]
373    fn test_vector_db_config_clone() {
374        let config = VectorDbConfig::default();
375        let cloned = config.clone();
376        assert_eq!(config.db_type, cloned.db_type);
377    }
378
379    // ==================== AlertingConfig Tests ====================
380
381    #[test]
382    fn test_alerting_config_default() {
383        let config = AlertingConfig::default();
384        assert!(!config.enabled);
385        assert!(config.slack_webhook.is_none());
386        assert!(config.email.is_none());
387    }
388
389    #[test]
390    fn test_alerting_config_with_slack() {
391        let config = AlertingConfig {
392            enabled: true,
393            slack_webhook: Some("https://hooks.slack.com/xxx".to_string()),
394            email: None,
395        };
396        assert!(config.enabled);
397        assert!(config.slack_webhook.is_some());
398    }
399
400    #[test]
401    fn test_alerting_config_serialization() {
402        let config = AlertingConfig {
403            enabled: true,
404            slack_webhook: Some("https://slack.webhook".to_string()),
405            email: None,
406        };
407        let json = serde_json::to_value(&config).unwrap();
408        assert_eq!(json["enabled"], true);
409    }
410
411    #[test]
412    fn test_alerting_config_clone() {
413        let config = AlertingConfig::default();
414        let cloned = config.clone();
415        assert_eq!(config.enabled, cloned.enabled);
416    }
417
418    // ==================== EmailConfig Tests ====================
419
420    #[test]
421    fn test_email_config_structure() {
422        let config = EmailConfig {
423            smtp_server: "smtp.example.com".to_string(),
424            smtp_port: 587,
425            username: "user@example.com".to_string(),
426            password: "password".to_string(),
427            from_address: "alerts@example.com".to_string(),
428            to_addresses: vec!["admin@example.com".to_string()],
429        };
430        assert_eq!(config.smtp_server, "smtp.example.com");
431        assert_eq!(config.smtp_port, 587);
432        assert_eq!(config.to_addresses.len(), 1);
433    }
434
435    #[test]
436    fn test_email_config_serialization() {
437        let config = EmailConfig {
438            smtp_server: "mail.test.com".to_string(),
439            smtp_port: 465,
440            username: "sender".to_string(),
441            password: "pass".to_string(),
442            from_address: "from@test.com".to_string(),
443            to_addresses: vec!["to1@test.com".to_string(), "to2@test.com".to_string()],
444        };
445        let json = serde_json::to_value(&config).unwrap();
446        assert_eq!(json["smtp_port"], 465);
447        assert!(json["to_addresses"].is_array());
448    }
449
450    #[test]
451    fn test_email_config_deserialization() {
452        let json = r#"{
453            "smtp_server": "smtp.gmail.com",
454            "smtp_port": 587,
455            "username": "user",
456            "password": "pass",
457            "from_address": "from@gmail.com",
458            "to_addresses": ["to@gmail.com"]
459        }"#;
460        let config: EmailConfig = serde_json::from_str(json).unwrap();
461        assert_eq!(config.smtp_server, "smtp.gmail.com");
462        assert_eq!(config.smtp_port, 587);
463    }
464
465    #[test]
466    fn test_email_config_clone() {
467        let config = EmailConfig {
468            smtp_server: "smtp.clone.com".to_string(),
469            smtp_port: 25,
470            username: "clone".to_string(),
471            password: "pass".to_string(),
472            from_address: "clone@test.com".to_string(),
473            to_addresses: vec![],
474        };
475        let cloned = config.clone();
476        assert_eq!(config.smtp_server, cloned.smtp_server);
477    }
478}