1use super::dual::DualCache;
7use super::key_generator::{
8 generate_chat_key, generate_chat_key_with_user, generate_embedding_key_with_user,
9};
10use super::types::{
11 CacheKey, CacheStatsSnapshot, CacheWriteIdentity, DualCacheConfig, serialize_write_identity,
12};
13use crate::core::models::openai::{
14 ChatCompletionRequest, ChatCompletionResponse, EmbeddingRequest, EmbeddingResponse,
15};
16use crate::storage::redis::RedisPool;
17use crate::utils::error::gateway_error::Result;
18use serde::{Deserialize, Deserializer, Serialize, Serializer};
19use std::sync::Arc;
20use std::time::Duration;
21use tracing::{info, trace};
22
23pub struct LLMCache {
28 chat_cache: DualCache<CachedChatResponse>,
30 embedding_cache: DualCache<CachedEmbeddingResponse>,
32 config: LLMCacheConfig,
34}
35
36#[derive(Debug, Clone)]
38pub struct LLMCacheConfig {
39 pub cache_config: DualCacheConfig,
41 pub chat_ttl: Duration,
43 pub embedding_ttl: Duration,
45 pub user_specific: bool,
47}
48
49impl Default for LLMCacheConfig {
50 fn default() -> Self {
51 Self {
52 cache_config: DualCacheConfig::default(),
53 chat_ttl: Duration::from_secs(3600), embedding_ttl: Duration::from_secs(86400), user_specific: false,
56 }
57 }
58}
59
60impl LLMCacheConfig {
61 pub fn memory_only() -> Self {
63 Self {
64 cache_config: DualCacheConfig::memory_only(),
65 ..Default::default()
66 }
67 }
68
69 pub fn with_chat_ttl(mut self, ttl: Duration) -> Self {
71 self.chat_ttl = ttl;
72 self
73 }
74
75 pub fn with_embedding_ttl(mut self, ttl: Duration) -> Self {
77 self.embedding_ttl = ttl;
78 self
79 }
80
81 pub fn with_user_specific(mut self) -> Self {
83 self.user_specific = true;
84 self
85 }
86}
87
88fn serialize_chat_response_arc<S>(
89 response: &Arc<ChatCompletionResponse>,
90 serializer: S,
91) -> std::result::Result<S::Ok, S::Error>
92where
93 S: Serializer,
94{
95 response.as_ref().serialize(serializer)
96}
97
98fn deserialize_chat_response_arc<'de, D>(
99 deserializer: D,
100) -> std::result::Result<Arc<ChatCompletionResponse>, D::Error>
101where
102 D: Deserializer<'de>,
103{
104 ChatCompletionResponse::deserialize(deserializer).map(Arc::new)
105}
106
107fn serialize_embedding_response_arc<S>(
108 response: &Arc<EmbeddingResponse>,
109 serializer: S,
110) -> std::result::Result<S::Ok, S::Error>
111where
112 S: Serializer,
113{
114 response.as_ref().serialize(serializer)
115}
116
117fn deserialize_embedding_response_arc<'de, D>(
118 deserializer: D,
119) -> std::result::Result<Arc<EmbeddingResponse>, D::Error>
120where
121 D: Deserializer<'de>,
122{
123 EmbeddingResponse::deserialize(deserializer).map(Arc::new)
124}
125
126#[derive(Debug, Clone, Serialize, Deserialize)]
128pub struct CachedChatResponse {
129 #[serde(
131 serialize_with = "serialize_chat_response_arc",
132 deserialize_with = "deserialize_chat_response_arc"
133 )]
134 pub response: Arc<ChatCompletionResponse>,
135 pub model: String,
137 pub cached: bool,
139 pub cached_at: u64,
141}
142
143impl CachedChatResponse {
144 pub fn new(response: ChatCompletionResponse, model: String) -> Self {
146 Self::from_arc_response(Arc::new(response), model)
147 }
148
149 pub fn from_arc_response(response: Arc<ChatCompletionResponse>, model: String) -> Self {
151 Self {
152 response,
153 model,
154 cached: true,
155 cached_at: std::time::SystemTime::now()
156 .duration_since(std::time::UNIX_EPOCH)
157 .unwrap_or_default()
158 .as_secs(),
159 }
160 }
161
162 pub fn response_arc(&self) -> Arc<ChatCompletionResponse> {
164 Arc::clone(&self.response)
165 }
166
167 pub fn into_response_arc(self) -> Arc<ChatCompletionResponse> {
169 self.response
170 }
171
172 pub fn into_response(self) -> ChatCompletionResponse {
174 Arc::try_unwrap(self.response).unwrap_or_else(|response| (*response).clone())
175 }
176}
177
178impl CacheWriteIdentity for CachedChatResponse {
179 fn cache_write_identity(&self) -> Option<u64> {
180 serialize_write_identity(self.response.as_ref())
182 }
183}
184
185#[derive(Debug, Clone, Serialize, Deserialize)]
187pub struct CachedEmbeddingResponse {
188 #[serde(
190 serialize_with = "serialize_embedding_response_arc",
191 deserialize_with = "deserialize_embedding_response_arc"
192 )]
193 pub response: Arc<EmbeddingResponse>,
194 pub model: String,
196 pub cached: bool,
198 pub cached_at: u64,
200}
201
202impl CachedEmbeddingResponse {
203 pub fn new(response: EmbeddingResponse, model: String) -> Self {
205 Self::from_arc_response(Arc::new(response), model)
206 }
207
208 pub fn from_arc_response(response: Arc<EmbeddingResponse>, model: String) -> Self {
210 Self {
211 response,
212 model,
213 cached: true,
214 cached_at: std::time::SystemTime::now()
215 .duration_since(std::time::UNIX_EPOCH)
216 .unwrap_or_default()
217 .as_secs(),
218 }
219 }
220
221 pub fn response_arc(&self) -> Arc<EmbeddingResponse> {
223 Arc::clone(&self.response)
224 }
225
226 pub fn into_response_arc(self) -> Arc<EmbeddingResponse> {
228 self.response
229 }
230
231 pub fn into_response(self) -> EmbeddingResponse {
233 Arc::try_unwrap(self.response).unwrap_or_else(|response| (*response).clone())
234 }
235}
236
237impl CacheWriteIdentity for CachedEmbeddingResponse {
238 fn cache_write_identity(&self) -> Option<u64> {
239 serialize_write_identity(self.response.as_ref())
240 }
241}
242
243impl LLMCache {
244 pub fn new(config: LLMCacheConfig, redis_pool: Option<Arc<RedisPool>>) -> Self {
246 let chat_cache = DualCache::with_write_identity(
249 config.cache_config.clone(),
250 redis_pool.clone(),
251 <CachedChatResponse as CacheWriteIdentity>::cache_write_identity,
252 );
253 let embedding_cache = DualCache::with_write_identity(
254 config.cache_config.clone(),
255 redis_pool,
256 <CachedEmbeddingResponse as CacheWriteIdentity>::cache_write_identity,
257 );
258
259 Self {
260 chat_cache,
261 embedding_cache,
262 config,
263 }
264 }
265
266 pub fn memory_only() -> Self {
268 Self::new(LLMCacheConfig::memory_only(), None)
269 }
270
271 pub fn with_defaults() -> Self {
273 Self::new(LLMCacheConfig::default(), None)
274 }
275
276 pub fn start_cleanup_tasks(&self) {
278 self.chat_cache.start_cleanup_task();
279 self.embedding_cache.start_cleanup_task();
280 }
281
282 pub async fn get_chat_response(
286 &self,
287 request: &ChatCompletionRequest,
288 ) -> Result<Option<Arc<ChatCompletionResponse>>> {
289 self.get_chat_response_with_user(request, None).await
290 }
291
292 pub async fn get_chat_response_with_user(
294 &self,
295 request: &ChatCompletionRequest,
296 user_id: Option<&str>,
297 ) -> Result<Option<Arc<ChatCompletionResponse>>> {
298 if request.stream.unwrap_or(false) {
300 return Ok(None);
301 }
302
303 let key = if self.config.user_specific {
304 generate_chat_key_with_user(request, user_id)
305 } else {
306 generate_chat_key(request)
307 };
308
309 if let Some(cached) = self.chat_cache.get(&key).await? {
310 trace!(
311 model = %cached.model,
312 key = %key,
313 "Chat cache hit"
314 );
315 return Ok(Some(cached.response_arc()));
316 }
317
318 Ok(None)
319 }
320
321 pub async fn cache_chat_response(
323 &self,
324 request: &ChatCompletionRequest,
325 response: ChatCompletionResponse,
326 ) -> Result<()> {
327 self.cache_chat_response_with_user(request, response, None)
328 .await
329 }
330
331 pub async fn cache_chat_response_with_user(
333 &self,
334 request: &ChatCompletionRequest,
335 response: ChatCompletionResponse,
336 user_id: Option<&str>,
337 ) -> Result<()> {
338 if request.stream.unwrap_or(false) {
340 return Ok(());
341 }
342
343 let key = if self.config.user_specific {
344 generate_chat_key_with_user(request, user_id)
345 } else {
346 generate_chat_key(request)
347 };
348
349 let cached = CachedChatResponse::new(response, request.model.clone());
350 self.chat_cache
351 .set_with_ttl(key.clone(), cached, self.config.chat_ttl)
352 .await?;
353
354 trace!(
355 model = %request.model,
356 key = %key,
357 ttl_secs = self.config.chat_ttl.as_secs(),
358 "Chat response cached"
359 );
360
361 Ok(())
362 }
363
364 pub async fn invalidate_chat(&self, request: &ChatCompletionRequest) -> Result<bool> {
366 self.invalidate_chat_with_user(request, None).await
367 }
368
369 pub async fn invalidate_chat_with_user(
375 &self,
376 request: &ChatCompletionRequest,
377 user_id: Option<&str>,
378 ) -> Result<bool> {
379 let key = if self.config.user_specific {
380 generate_chat_key_with_user(request, user_id)
381 } else {
382 generate_chat_key(request)
383 };
384 self.chat_cache.delete(&key).await
385 }
386
387 pub async fn invalidate_chat_with_user_matching(
398 &self,
399 request: &ChatCompletionRequest,
400 user_id: Option<&str>,
401 expected: &ChatCompletionResponse,
402 ) -> Result<bool> {
403 let key = if self.config.user_specific {
404 generate_chat_key_with_user(request, user_id)
405 } else {
406 generate_chat_key(request)
407 };
408
409 self.chat_cache
410 .delete_if(&key, |current| {
411 chat_response_matches_cached(current.response.as_ref(), expected)
412 })
413 .await
414 }
415
416 fn embedding_key(
419 &self,
420 request: &EmbeddingRequest,
421 user_id: Option<&str>,
422 deployment_id: &str,
423 ) -> CacheKey {
424 generate_embedding_key_with_user(
425 request,
426 if self.config.user_specific {
427 user_id
428 } else {
429 None
430 },
431 Some(deployment_id),
432 )
433 }
434
435 pub async fn get_embedding_response(
437 &self,
438 request: &EmbeddingRequest,
439 user_id: Option<&str>,
440 deployment_id: &str,
441 ) -> Result<Option<Arc<EmbeddingResponse>>> {
442 let key = self.embedding_key(request, user_id, deployment_id);
443
444 if let Some(cached) = self.embedding_cache.get(&key).await? {
445 trace!(
446 model = %cached.model,
447 key = %key,
448 "Embedding cache hit"
449 );
450 return Ok(Some(cached.response_arc()));
451 }
452
453 Ok(None)
454 }
455
456 pub async fn cache_embedding_response(
458 &self,
459 request: &EmbeddingRequest,
460 response: EmbeddingResponse,
461 user_id: Option<&str>,
462 deployment_id: &str,
463 ) -> Result<()> {
464 let key = self.embedding_key(request, user_id, deployment_id);
465 let cached = CachedEmbeddingResponse::new(response, request.model.clone());
466
467 self.embedding_cache
468 .set_with_ttl(key.clone(), cached, self.config.embedding_ttl)
469 .await?;
470
471 trace!(
472 model = %request.model,
473 key = %key,
474 ttl_secs = self.config.embedding_ttl.as_secs(),
475 "Embedding response cached"
476 );
477
478 Ok(())
479 }
480
481 pub async fn invalidate_embedding(
483 &self,
484 request: &EmbeddingRequest,
485 user_id: Option<&str>,
486 deployment_id: &str,
487 ) -> Result<bool> {
488 let key = self.embedding_key(request, user_id, deployment_id);
489 self.embedding_cache.delete(&key).await
490 }
491
492 pub async fn get<T>(&self, _key: &CacheKey) -> Result<Option<T>>
496 where
497 T: serde::de::DeserializeOwned + Clone + Send + Sync + 'static,
498 {
499 Ok(None)
502 }
503
504 pub async fn set<T>(&self, _key: CacheKey, _value: T, _ttl: Duration) -> Result<()>
506 where
507 T: serde::Serialize + Clone + Send + Sync + 'static,
508 {
509 Ok(())
511 }
512
513 pub fn chat_stats(&self) -> CacheStatsSnapshot {
517 self.chat_cache.stats()
518 }
519
520 pub fn embedding_stats(&self) -> CacheStatsSnapshot {
522 self.embedding_cache.stats()
523 }
524
525 pub fn combined_stats(&self) -> CombinedCacheStats {
527 CombinedCacheStats {
528 chat: self.chat_cache.stats(),
529 embedding: self.embedding_cache.stats(),
530 }
531 }
532
533 pub async fn clear(&self) -> Result<()> {
535 self.chat_cache.clear().await?;
536 self.embedding_cache.clear().await?;
537 info!("LLM caches cleared");
538 Ok(())
539 }
540
541 pub async fn clear_chat(&self) -> Result<()> {
543 self.chat_cache.clear().await
544 }
545
546 pub async fn clear_embedding(&self) -> Result<()> {
548 self.embedding_cache.clear().await
549 }
550
551 pub async fn is_redis_available(&self) -> bool {
553 self.chat_cache.is_redis_available().await
554 }
555
556 pub fn config(&self) -> &LLMCacheConfig {
558 &self.config
559 }
560
561 pub fn shutdown(&self) {
563 self.chat_cache.shutdown();
564 self.embedding_cache.shutdown();
565 }
566}
567
568fn chat_response_matches_cached(
575 current: &ChatCompletionResponse,
576 expected: &ChatCompletionResponse,
577) -> bool {
578 match (serde_json::to_vec(current), serde_json::to_vec(expected)) {
579 (Ok(left), Ok(right)) => left == right,
580 _ => false,
583 }
584}
585
586#[derive(Debug, Clone, Serialize, Deserialize)]
588pub struct CombinedCacheStats {
589 pub chat: CacheStatsSnapshot,
591 pub embedding: CacheStatsSnapshot,
593}
594
595impl CombinedCacheStats {
596 pub fn total_hits(&self) -> u64 {
598 self.chat.total_hits() + self.embedding.total_hits()
599 }
600
601 pub fn total_misses(&self) -> u64 {
603 self.chat.total_misses() + self.embedding.total_misses()
604 }
605
606 pub fn hit_rate(&self) -> f64 {
608 let total = self.total_hits() + self.total_misses();
609 if total == 0 {
610 0.0
611 } else {
612 self.total_hits() as f64 / total as f64
613 }
614 }
615}
616
617#[cfg(test)]
618#[path = "llm_cache_tests.rs"]
619mod tests;