litellm_rs/core/cache/cloud/
s3.rs1#[cfg(feature = "s3")]
6mod implementation {
7 use async_trait::async_trait;
8 use aws_sdk_s3::Client;
9 use aws_sdk_s3::primitives::ByteStream;
10 use aws_sdk_s3::types::Object as S3Object;
11 use serde::{Serialize, de::DeserializeOwned};
12 use std::time::Duration;
13 use tracing::{debug, error, warn};
14
15 use crate::core::cache::cloud::{CacheMetadata, CloudCache, CloudCacheConfig};
16 use crate::core::cache::types::CacheKey;
17 use crate::utils::error::gateway_error::{GatewayError, Result};
18
19 #[derive(Debug, Clone, Default)]
21 pub struct S3CacheConfig {
22 pub base: CloudCacheConfig,
24 pub region: Option<String>,
26 pub endpoint_url: Option<String>,
28 pub storage_class: S3StorageClass,
30 }
31
32 #[derive(Debug, Clone, Copy, Default)]
34 pub enum S3StorageClass {
35 #[default]
36 Standard,
37 StandardIa,
38 OnezoneIa,
39 IntelligentTiering,
40 Glacier,
41 GlacierIr,
42 }
43
44 impl S3StorageClass {
45 pub fn as_str(&self) -> &'static str {
47 match self {
48 S3StorageClass::Standard => "STANDARD",
49 S3StorageClass::StandardIa => "STANDARD_IA",
50 S3StorageClass::OnezoneIa => "ONEZONE_IA",
51 S3StorageClass::IntelligentTiering => "INTELLIGENT_TIERING",
52 S3StorageClass::Glacier => "GLACIER",
53 S3StorageClass::GlacierIr => "GLACIER_IR",
54 }
55 }
56 }
57
58 impl S3CacheConfig {
59 pub fn new(bucket: impl Into<String>) -> Self {
61 Self {
62 base: CloudCacheConfig::new(bucket),
63 ..Default::default()
64 }
65 }
66
67 pub fn region(mut self, region: impl Into<String>) -> Self {
69 self.region = Some(region.into());
70 self
71 }
72
73 pub fn endpoint_url(mut self, url: impl Into<String>) -> Self {
75 self.endpoint_url = Some(url.into());
76 self
77 }
78
79 pub fn storage_class(mut self, class: S3StorageClass) -> Self {
81 self.storage_class = class;
82 self
83 }
84
85 pub fn prefix(mut self, prefix: impl Into<String>) -> Self {
87 self.base.prefix = prefix.into();
88 self
89 }
90
91 pub fn default_ttl(mut self, ttl: Duration) -> Self {
93 self.base.default_ttl = ttl;
94 self
95 }
96
97 pub fn from_env() -> Option<Self> {
99 let bucket = std::env::var("S3_CACHE_BUCKET").ok()?;
100 Some(Self {
101 base: CloudCacheConfig::new(bucket),
102 region: std::env::var("AWS_REGION").ok(),
103 endpoint_url: std::env::var("S3_ENDPOINT_URL").ok(),
104 storage_class: S3StorageClass::default(),
105 })
106 }
107 }
108
109 pub struct S3Cache {
111 client: Client,
112 config: S3CacheConfig,
113 }
114
115 impl S3Cache {
116 pub async fn new(config: S3CacheConfig) -> Result<Self> {
118 let mut aws_config_builder =
119 aws_config::defaults(aws_config::BehaviorVersion::latest());
120
121 if let Some(ref region) = config.region {
122 aws_config_builder =
123 aws_config_builder.region(aws_config::Region::new(region.clone()));
124 }
125
126 let aws_config = aws_config_builder.load().await;
127
128 let mut s3_config = aws_sdk_s3::config::Builder::from(&aws_config);
129
130 if let Some(ref endpoint) = config.endpoint_url {
131 s3_config = s3_config.endpoint_url(endpoint).force_path_style(true);
132 }
133
134 let client = Client::from_conf(s3_config.build());
135
136 Ok(Self { client, config })
137 }
138
139 pub async fn from_env() -> Result<Self> {
141 let config = S3CacheConfig::from_env().ok_or_else(|| {
142 GatewayError::Config("S3_CACHE_BUCKET not set in environment".to_string())
143 })?;
144 Self::new(config).await
145 }
146
147 fn build_key(&self, key: &CacheKey) -> String {
149 format!("{}{}", self.config.base.prefix, key.as_str())
150 }
151
152 fn build_metadata_key(&self, key: &CacheKey) -> String {
154 format!("{}{}._meta", self.config.base.prefix, key.as_str())
155 }
156 }
157
158 #[async_trait]
159 impl CloudCache for S3Cache {
160 async fn get<T: DeserializeOwned + Send>(&self, key: &CacheKey) -> Result<Option<T>> {
161 let s3_key = self.build_key(key);
162 let meta_key = self.build_metadata_key(key);
163 debug!(key = %s3_key, "Reading from S3 cache");
164
165 match self
167 .client
168 .get_object()
169 .bucket(&self.config.base.bucket)
170 .key(&meta_key)
171 .send()
172 .await
173 {
174 Ok(meta_response) => {
175 let meta_bytes = meta_response
176 .body
177 .collect()
178 .await
179 .map_err(|e| {
180 GatewayError::Internal(format!("Failed to read metadata: {}", e))
181 })?
182 .into_bytes();
183
184 let metadata: CacheMetadata =
185 serde_json::from_slice(&meta_bytes).map_err(|e| {
186 GatewayError::Internal(format!("Failed to parse metadata: {}", e))
187 })?;
188
189 if metadata.is_expired() {
190 debug!(key = %s3_key, "Cache entry expired");
191 let _ = self.delete(key).await;
193 return Ok(None);
194 }
195 }
196 Err(err) => {
197 let err_str = err.to_string();
198 if err_str.contains("NoSuchKey") || err_str.contains("404") {
199 debug!(key = %s3_key, "Cache miss - no metadata");
200 return Ok(None);
201 }
202 warn!(key = %s3_key, error = %err, "Failed to read metadata");
203 return Ok(None);
204 }
205 }
206
207 match self
209 .client
210 .get_object()
211 .bucket(&self.config.base.bucket)
212 .key(&s3_key)
213 .send()
214 .await
215 {
216 Ok(response) => {
217 let bytes = response
218 .body
219 .collect()
220 .await
221 .map_err(|e| GatewayError::Internal(format!("Failed to read body: {}", e)))?
222 .into_bytes();
223
224 let value: T = serde_json::from_slice(&bytes).map_err(|e| {
225 GatewayError::Internal(format!("Failed to deserialize: {}", e))
226 })?;
227
228 debug!(key = %s3_key, "S3 cache hit");
229 Ok(Some(value))
230 }
231 Err(err) => {
232 let err_str = err.to_string();
233 if err_str.contains("NoSuchKey") || err_str.contains("404") {
234 debug!(key = %s3_key, "Cache miss");
235 Ok(None)
236 } else {
237 error!(key = %s3_key, error = %err, "Failed to read from S3");
238 Err(GatewayError::Internal(format!("S3 read error: {}", err)))
239 }
240 }
241 }
242 }
243
244 async fn set<T: Serialize + Send + Sync>(
245 &self,
246 key: &CacheKey,
247 value: &T,
248 ttl: Duration,
249 ) -> Result<()> {
250 let s3_key = self.build_key(key);
251 let meta_key = self.build_metadata_key(key);
252 debug!(key = %s3_key, ttl_secs = ttl.as_secs(), "Writing to S3 cache");
253
254 let bytes = serde_json::to_vec(value)
256 .map_err(|e| GatewayError::Internal(format!("Failed to serialize: {}", e)))?;
257
258 let metadata = CacheMetadata::new(ttl, bytes.len(), false);
260 let meta_bytes = serde_json::to_vec(&metadata).map_err(|e| {
261 GatewayError::Internal(format!("Failed to serialize metadata: {}", e))
262 })?;
263
264 self.client
266 .put_object()
267 .bucket(&self.config.base.bucket)
268 .key(&s3_key)
269 .body(ByteStream::from(bytes))
270 .storage_class(self.config.storage_class.as_str().parse().map_err(|e| {
271 GatewayError::Config(format!("Invalid S3 storage class: {}", e))
272 })?)
273 .content_type("application/json")
274 .send()
275 .await
276 .map_err(|e| GatewayError::Internal(format!("Failed to write to S3: {}", e)))?;
277
278 self.client
280 .put_object()
281 .bucket(&self.config.base.bucket)
282 .key(&meta_key)
283 .body(ByteStream::from(meta_bytes))
284 .content_type("application/json")
285 .send()
286 .await
287 .map_err(|e| GatewayError::Internal(format!("Failed to write metadata: {}", e)))?;
288
289 debug!(key = %s3_key, "S3 cache write successful");
290 Ok(())
291 }
292
293 async fn delete(&self, key: &CacheKey) -> Result<bool> {
294 let s3_key = self.build_key(key);
295 let meta_key = self.build_metadata_key(key);
296 debug!(key = %s3_key, "Deleting from S3 cache");
297
298 let value_result = self
300 .client
301 .delete_object()
302 .bucket(&self.config.base.bucket)
303 .key(&s3_key)
304 .send()
305 .await;
306
307 let meta_result = self
308 .client
309 .delete_object()
310 .bucket(&self.config.base.bucket)
311 .key(&meta_key)
312 .send()
313 .await;
314
315 if value_result.is_err() && meta_result.is_err() {
317 return Ok(false);
318 }
319
320 debug!(key = %s3_key, "S3 cache delete successful");
321 Ok(true)
322 }
323
324 async fn exists(&self, key: &CacheKey) -> Result<bool> {
325 let s3_key = self.build_key(key);
326
327 match self
328 .client
329 .head_object()
330 .bucket(&self.config.base.bucket)
331 .key(&s3_key)
332 .send()
333 .await
334 {
335 Ok(_) => Ok(true),
336 Err(err) => {
337 let err_str = err.to_string();
338 if err_str.contains("NotFound") || err_str.contains("404") {
339 Ok(false)
340 } else {
341 Err(GatewayError::Internal(format!("S3 head error: {}", err)))
342 }
343 }
344 }
345 }
346
347 async fn list_keys(&self, prefix: &str) -> Result<Vec<String>> {
348 let full_prefix = format!("{}{}", self.config.base.prefix, prefix);
349
350 let response = self
351 .client
352 .list_objects_v2()
353 .bucket(&self.config.base.bucket)
354 .prefix(&full_prefix)
355 .send()
356 .await
357 .map_err(|e| GatewayError::Internal(format!("Failed to list objects: {}", e)))?;
358
359 let mut keys = Vec::new();
360 for obj in response.contents().iter() {
361 let obj: &S3Object = obj;
362 if let Some(k) = obj.key() {
363 let key = k
364 .strip_prefix(&self.config.base.prefix)
365 .unwrap_or(k)
366 .to_string();
367 if !key.ends_with("._meta") {
368 keys.push(key);
369 }
370 }
371 }
372
373 Ok(keys)
374 }
375
376 async fn clear(&self) -> Result<()> {
377 warn!("Clearing all S3 cache entries");
378
379 let keys = self.list_keys("").await?;
380
381 for key in keys {
382 let cache_key = CacheKey::new(key);
383 let _ = self.delete(&cache_key).await;
384 }
385
386 Ok(())
387 }
388
389 fn name(&self) -> &'static str {
390 "s3"
391 }
392 }
393
394 impl std::fmt::Debug for S3Cache {
395 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
396 f.debug_struct("S3Cache")
397 .field("bucket", &self.config.base.bucket)
398 .field("prefix", &self.config.base.prefix)
399 .field("region", &self.config.region)
400 .finish()
401 }
402 }
403}
404
405#[cfg(feature = "s3")]
406pub use implementation::*;
407
408#[cfg(not(feature = "s3"))]
410mod stub {
411 use crate::utils::error::gateway_error::{GatewayError, Result};
412
413 #[derive(Debug, Clone, Default)]
415 pub struct S3CacheConfig {
416 pub bucket: String,
417 }
418
419 impl S3CacheConfig {
420 pub fn new(bucket: impl Into<String>) -> Self {
421 Self {
422 bucket: bucket.into(),
423 }
424 }
425
426 pub fn from_env() -> Option<Self> {
427 None
428 }
429 }
430
431 #[derive(Debug)]
433 pub struct S3Cache;
434
435 impl S3Cache {
436 pub async fn new(_config: S3CacheConfig) -> Result<Self> {
437 Err(GatewayError::Config(
438 "S3 cache support not enabled. Enable the 's3' feature.".to_string(),
439 ))
440 }
441
442 pub async fn from_env() -> Result<Self> {
443 Err(GatewayError::Config(
444 "S3 cache support not enabled. Enable the 's3' feature.".to_string(),
445 ))
446 }
447 }
448}
449
450#[cfg(not(feature = "s3"))]
451pub use stub::*;
452
453#[cfg(all(test, feature = "s3"))]
454mod tests {
455 use super::*;
456
457 #[test]
458 fn test_s3_cache_config_builder() {
459 let config = S3CacheConfig::new("my-bucket")
460 .region("us-east-1")
461 .prefix("cache/")
462 .storage_class(S3StorageClass::StandardIa);
463
464 assert_eq!(config.base.bucket, "my-bucket");
465 assert_eq!(config.region, Some("us-east-1".to_string()));
466 assert_eq!(config.base.prefix, "cache/");
467 }
468
469 #[test]
470 fn test_s3_storage_class() {
471 assert_eq!(S3StorageClass::Standard.as_str(), "STANDARD");
472 assert_eq!(S3StorageClass::StandardIa.as_str(), "STANDARD_IA");
473 assert_eq!(S3StorageClass::Glacier.as_str(), "GLACIER");
474 }
475}