litellm_rs/config/validation/
storage_validators.rs1use 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 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}