1use std::collections::HashMap;
21use std::sync::{Mutex, OnceLock};
22
23use serde_json::Value;
24
25const MAX_TRACKED: usize = 2048;
26
27#[derive(Clone)]
28struct ConversationPrefix {
29 forwarded_bytes: Vec<u8>,
31 original_hashes: Vec<u64>,
32 count: usize,
33 sent: SentPrefix,
34}
35
36#[derive(Clone, Debug, Default, PartialEq, Eq)]
40pub struct SentPrefix {
41 pub client: Vec<u64>,
43 pub forwarded: Vec<u64>,
45}
46
47pub struct AppendDelta {
48 pub prefix_bytes: Vec<u8>,
49 pub delta_start: usize,
50}
51
52fn store() -> &'static Mutex<HashMap<u64, ConversationPrefix>> {
53 static STORE: OnceLock<Mutex<HashMap<u64, ConversationPrefix>>> = OnceLock::new();
54 STORE.get_or_init(|| Mutex::new(HashMap::new()))
55}
56
57pub fn clear() {
59 if let Ok(mut guard) = store().lock() {
60 guard.clear();
61 }
62}
63
64fn message_hash(msg: &Value) -> u64 {
65 use std::hash::{Hash, Hasher};
66 let canonical = serde_json::to_string(msg).unwrap_or_default();
67 let mut hasher = std::collections::hash_map::DefaultHasher::new();
68 canonical.hash(&mut hasher);
69 hasher.finish()
70}
71
72pub fn conversation_id(system: Option<&Value>, messages: &[Value]) -> u64 {
74 use std::hash::{Hash, Hasher};
75 let mut hasher = std::collections::hash_map::DefaultHasher::new();
76 if let Some(sys) = system {
77 serde_json::to_string(sys)
78 .unwrap_or_default()
79 .hash(&mut hasher);
80 }
81 if let Some(first) = messages.first() {
82 serde_json::to_string(first)
83 .unwrap_or_default()
84 .hash(&mut hasher);
85 }
86 hasher.finish()
87}
88
89pub fn request_conversation_id(request: &Value) -> Option<u64> {
91 let messages = request.get("messages")?.as_array()?;
92 Some(conversation_id(request.get("system"), messages))
93}
94
95pub fn detect_append_only(conv_id: u64, messages: &[Value]) -> Option<AppendDelta> {
98 let guard = store().lock().ok()?;
99 let prev = guard.get(&conv_id)?;
100
101 if prev.forwarded_bytes.is_empty() || messages.len() <= prev.count {
102 return None;
103 }
104
105 let current_hashes: Vec<u64> = messages.iter().map(message_hash).collect();
106 for (i, prev_hash) in prev.original_hashes.iter().enumerate() {
107 if current_hashes.get(i) != Some(prev_hash) {
108 return None;
109 }
110 }
111
112 Some(AppendDelta {
113 prefix_bytes: prev.forwarded_bytes.clone(),
114 delta_start: prev.count,
115 })
116}
117
118pub fn sent_prefix(conv_ids: &[u64]) -> Option<SentPrefix> {
125 let guard = store().lock().ok()?;
126 conv_ids
127 .iter()
128 .find_map(|id| guard.get(id))
129 .map(|prev| prev.sent.clone())
130}
131
132pub fn record_forwarded(
135 conv_id: u64,
136 forwarded: Vec<u8>,
137 originals: &[Value],
138 msg_count: usize,
139 sent: SentPrefix,
140) {
141 let hashes: Vec<u64> = originals.iter().take(msg_count).map(message_hash).collect();
142 let entry = ConversationPrefix {
143 forwarded_bytes: forwarded,
144 original_hashes: hashes,
145 count: msg_count,
146 sent,
147 };
148
149 if let Ok(mut guard) = store().lock() {
150 if guard.len() >= MAX_TRACKED
151 && !guard.contains_key(&conv_id)
152 && let Some(&oldest) = guard.keys().next()
153 {
154 guard.remove(&oldest);
155 }
156 guard.insert(conv_id, entry);
157 }
158}
159
160pub fn same_request_frame(prefix_bytes: &[u8], current: &Value) -> bool {
169 let Ok(Value::Object(previous)) = serde_json::from_slice::<Value>(prefix_bytes) else {
170 return false;
171 };
172 let Some(current) = current.as_object() else {
173 return false;
174 };
175 previous.len() == current.len()
176 && previous
177 .iter()
178 .all(|(key, value)| key == "messages" || current.get(key) == Some(value))
179}
180
181pub fn overlay_prefix(prefix_bytes: &[u8], delta_messages: &[Value]) -> Option<Vec<u8>> {
184 if delta_messages.is_empty() {
185 return Some(prefix_bytes.to_vec());
186 }
187
188 let mut request: Value = serde_json::from_slice(prefix_bytes).ok()?;
192 let messages = match &mut request {
193 Value::Object(_) => request.get_mut("messages")?.as_array_mut()?,
194 Value::Array(messages) => messages,
195 _ => return None,
196 };
197 messages.extend(delta_messages.iter().cloned());
198 serde_json::to_vec(&request).ok()
199}
200
201#[cfg(test)]
202mod tests {
203 use super::*;
204 use serde_json::json;
205
206 fn unique_messages(tag: &str) -> Vec<Value> {
207 vec![
208 json!({"role": "user", "content": format!("hello-{tag}")}),
209 json!({"role": "assistant", "content": format!("hi-{tag}")}),
210 ]
211 }
212
213 #[test]
217 fn replay_is_limited_to_the_same_request_frame() {
218 let hello = json!({"role": "user", "content": "hello"});
219 let forwarded =
220 serde_json::to_vec(&json!({"model": "gpt-test", "messages": [hello]})).unwrap();
221 let next = json!({"role": "user", "content": "continue"});
222 assert!(same_request_frame(
223 &forwarded,
224 &json!({"model": "gpt-test", "messages": [hello, next]})
225 ));
226 for other_frame in [
227 json!({"model": "claude-test", "max_tokens": 16, "messages": [hello, next]}),
228 json!({"model": "gpt-test", "max_tokens": 16, "messages": [hello, next]}),
229 json!({"model": "claude-test", "messages": [hello, next]}),
230 ] {
231 assert!(
232 !same_request_frame(&forwarded, &other_frame),
233 "{other_frame}"
234 );
235 }
236 }
237
238 #[test]
239 fn append_only_detection_works() {
240 let msgs = unique_messages("append");
241 let conv = conversation_id(None, &msgs);
242 let forwarded = serde_json::to_vec(&msgs).unwrap();
243
244 record_forwarded(
245 conv,
246 forwarded.clone(),
247 &msgs,
248 msgs.len(),
249 SentPrefix::default(),
250 );
251
252 let mut extended = msgs.clone();
253 extended.push(json!({"role": "user", "content": "what's up?"}));
254
255 let delta = detect_append_only(conv, &extended).expect("should detect append-only");
256 assert_eq!(delta.delta_start, 2);
257 assert_eq!(delta.prefix_bytes, forwarded);
258 }
259
260 #[test]
261 fn detection_fails_on_modified_prefix() {
262 let msgs = unique_messages("modified");
263 let conv = conversation_id(None, &msgs);
264 let forwarded = serde_json::to_vec(&msgs).unwrap();
265 record_forwarded(conv, forwarded, &msgs, msgs.len(), SentPrefix::default());
266
267 let mut modified = msgs;
268 modified[0] = json!({"role": "user", "content": "different"});
269 modified.push(json!({"role": "user", "content": "extra"}));
270 assert!(detect_append_only(conv, &modified).is_none());
271 }
272
273 #[test]
274 fn prefix_replay_is_byte_identical_across_turns() {
275 let msgs = unique_messages("replay");
276 let conv = conversation_id(None, &msgs);
277 let forwarded = serde_json::to_vec(&msgs).unwrap();
278 record_forwarded(
279 conv,
280 forwarded.clone(),
281 &msgs,
282 msgs.len(),
283 SentPrefix::default(),
284 );
285
286 let mut turn2 = msgs.clone();
287 turn2.push(json!({"role": "user", "content": "next"}));
288
289 let delta = detect_append_only(conv, &turn2).unwrap();
290 let result = overlay_prefix(&delta.prefix_bytes, &turn2[delta.delta_start..]).unwrap();
291 let result_str = String::from_utf8(result).unwrap();
292 let parsed: Vec<Value> = serde_json::from_str(&result_str).unwrap();
293 assert_eq!(parsed.len(), 3);
294
295 let prefix_portion = &result_str[..forwarded.len() - 1];
296 let original_prefix = std::str::from_utf8(&forwarded[..forwarded.len() - 1]).unwrap();
297 assert_eq!(
298 prefix_portion, original_prefix,
299 "prefix bytes must be identical"
300 );
301 }
302
303 #[test]
304 fn overlay_with_empty_delta_returns_prefix() {
305 let msgs = unique_messages("overlay");
306 let bytes = serde_json::to_vec(&msgs).unwrap();
307 let result = overlay_prefix(&bytes, &[]).unwrap();
308 assert_eq!(result, bytes);
309 }
310
311 #[test]
312 fn overlay_preserves_complete_request_object() {
313 let request = json!({"model":"m","messages":unique_messages("object")});
314 let prefix = serde_json::to_vec(&request).unwrap();
315 let out = overlay_prefix(&prefix, &[json!({"role":"user","content":"new"})]).unwrap();
316 let decoded: Value = serde_json::from_slice(&out).unwrap();
317 assert_eq!(decoded["model"], "m");
318 assert_eq!(decoded["messages"].as_array().unwrap().len(), 3);
319 }
320
321 #[test]
322 fn conversation_id_is_deterministic() {
323 let sys = json!("You are helpful");
324 let msgs = unique_messages("deterministic");
325 assert_eq!(
326 conversation_id(Some(&sys), &msgs),
327 conversation_id(Some(&sys), &msgs)
328 );
329 }
330
331 #[test]
332 fn max_tracked_evicts_oldest() {
333 for i in 0..MAX_TRACKED + 10 {
334 let msgs = vec![json!({"role": "user", "content": format!("msg {i}")})];
335 let conv = conversation_id(None, &msgs);
336 record_forwarded(
337 conv,
338 serde_json::to_vec(&msgs).unwrap(),
339 &msgs,
340 1,
341 SentPrefix::default(),
342 );
343 }
344 assert!(store().lock().unwrap().len() <= MAX_TRACKED);
345 }
346}