lc_core/cache/
llm_cache.rs1use crate::language_models::LLMResult;
8use lc_schema::Message;
9use std::collections::HashMap;
10use std::time::{Duration, Instant};
11use tokio::sync::RwLock;
12
13#[derive(Debug, Clone)]
15pub struct CachedLLMResult {
16 pub result: LLMResult,
18 pub cached_at: Instant,
20}
21
22#[derive(Debug, Clone)]
24pub struct CacheConfig {
25 pub max_entries: usize,
27 pub ttl: Option<Duration>,
29 pub enabled: bool,
31}
32
33impl Default for CacheConfig {
34 fn default() -> Self {
35 Self {
36 max_entries: 1000,
37 ttl: Some(Duration::from_secs(3600)), enabled: true,
39 }
40 }
41}
42
43impl CacheConfig {
44 pub fn new() -> Self {
45 Self::default()
46 }
47
48 pub fn no_ttl(mut self) -> Self {
50 self.ttl = None;
51 self
52 }
53
54 pub fn with_ttl(mut self, ttl: Duration) -> Self {
56 self.ttl = Some(ttl);
57 self
58 }
59
60 pub fn with_max_entries(mut self, max: usize) -> Self {
62 self.max_entries = max;
63 self
64 }
65
66 pub fn disabled(mut self) -> Self {
68 self.enabled = false;
69 self
70 }
71}
72
73pub struct LLMCache {
89 config: CacheConfig,
90 store: RwLock<HashMap<String, CachedLLMResult>>,
91}
92
93impl LLMCache {
94 pub fn new() -> Self {
95 Self::with_config(CacheConfig::default())
96 }
97
98 pub fn with_config(config: CacheConfig) -> Self {
99 Self {
100 config,
101 store: RwLock::new(HashMap::new()),
102 }
103 }
104
105 pub fn build_key(messages: &[Message], model: &str) -> Result<String, String> {
111 let serialized = serde_json::to_string(messages)
112 .map_err(|e| format!("cache key serialization failed: {}", e))?;
113 Ok(format!("{}:{}", model, serialized))
114 }
115
116 pub async fn get(&self, key: &str) -> Option<CachedLLMResult> {
120 if !self.config.enabled {
121 return None;
122 }
123
124 let store = self.store.read().await;
125 if let Some(entry) = store.get(key) {
126 if let Some(ttl) = self.config.ttl {
128 if entry.cached_at.elapsed() > ttl {
129 drop(store);
131 let mut store = self.store.write().await;
132 if let Some(entry) = store.get(key) {
134 if entry.cached_at.elapsed() > ttl {
135 store.remove(key);
136 }
137 }
138 return None;
139 }
140 }
141 Some(entry.clone())
142 } else {
143 None
144 }
145 }
146
147 pub async fn put(&self, key: String, result: LLMResult) {
149 if !self.config.enabled {
150 return;
151 }
152
153 let mut store = self.store.write().await;
154
155 if self.config.max_entries > 0 && store.len() >= self.config.max_entries {
157 if let Some(oldest_key) = store
159 .iter()
160 .min_by_key(|(_, v)| v.cached_at)
161 .map(|(k, _)| k.clone())
162 {
163 store.remove(&oldest_key);
164 }
165 }
166
167 store.insert(
168 key,
169 CachedLLMResult {
170 result,
171 cached_at: Instant::now(),
172 },
173 );
174 }
175
176 pub async fn clear(&self) {
178 let mut store = self.store.write().await;
179 store.clear();
180 }
181
182 pub async fn len(&self) -> usize {
184 let store = self.store.read().await;
185 store.len()
186 }
187
188 pub async fn is_empty(&self) -> bool {
190 self.len().await == 0
191 }
192
193 pub async fn evict_expired(&self) -> usize {
195 if let Some(ttl) = self.config.ttl {
196 let mut store = self.store.write().await;
197 let before = store.len();
198 store.retain(|_, v| v.cached_at.elapsed() <= ttl);
199 before - store.len()
200 } else {
201 0
202 }
203 }
204}
205
206impl Default for LLMCache {
207 fn default() -> Self {
208 Self::new()
209 }
210}
211
212#[cfg(test)]
213mod tests {
214 use super::*;
215 use crate::language_models::TokenUsage;
216
217 fn make_result(content: &str) -> LLMResult {
218 LLMResult {
219 content: content.to_string(),
220 model: "test-model".to_string(),
221 token_usage: Some(TokenUsage {
222 prompt_tokens: 10,
223 completion_tokens: 5,
224 total_tokens: 15,
225 }),
226 tool_calls: None,
227 thinking_content: None,
228 }
229 }
230
231 #[tokio::test]
232 async fn test_cache_put_and_get() {
233 let cache = LLMCache::new();
234 let key = "test-key";
235 let result = make_result("Hello, world!");
236
237 cache.put(key.to_string(), result.clone()).await;
238 let cached = cache.get(key).await;
239
240 assert!(cached.is_some());
241 assert_eq!(cached.unwrap().result.content, "Hello, world!");
242 }
243
244 #[tokio::test]
245 async fn test_cache_miss() {
246 let cache = LLMCache::new();
247 let cached = cache.get("non-existent").await;
248 assert!(cached.is_none());
249 }
250
251 #[tokio::test]
252 async fn test_cache_clear() {
253 let cache = LLMCache::new();
254 cache.put("k1".to_string(), make_result("r1")).await;
255 cache.put("k2".to_string(), make_result("r2")).await;
256 assert_eq!(cache.len().await, 2);
257
258 cache.clear().await;
259 assert_eq!(cache.len().await, 0);
260 }
261
262 #[tokio::test]
263 async fn test_cache_disabled() {
264 let config = CacheConfig::new().disabled();
265 let cache = LLMCache::with_config(config);
266
267 cache.put("key".to_string(), make_result("test")).await;
268 let cached = cache.get("key").await;
269 assert!(cached.is_none());
270 }
271
272 #[tokio::test]
273 async fn test_cache_ttl_expiry() {
274 let config = CacheConfig::new().with_ttl(Duration::from_millis(10));
275 let cache = LLMCache::with_config(config);
276
277 cache.put("key".to_string(), make_result("test")).await;
278 assert!(cache.get("key").await.is_some());
279
280 tokio::time::sleep(Duration::from_millis(20)).await;
282 assert!(cache.get("key").await.is_none());
283 }
284
285 #[tokio::test]
286 async fn test_cache_max_entries() {
287 let config = CacheConfig::new().with_max_entries(3).no_ttl();
288 let cache = LLMCache::with_config(config);
289
290 cache.put("a".to_string(), make_result("1")).await;
291 cache.put("b".to_string(), make_result("2")).await;
292 cache.put("c".to_string(), make_result("3")).await;
293 assert_eq!(cache.len().await, 3);
294
295 cache.put("d".to_string(), make_result("4")).await;
297 assert_eq!(cache.len().await, 3);
298 assert!(cache.get("a").await.is_none());
300 }
301
302 #[tokio::test]
303 async fn test_cache_no_ttl() {
304 let config = CacheConfig::new().no_ttl();
305 let cache = LLMCache::with_config(config);
306
307 cache.put("key".to_string(), make_result("persist")).await;
308
309 tokio::time::sleep(Duration::from_millis(10)).await;
311 assert!(cache.get("key").await.is_some());
312 }
313
314 #[tokio::test]
315 async fn test_cache_evict_expired() {
316 let config = CacheConfig::new().with_ttl(Duration::from_millis(0));
318 let cache = LLMCache::with_config(config);
319
320 cache.put("key".to_string(), make_result("test")).await;
321 tokio::time::sleep(Duration::from_millis(1)).await;
322
323 let evicted = cache.evict_expired().await;
324 assert_eq!(evicted, 1);
325 assert!(cache.is_empty().await);
326 }
327
328 #[tokio::test]
329 async fn test_cache_build_key() {
330 let messages = vec![Message::human("Hello"), Message::ai("Hi!")];
331 let key = LLMCache::build_key(&messages, "gpt-4").unwrap();
332 assert!(key.contains("gpt-4"));
333 assert!(key.contains("Hello"));
334 }
335
336 #[tokio::test]
337 async fn test_cache_is_empty() {
338 let cache = LLMCache::new();
339 assert!(cache.is_empty().await);
340
341 cache.put("key".to_string(), make_result("test")).await;
342 assert!(!cache.is_empty().await);
343 }
344}