use super::dual::DualCache;
use super::key_generator::{
generate_chat_key, generate_chat_key_with_user, generate_embedding_key,
};
use super::types::{CacheKey, CacheStatsSnapshot, DualCacheConfig};
use crate::core::models::openai::{
ChatCompletionRequest, ChatCompletionResponse, EmbeddingRequest, EmbeddingResponse,
};
use crate::storage::redis::RedisPool;
use crate::utils::error::gateway_error::Result;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::sync::Arc;
use std::time::Duration;
use tracing::{info, trace};
pub struct LLMCache {
chat_cache: DualCache<CachedChatResponse>,
embedding_cache: DualCache<CachedEmbeddingResponse>,
config: LLMCacheConfig,
}
#[derive(Debug, Clone)]
pub struct LLMCacheConfig {
pub cache_config: DualCacheConfig,
pub chat_ttl: Duration,
pub embedding_ttl: Duration,
pub user_specific: bool,
pub semantic_cache_enabled: bool,
pub similarity_threshold: f64,
}
impl Default for LLMCacheConfig {
fn default() -> Self {
Self {
cache_config: DualCacheConfig::default(),
chat_ttl: Duration::from_secs(3600), embedding_ttl: Duration::from_secs(86400), user_specific: false,
semantic_cache_enabled: false,
similarity_threshold: 0.95,
}
}
}
impl LLMCacheConfig {
pub fn memory_only() -> Self {
Self {
cache_config: DualCacheConfig::memory_only(),
..Default::default()
}
}
pub fn with_chat_ttl(mut self, ttl: Duration) -> Self {
self.chat_ttl = ttl;
self
}
pub fn with_embedding_ttl(mut self, ttl: Duration) -> Self {
self.embedding_ttl = ttl;
self
}
pub fn with_user_specific(mut self) -> Self {
self.user_specific = true;
self
}
}
fn serialize_chat_response_arc<S>(
response: &Arc<ChatCompletionResponse>,
serializer: S,
) -> std::result::Result<S::Ok, S::Error>
where
S: Serializer,
{
response.as_ref().serialize(serializer)
}
fn deserialize_chat_response_arc<'de, D>(
deserializer: D,
) -> std::result::Result<Arc<ChatCompletionResponse>, D::Error>
where
D: Deserializer<'de>,
{
ChatCompletionResponse::deserialize(deserializer).map(Arc::new)
}
fn serialize_embedding_response_arc<S>(
response: &Arc<EmbeddingResponse>,
serializer: S,
) -> std::result::Result<S::Ok, S::Error>
where
S: Serializer,
{
response.as_ref().serialize(serializer)
}
fn deserialize_embedding_response_arc<'de, D>(
deserializer: D,
) -> std::result::Result<Arc<EmbeddingResponse>, D::Error>
where
D: Deserializer<'de>,
{
EmbeddingResponse::deserialize(deserializer).map(Arc::new)
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CachedChatResponse {
#[serde(
serialize_with = "serialize_chat_response_arc",
deserialize_with = "deserialize_chat_response_arc"
)]
pub response: Arc<ChatCompletionResponse>,
pub model: String,
pub cached: bool,
pub cached_at: u64,
}
impl CachedChatResponse {
pub fn new(response: ChatCompletionResponse, model: String) -> Self {
Self::from_arc_response(Arc::new(response), model)
}
pub fn from_arc_response(response: Arc<ChatCompletionResponse>, model: String) -> Self {
Self {
response,
model,
cached: true,
cached_at: std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs(),
}
}
pub fn response_arc(&self) -> Arc<ChatCompletionResponse> {
Arc::clone(&self.response)
}
pub fn into_response_arc(self) -> Arc<ChatCompletionResponse> {
self.response
}
pub fn into_response(self) -> ChatCompletionResponse {
Arc::try_unwrap(self.response).unwrap_or_else(|response| (*response).clone())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CachedEmbeddingResponse {
#[serde(
serialize_with = "serialize_embedding_response_arc",
deserialize_with = "deserialize_embedding_response_arc"
)]
pub response: Arc<EmbeddingResponse>,
pub model: String,
pub cached: bool,
pub cached_at: u64,
}
impl CachedEmbeddingResponse {
pub fn new(response: EmbeddingResponse, model: String) -> Self {
Self::from_arc_response(Arc::new(response), model)
}
pub fn from_arc_response(response: Arc<EmbeddingResponse>, model: String) -> Self {
Self {
response,
model,
cached: true,
cached_at: std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs(),
}
}
pub fn response_arc(&self) -> Arc<EmbeddingResponse> {
Arc::clone(&self.response)
}
pub fn into_response_arc(self) -> Arc<EmbeddingResponse> {
self.response
}
pub fn into_response(self) -> EmbeddingResponse {
Arc::try_unwrap(self.response).unwrap_or_else(|response| (*response).clone())
}
}
impl LLMCache {
pub fn new(config: LLMCacheConfig, redis_pool: Option<Arc<RedisPool>>) -> Self {
let chat_cache = DualCache::new(config.cache_config.clone(), redis_pool.clone());
let embedding_cache = DualCache::new(config.cache_config.clone(), redis_pool);
Self {
chat_cache,
embedding_cache,
config,
}
}
pub fn memory_only() -> Self {
Self::new(LLMCacheConfig::memory_only(), None)
}
pub fn with_defaults() -> Self {
Self::new(LLMCacheConfig::default(), None)
}
pub fn start_cleanup_tasks(&self) {
self.chat_cache.start_cleanup_task();
self.embedding_cache.start_cleanup_task();
}
pub async fn get_chat_response(
&self,
request: &ChatCompletionRequest,
) -> Result<Option<Arc<ChatCompletionResponse>>> {
self.get_chat_response_with_user(request, None).await
}
pub async fn get_chat_response_with_user(
&self,
request: &ChatCompletionRequest,
user_id: Option<&str>,
) -> Result<Option<Arc<ChatCompletionResponse>>> {
if request.stream.unwrap_or(false) {
return Ok(None);
}
let key = if self.config.user_specific {
generate_chat_key_with_user(request, user_id)
} else {
generate_chat_key(request)
};
if let Some(cached) = self.chat_cache.get(&key).await? {
trace!(
model = %cached.model,
key = %key,
"Chat cache hit"
);
return Ok(Some(cached.response_arc()));
}
Ok(None)
}
pub async fn cache_chat_response(
&self,
request: &ChatCompletionRequest,
response: ChatCompletionResponse,
) -> Result<()> {
self.cache_chat_response_with_user(request, response, None)
.await
}
pub async fn cache_chat_response_with_user(
&self,
request: &ChatCompletionRequest,
response: ChatCompletionResponse,
user_id: Option<&str>,
) -> Result<()> {
if request.stream.unwrap_or(false) {
return Ok(());
}
let key = if self.config.user_specific {
generate_chat_key_with_user(request, user_id)
} else {
generate_chat_key(request)
};
let cached = CachedChatResponse::new(response, request.model.clone());
self.chat_cache
.set_with_ttl(key.clone(), cached, self.config.chat_ttl)
.await?;
trace!(
model = %request.model,
key = %key,
ttl_secs = self.config.chat_ttl.as_secs(),
"Chat response cached"
);
Ok(())
}
pub async fn invalidate_chat(&self, request: &ChatCompletionRequest) -> Result<bool> {
self.invalidate_chat_with_user(request, None).await
}
pub async fn invalidate_chat_with_user(
&self,
request: &ChatCompletionRequest,
user_id: Option<&str>,
) -> Result<bool> {
let key = if self.config.user_specific {
generate_chat_key_with_user(request, user_id)
} else {
generate_chat_key(request)
};
self.chat_cache.delete(&key).await
}
pub async fn get_embedding_response(
&self,
request: &EmbeddingRequest,
) -> Result<Option<Arc<EmbeddingResponse>>> {
let key = generate_embedding_key(request);
if let Some(cached) = self.embedding_cache.get(&key).await? {
trace!(
model = %cached.model,
key = %key,
"Embedding cache hit"
);
return Ok(Some(cached.response_arc()));
}
Ok(None)
}
pub async fn cache_embedding_response(
&self,
request: &EmbeddingRequest,
response: EmbeddingResponse,
) -> Result<()> {
let key = generate_embedding_key(request);
let cached = CachedEmbeddingResponse::new(response, request.model.clone());
self.embedding_cache
.set_with_ttl(key.clone(), cached, self.config.embedding_ttl)
.await?;
trace!(
model = %request.model,
key = %key,
ttl_secs = self.config.embedding_ttl.as_secs(),
"Embedding response cached"
);
Ok(())
}
pub async fn invalidate_embedding(&self, request: &EmbeddingRequest) -> Result<bool> {
let key = generate_embedding_key(request);
self.embedding_cache.delete(&key).await
}
pub async fn get<T>(&self, _key: &CacheKey) -> Result<Option<T>>
where
T: serde::de::DeserializeOwned + Clone + Send + Sync + 'static,
{
Ok(None)
}
pub async fn set<T>(&self, _key: CacheKey, _value: T, _ttl: Duration) -> Result<()>
where
T: serde::Serialize + Clone + Send + Sync + 'static,
{
Ok(())
}
pub fn chat_stats(&self) -> CacheStatsSnapshot {
self.chat_cache.stats()
}
pub fn embedding_stats(&self) -> CacheStatsSnapshot {
self.embedding_cache.stats()
}
pub fn combined_stats(&self) -> CombinedCacheStats {
CombinedCacheStats {
chat: self.chat_cache.stats(),
embedding: self.embedding_cache.stats(),
}
}
pub async fn clear(&self) -> Result<()> {
self.chat_cache.clear().await?;
self.embedding_cache.clear().await?;
info!("LLM caches cleared");
Ok(())
}
pub async fn clear_chat(&self) -> Result<()> {
self.chat_cache.clear().await
}
pub async fn clear_embedding(&self) -> Result<()> {
self.embedding_cache.clear().await
}
pub async fn is_redis_available(&self) -> bool {
self.chat_cache.is_redis_available().await
}
pub fn config(&self) -> &LLMCacheConfig {
&self.config
}
pub fn shutdown(&self) {
self.chat_cache.shutdown();
self.embedding_cache.shutdown();
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CombinedCacheStats {
pub chat: CacheStatsSnapshot,
pub embedding: CacheStatsSnapshot,
}
impl CombinedCacheStats {
pub fn total_hits(&self) -> u64 {
self.chat.total_hits() + self.embedding.total_hits()
}
pub fn total_misses(&self) -> u64 {
self.chat.total_misses() + self.embedding.total_misses()
}
pub fn hit_rate(&self) -> f64 {
let total = self.total_hits() + self.total_misses();
if total == 0 {
0.0
} else {
self.total_hits() as f64 / total as f64
}
}
}
#[cfg(test)]
#[path = "llm_cache_tests.rs"]
mod tests;