Skip to main content

litellm_rs/config/validation/
storage_validators.rs

1//! Storage configuration validators
2//!
3//! This module provides validation implementations for storage-related configuration
4//! structures including StorageConfig, DatabaseConfig, RedisConfig, and VectorDbConfig.
5
6use super::trait_def::Validate;
7use crate::config::models::file_storage::{FileStorageConfig, VectorDbConfig};
8use crate::config::models::storage::{DatabaseConfig, RedisConfig, StorageConfig};
9use tracing::debug;
10
11impl Validate for StorageConfig {
12    fn validate(&self) -> Result<(), String> {
13        debug!("Validating storage configuration");
14
15        if let Some(key_env) = &self.config_sync_key_env
16            && (key_env.trim().is_empty()
17                || !self.database.enabled
18                || !self.redis.enabled
19                || self.redis.allow_degraded
20                || !self.database.url.starts_with("postgres")
21                || self.database.fallback_to_sqlite)
22        {
23            return Err("config_sync_key_env requires a nonempty env name, PostgreSQL without SQLite fallback, and enabled Redis without degraded mode".into());
24        }
25        self.database.validate()?;
26        if self.redis.enabled {
27            self.redis.validate()?;
28        }
29
30        self.files.validate()?;
31
32        if let Some(vector_db) = &self.vector_db {
33            vector_db.validate()?;
34        }
35
36        self.request_ledger.validate()?;
37
38        Ok(())
39    }
40}
41
42impl Validate for FileStorageConfig {
43    fn validate(&self) -> Result<(), String> {
44        match self.storage_type.as_str() {
45            "local" => {
46                if self
47                    .local_path
48                    .as_deref()
49                    .is_some_and(|path| path.trim().is_empty())
50                {
51                    return Err("File storage local_path cannot be empty".to_string());
52                }
53                Ok(())
54            }
55            "s3" => {
56                let s3 = self.s3.as_ref().ok_or_else(|| {
57                    "File storage s3 configuration is required when storage_type is s3".to_string()
58                })?;
59                if s3.bucket.trim().is_empty() {
60                    return Err("File storage S3 bucket cannot be empty".to_string());
61                }
62                if s3.region.trim().is_empty() {
63                    return Err("File storage S3 region cannot be empty".to_string());
64                }
65                if s3.access_key_id.trim().is_empty() {
66                    return Err("File storage S3 access_key_id cannot be empty".to_string());
67                }
68                if s3.secret_access_key.trim().is_empty() {
69                    return Err("File storage S3 secret_access_key cannot be empty".to_string());
70                }
71                Ok(())
72            }
73            other => Err(format!("Unsupported file storage type: {}", other)),
74        }
75    }
76}
77
78impl Validate for DatabaseConfig {
79    fn validate(&self) -> Result<(), String> {
80        if !self.enabled {
81            if cfg!(feature = "storage") && !cfg!(feature = "sqlite") {
82                return Err(
83                    "storage.database.enabled=false requires the `sqlite` feature because the runtime uses an in-memory SQLite backend"
84                        .to_string(),
85                );
86            }
87            return Ok(());
88        }
89
90        if self.url.is_empty() {
91            return Err("Database URL cannot be empty".to_string());
92        }
93
94        if self.fallback_to_sqlite && !cfg!(feature = "sqlite") {
95            return Err(
96                "storage.database.fallback_to_sqlite=true requires the `sqlite` feature"
97                    .to_string(),
98            );
99        }
100
101        if self.url.starts_with("postgresql://") || self.url.starts_with("postgres://") {
102            if !cfg!(feature = "postgres") {
103                return Err("PostgreSQL database URLs require the `postgres` feature".to_string());
104            }
105        } else if self.url.starts_with("sqlite:") {
106            if !cfg!(feature = "sqlite") {
107                return Err("SQLite database URLs require the `sqlite` feature".to_string());
108            }
109        } else {
110            return Err("Database URL must use PostgreSQL or SQLite".to_string());
111        }
112
113        if self.max_connections == 0 {
114            return Err("Database max connections must be greater than 0".to_string());
115        }
116
117        if self.max_connections > 1000 {
118            return Err("Database max connections should not exceed 1000".to_string());
119        }
120
121        if self.connection_timeout == 0 {
122            return Err("Database connection timeout must be greater than 0".to_string());
123        }
124
125        Ok(())
126    }
127}
128
129impl Validate for RedisConfig {
130    fn validate(&self) -> Result<(), String> {
131        if !self.enabled {
132            return Ok(());
133        }
134
135        if self.url.is_empty() {
136            return Err("Redis URL cannot be empty".to_string());
137        }
138
139        if !self.url.starts_with("redis://") && !self.url.starts_with("rediss://") {
140            return Err("Redis URL must start with redis:// or rediss://".to_string());
141        }
142
143        if self.max_connections == 0 {
144            return Err("Redis max connections must be greater than 0".to_string());
145        }
146
147        if self.connection_timeout == 0 {
148            return Err("Redis connection timeout must be greater than 0".to_string());
149        }
150
151        Ok(())
152    }
153}
154
155impl Validate for VectorDbConfig {
156    fn validate(&self) -> Result<(), String> {
157        // Only Qdrant has a complete runtime implementation today. Weaviate
158        // and Pinecone modules are placeholders and should be rejected during
159        // config validation instead of failing later at runtime.
160        let implemented_types = ["qdrant"];
161        let planned_types = ["weaviate", "pinecone"];
162        if planned_types.contains(&self.db_type.as_str()) {
163            return Err(format!(
164                "Vector DB type '{}' is declared but not implemented yet. Implemented types: {:?}",
165                self.db_type, implemented_types
166            ));
167        }
168        if !implemented_types.contains(&self.db_type.as_str()) {
169            return Err(format!(
170                "Unsupported vector DB type: {}. Implemented types: {:?}",
171                self.db_type, implemented_types
172            ));
173        }
174
175        if self.url.is_empty() {
176            return Err("Vector DB URL cannot be empty".to_string());
177        }
178
179        if self.index_name.is_empty() {
180            return Err("Vector DB index name cannot be empty".to_string());
181        }
182
183        Ok(())
184    }
185}
186
187#[cfg(test)]
188mod tests {
189    use super::*;
190
191    fn vector_config(db_type: &str) -> VectorDbConfig {
192        VectorDbConfig {
193            db_type: db_type.to_string(),
194            url: "http://localhost:6333".to_string(),
195            api_key: "test-key".to_string(),
196            index_name: "vectors".to_string(),
197            allow_degraded: false,
198        }
199    }
200
201    #[test]
202    fn vector_validation_accepts_qdrant() {
203        assert!(vector_config("qdrant").validate().is_ok());
204    }
205
206    #[test]
207    fn vector_validation_rejects_placeholder_backends() {
208        for db_type in ["pinecone", "weaviate"] {
209            let err = vector_config(db_type)
210                .validate()
211                .expect_err("placeholder vector backend must fail validation");
212            assert!(err.contains("not implemented yet"));
213        }
214    }
215
216    #[test]
217    fn file_storage_validation_accepts_configured_local_path() {
218        let config = FileStorageConfig {
219            storage_type: "local".to_string(),
220            local_path: Some("/var/lib/litellm/files".to_string()),
221            s3: None,
222        };
223
224        assert!(config.validate().is_ok());
225    }
226
227    #[test]
228    fn file_storage_validation_rejects_missing_s3_config() {
229        let config = FileStorageConfig {
230            storage_type: "s3".to_string(),
231            local_path: None,
232            s3: None,
233        };
234
235        let err = config
236            .validate()
237            .expect_err("s3 file storage should require s3 config");
238        assert!(err.contains("s3 configuration is required"));
239    }
240}