1use parking_lot::Mutex;
4
5use crate::llm::types::{CompletionResponse, Message};
6use crate::util::fnv1a_hash;
7
8pub struct ResponseCache {
21 entries: Mutex<Vec<(u64, CompletionResponse)>>,
22 capacity: usize,
23}
24
25impl std::fmt::Debug for ResponseCache {
26 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
27 f.debug_struct("ResponseCache")
28 .field("capacity", &self.capacity)
29 .field("len", &self.entries.lock().len())
30 .finish()
31 }
32}
33
34impl ResponseCache {
35 pub fn new(capacity: usize) -> Self {
37 Self {
38 entries: Mutex::new(Vec::with_capacity(capacity)),
39 capacity,
40 }
41 }
42
43 pub fn get(&self, key: u64) -> Option<CompletionResponse> {
45 let mut entries = self.entries.lock();
46 if let Some(pos) = entries.iter().position(|(k, _)| *k == key) {
47 let entry = entries.remove(pos);
48 let response = entry.1.clone();
49 entries.insert(0, entry);
50 Some(response)
51 } else {
52 None
53 }
54 }
55
56 pub fn put(&self, key: u64, response: CompletionResponse) {
59 let mut entries = self.entries.lock();
60 if let Some(pos) = entries.iter().position(|(k, _)| *k == key) {
62 entries.remove(pos);
63 }
64 if entries.len() >= self.capacity {
66 entries.pop();
67 }
68 entries.insert(0, (key, response));
69 }
70
71 pub fn compute_key(system_prompt: &str, messages: &[Message], tool_names: &[&str]) -> u64 {
79 Self::compute_key_scoped(system_prompt, messages, tool_names, None)
80 }
81
82 pub fn compute_key_scoped(
91 system_prompt: &str,
92 messages: &[Message],
93 tool_names: &[&str],
94 namespace: Option<&str>,
95 ) -> u64 {
96 let mut sorted_names: Vec<&str> = tool_names.to_vec();
97 sorted_names.sort();
98
99 let messages_json = serde_json::to_string(messages).expect("messages serialize infallibly");
100
101 let mut data = Vec::new();
102 if let Some(ns) = namespace {
103 data.extend_from_slice(b"ns=");
104 data.extend_from_slice(ns.as_bytes());
105 data.push(0);
106 }
107 data.extend_from_slice(system_prompt.as_bytes());
108 data.push(0); data.extend_from_slice(messages_json.as_bytes());
110 data.push(0); for name in &sorted_names {
112 data.extend_from_slice(name.as_bytes());
113 data.push(0);
114 }
115
116 fnv1a_hash(&data)
117 }
118
119 pub fn clear(&self) {
121 self.entries.lock().clear();
122 }
123
124 pub fn len(&self) -> usize {
126 self.entries.lock().len()
127 }
128
129 pub fn is_empty(&self) -> bool {
131 self.entries.lock().is_empty()
132 }
133}
134
135#[cfg(test)]
136mod tests {
137 use super::*;
138 use crate::llm::types::{ContentBlock, Message, StopReason, TokenUsage};
139
140 fn make_response(text: &str) -> CompletionResponse {
141 CompletionResponse {
142 content: vec![ContentBlock::Text { text: text.into() }],
143 stop_reason: StopReason::EndTurn,
144 reasoning: None,
145 usage: TokenUsage::default(),
146 model: None,
147 }
148 }
149
150 #[test]
151 fn cache_stores_and_retrieves() {
152 let cache = ResponseCache::new(10);
153 let response = make_response("hello");
154 let key = 42;
155 cache.put(key, response.clone());
156
157 let cached = cache.get(key);
158 assert!(cached.is_some());
159 assert_eq!(cached.unwrap().text(), "hello");
160 }
161
162 #[test]
163 fn cache_miss_returns_none() {
164 let cache = ResponseCache::new(10);
165 assert!(cache.get(999).is_none());
166 }
167
168 #[test]
169 fn lru_eviction() {
170 let cache = ResponseCache::new(3);
171 cache.put(1, make_response("one"));
172 cache.put(2, make_response("two"));
173 cache.put(3, make_response("three"));
174
175 cache.put(4, make_response("four"));
177 assert_eq!(cache.len(), 3);
178 assert!(cache.get(1).is_none());
179 assert!(cache.get(2).is_some());
180 assert!(cache.get(3).is_some());
181 assert!(cache.get(4).is_some());
182 }
183
184 #[test]
185 fn lru_access_refreshes_order() {
186 let cache = ResponseCache::new(3);
187 cache.put(1, make_response("one"));
188 cache.put(2, make_response("two"));
189 cache.put(3, make_response("three"));
190
191 let _ = cache.get(1);
193
194 cache.put(4, make_response("four"));
196 assert!(cache.get(1).is_some());
197 assert!(cache.get(2).is_none());
198 assert!(cache.get(3).is_some());
199 assert!(cache.get(4).is_some());
200 }
201
202 #[test]
203 fn compute_key_deterministic() {
204 let msgs = vec![Message::user("hello")];
205 let tools = vec!["search", "read"];
206
207 let key1 = ResponseCache::compute_key("system", &msgs, &tools);
208 let key2 = ResponseCache::compute_key("system", &msgs, &tools);
209 assert_eq!(key1, key2);
210 }
211
212 #[test]
213 fn compute_key_different_for_different_inputs() {
214 let msgs_a = vec![Message::user("hello")];
215 let msgs_b = vec![Message::user("world")];
216 let tools = vec!["search"];
217
218 let key_a = ResponseCache::compute_key("system", &msgs_a, &tools);
219 let key_b = ResponseCache::compute_key("system", &msgs_b, &tools);
220 assert_ne!(key_a, key_b);
221
222 let key_c = ResponseCache::compute_key("other", &msgs_a, &tools);
224 assert_ne!(key_a, key_c);
225
226 let key_d = ResponseCache::compute_key("system", &msgs_a, &["write"]);
228 assert_ne!(key_a, key_d);
229 }
230
231 #[test]
232 fn compute_key_tool_order_independent() {
233 let msgs = vec![Message::user("hi")];
234 let key1 = ResponseCache::compute_key("sys", &msgs, &["a", "b", "c"]);
235 let key2 = ResponseCache::compute_key("sys", &msgs, &["c", "a", "b"]);
236 assert_eq!(key1, key2);
237 }
238
239 #[test]
240 fn clear_empties_cache() {
241 let cache = ResponseCache::new(10);
242 cache.put(1, make_response("one"));
243 cache.put(2, make_response("two"));
244 assert_eq!(cache.len(), 2);
245
246 cache.clear();
247 assert!(cache.is_empty());
248 assert_eq!(cache.len(), 0);
249 }
250
251 #[test]
252 fn put_overwrites_existing_key() {
253 let cache = ResponseCache::new(10);
254 cache.put(1, make_response("first"));
255 cache.put(1, make_response("second"));
256
257 assert_eq!(cache.len(), 1);
258 let cached = cache.get(1).unwrap();
259 assert_eq!(cached.text(), "second");
260 }
261
262 #[test]
263 fn thread_safety() {
264 use std::sync::Arc;
265
266 let cache = Arc::new(ResponseCache::new(100));
267 let mut handles = vec![];
268
269 for i in 0..10 {
270 let cache = cache.clone();
271 handles.push(std::thread::spawn(move || {
272 for j in 0..100 {
273 let key = (i * 100 + j) as u64;
274 cache.put(key, make_response(&format!("resp-{key}")));
275 let _ = cache.get(key);
276 }
277 }));
278 }
279
280 for h in handles {
281 h.join().expect("thread panicked");
282 }
283
284 assert!(cache.len() <= 100);
285 }
286
287 #[test]
292 fn compute_key_scoped_differs_per_tenant() {
293 let msgs = vec![Message::user("hello")];
294 let key_a = ResponseCache::compute_key_scoped("sys", &msgs, &["a"], Some("tenant-a:user1"));
295 let key_b = ResponseCache::compute_key_scoped("sys", &msgs, &["a"], Some("tenant-b:user1"));
296 let key_unscoped = ResponseCache::compute_key("sys", &msgs, &["a"]);
297 assert_ne!(
298 key_a, key_b,
299 "different tenants must produce different keys"
300 );
301 assert_ne!(
302 key_a, key_unscoped,
303 "scoped key must differ from unscoped key"
304 );
305 }
306
307 #[test]
310 fn compute_key_scoped_stable_for_same_tenant() {
311 let msgs = vec![Message::user("hello")];
312 let key1 = ResponseCache::compute_key_scoped("sys", &msgs, &["a"], Some("tenant-a:user1"));
313 let key2 = ResponseCache::compute_key_scoped("sys", &msgs, &["a"], Some("tenant-a:user1"));
314 assert_eq!(key1, key2);
315 }
316}