Skip to main content

litellm_rs/core/cache/
key_generator.rs

1//! Cache key generation utilities
2//!
3//! This module provides functions for generating consistent cache keys
4//! from LLM requests, including normalization and hashing.
5
6use super::key_policy::{CACHE_KEY_SCHEMA_VERSION, stable_digest_value, versioned_key};
7use super::types::CacheKey;
8use crate::core::models::openai::{ChatCompletionRequest, EmbeddingRequest};
9use serde::Serialize;
10use serde_json::{Value, json};
11use tracing::warn;
12
13/// Prefix for chat completion cache keys
14pub const CHAT_KEY_PREFIX: &str = "chat";
15/// Prefix for embedding cache keys
16pub const EMBEDDING_KEY_PREFIX: &str = "embed";
17/// Prefix for completion cache keys
18pub const COMPLETION_KEY_PREFIX: &str = "completion";
19
20/// Generate a cache key for a chat completion request
21///
22/// The key is generated by hashing the normalized request parameters
23/// that affect the response (model, messages, temperature, etc.)
24pub fn generate_chat_key(request: &ChatCompletionRequest) -> CacheKey {
25    generate_chat_key_with_user(request, None)
26}
27
28/// Generate a cache key for a chat completion request with optional user ID
29pub fn generate_chat_key_with_user(
30    request: &ChatCompletionRequest,
31    user_id: Option<&str>,
32) -> CacheKey {
33    let payload = json!({
34        "kind": "chat_completion",
35        "schema_version": CACHE_KEY_SCHEMA_VERSION,
36        "model": &request.model,
37        "messages": &request.messages,
38        "temperature": request.temperature,
39        "max_tokens": request.max_tokens,
40        "max_completion_tokens": request.max_completion_tokens,
41        "top_p": request.top_p,
42        "n": request.n,
43        "stop": &request.stop,
44        "presence_penalty": request.presence_penalty,
45        "frequency_penalty": request.frequency_penalty,
46        "logit_bias": &request.logit_bias,
47        "functions": &request.functions,
48        "function_call": &request.function_call,
49        "tools": &request.tools,
50        "tool_choice": &request.tool_choice,
51        "parallel_tool_calls": request.parallel_tool_calls,
52        "response_format": &request.response_format,
53        "seed": request.seed,
54        "logprobs": request.logprobs,
55        "top_logprobs": request.top_logprobs,
56        "modalities": &request.modalities,
57        "audio": &request.audio,
58        "reasoning_effort": &request.reasoning_effort,
59        "service_tier": &request.service_tier,
60        "prediction": &request.prediction,
61        "safety_settings": &request.safety_settings,
62        "cache_control": &request.cache_control,
63        "extra_body": &request.extra_body,
64        "user_id": user_id,
65    });
66
67    let digest = stable_digest_value(&payload);
68    CacheKey::new(versioned_key(
69        CHAT_KEY_PREFIX,
70        Some(&request.model),
71        &digest,
72    ))
73}
74
75/// Generate a cache key for an embedding request
76pub fn generate_embedding_key(request: &EmbeddingRequest) -> CacheKey {
77    generate_embedding_key_with_user(request, None, None)
78}
79
80/// Generate a cache key for an embedding request with optional user ID
81pub fn generate_embedding_key_with_user(
82    request: &EmbeddingRequest,
83    user_id: Option<&str>,
84    deployment_id: Option<&str>,
85) -> CacheKey {
86    let payload = json!({
87        "kind": "embedding",
88        "deployment_id": deployment_id,
89        "schema_version": CACHE_KEY_SCHEMA_VERSION,
90        "model": &request.model,
91        "input": &request.input,
92        "encoding_format": &request.encoding_format,
93        "dimensions": request.dimensions,
94        "input_type": &request.input_type,
95        "truncation": request.truncation,
96        "user_id": user_id,
97    });
98
99    let digest = stable_digest_value(&payload);
100    CacheKey::new(versioned_key(
101        EMBEDDING_KEY_PREFIX,
102        Some(&request.model),
103        &digest,
104    ))
105}
106
107/// Generate a cache key from arbitrary serializable request
108pub fn generate_key_from_json<T: Serialize>(prefix: &str, request: &T) -> CacheKey {
109    let value = serde_json::to_value(request).unwrap_or_else(|error| {
110        warn!(
111            "Failed to serialize request for cache key generation: {}",
112            error
113        );
114        Value::Null
115    });
116    let digest = stable_digest_value(&value);
117
118    CacheKey::new(versioned_key(prefix, None, &digest))
119}
120
121/// Generate a cache key from raw string content
122pub fn generate_key_from_content(prefix: &str, content: &str) -> CacheKey {
123    let digest = stable_digest_value(&json!({
124        "kind": "content",
125        "content": content,
126    }));
127
128    CacheKey::new(versioned_key(prefix, None, &digest))
129}
130
131/// Generate a cache key from multiple parts
132pub fn generate_key_from_parts(prefix: &str, parts: &[&str]) -> CacheKey {
133    let digest = stable_digest_value(&json!({
134        "kind": "parts",
135        "parts": parts,
136    }));
137
138    CacheKey::new(versioned_key(prefix, None, &digest))
139}
140
141// ==================== Helper Functions ====================
142
143/// Normalize a JSON string for consistent hashing
144///
145/// - Sorts object keys
146/// - Removes whitespace
147/// - Removes timestamps and request IDs
148#[cfg(test)]
149fn normalize_json_string(json: &str) -> String {
150    super::key_policy::canonical_json_str(json)
151}
152
153/// Check if a field should be excluded from cache key generation
154#[cfg(test)]
155fn is_non_deterministic_field(field: &str) -> bool {
156    super::key_policy::is_non_deterministic_field(field)
157}
158
159/// Builder for constructing cache keys
160pub struct CacheKeyBuilder {
161    parts: Vec<String>,
162    prefix: String,
163}
164
165impl CacheKeyBuilder {
166    /// Create a new cache key builder with a prefix
167    pub fn new(prefix: impl Into<String>) -> Self {
168        Self {
169            parts: Vec::new(),
170            prefix: prefix.into(),
171        }
172    }
173
174    /// Add a string part to the key
175    pub fn with_part(mut self, part: impl Into<String>) -> Self {
176        self.parts.push(part.into());
177        self
178    }
179
180    /// Add an optional string part to the key
181    pub fn add_optional(mut self, part: Option<impl Into<String>>) -> Self {
182        if let Some(p) = part {
183            self.parts.push(p.into());
184        }
185        self
186    }
187
188    /// Add a numeric part to the key
189    pub fn add_num<N: std::fmt::Display>(mut self, num: N) -> Self {
190        self.parts.push(num.to_string());
191        self
192    }
193
194    /// Build the cache key
195    pub fn build(self) -> CacheKey {
196        let Self { parts, prefix } = self;
197        let digest = stable_digest_value(&json!({
198            "kind": "builder",
199            "parts": parts,
200        }));
201
202        CacheKey::new(versioned_key(&prefix, None, &digest))
203    }
204
205    /// Build the cache key with explicit parts (no hashing)
206    pub fn build_explicit(self) -> CacheKey {
207        let key = std::iter::once(self.prefix)
208            .chain(self.parts)
209            .collect::<Vec<_>>()
210            .join(":");
211
212        CacheKey::new(key)
213    }
214}
215
216#[cfg(test)]
217mod tests {
218    use super::*;
219    use crate::core::models::openai::messages::{ChatMessage, MessageContent, MessageRole};
220    use crate::core::models::openai::{
221        Function, ResponseFormat, Tool, ToolChoice, ToolChoiceFunction, ToolChoiceFunctionSpec,
222    };
223
224    // Helper to create a test chat message
225    fn create_user_message(content: &str) -> ChatMessage {
226        ChatMessage {
227            role: MessageRole::User,
228            content: Some(MessageContent::Text(content.to_string())),
229            name: None,
230            function_call: None,
231            tool_calls: None,
232            tool_call_id: None,
233            audio: None,
234        }
235    }
236
237    // ==================== Chat Key Generation Tests ====================
238
239    #[test]
240    fn test_generate_chat_key_basic() {
241        let request = ChatCompletionRequest {
242            model: "gpt-4".to_string(),
243            messages: vec![create_user_message("Hello")],
244            ..Default::default()
245        };
246
247        let key = generate_chat_key(&request);
248        assert!(key.as_str().starts_with("chat:gpt-4:v4:"));
249    }
250
251    #[test]
252    fn test_generate_chat_key_consistency() {
253        let request = ChatCompletionRequest {
254            model: "gpt-4".to_string(),
255            messages: vec![create_user_message("Hello")],
256            temperature: Some(0.7),
257            ..Default::default()
258        };
259
260        let key1 = generate_chat_key(&request);
261        let key2 = generate_chat_key(&request);
262
263        assert_eq!(key1, key2);
264    }
265
266    #[test]
267    fn test_generate_chat_key_different_messages() {
268        let request1 = ChatCompletionRequest {
269            model: "gpt-4".to_string(),
270            messages: vec![create_user_message("Hello")],
271            ..Default::default()
272        };
273
274        let request2 = ChatCompletionRequest {
275            model: "gpt-4".to_string(),
276            messages: vec![create_user_message("Goodbye")],
277            ..Default::default()
278        };
279
280        let key1 = generate_chat_key(&request1);
281        let key2 = generate_chat_key(&request2);
282
283        assert_ne!(key1, key2);
284    }
285
286    #[test]
287    fn test_generate_chat_key_different_models() {
288        let request1 = ChatCompletionRequest {
289            model: "gpt-4".to_string(),
290            messages: vec![],
291            ..Default::default()
292        };
293
294        let request2 = ChatCompletionRequest {
295            model: "gpt-3.5-turbo".to_string(),
296            messages: vec![],
297            ..Default::default()
298        };
299
300        let key1 = generate_chat_key(&request1);
301        let key2 = generate_chat_key(&request2);
302
303        assert_ne!(key1, key2);
304    }
305
306    #[test]
307    fn test_generate_chat_key_with_user() {
308        let request = ChatCompletionRequest {
309            model: "gpt-4".to_string(),
310            messages: vec![],
311            ..Default::default()
312        };
313
314        let key1 = generate_chat_key_with_user(&request, Some("user-123"));
315        let key2 = generate_chat_key_with_user(&request, Some("user-456"));
316        let key3 = generate_chat_key_with_user(&request, None);
317
318        assert_ne!(key1, key2);
319        assert_ne!(key1, key3);
320    }
321
322    #[test]
323    fn test_generate_chat_key_with_parameters() {
324        let request1 = ChatCompletionRequest {
325            model: "gpt-4".to_string(),
326            messages: vec![],
327            temperature: Some(0.7),
328            ..Default::default()
329        };
330
331        let request2 = ChatCompletionRequest {
332            model: "gpt-4".to_string(),
333            messages: vec![],
334            temperature: Some(0.9),
335            ..Default::default()
336        };
337
338        let key1 = generate_chat_key(&request1);
339        let key2 = generate_chat_key(&request2);
340
341        assert_ne!(key1, key2);
342    }
343
344    #[test]
345    fn test_generate_chat_key_includes_output_affecting_extras() {
346        let base = ChatCompletionRequest {
347            model: "gpt-4".to_string(),
348            messages: vec![create_user_message("hi")],
349            ..Default::default()
350        };
351
352        let mut parallel = base.clone();
353        parallel.parallel_tool_calls = Some(false);
354        assert_ne!(generate_chat_key(&base), generate_chat_key(&parallel));
355
356        let mut prediction = base.clone();
357        prediction.prediction = Some(serde_json::json!({"type":"content","content":"42"}));
358        assert_ne!(generate_chat_key(&base), generate_chat_key(&prediction));
359
360        let mut safety = base.clone();
361        safety.safety_settings =
362            Some(serde_json::json!([{"category":"HARM","threshold":"BLOCK_NONE"}]));
363        assert_ne!(generate_chat_key(&base), generate_chat_key(&safety));
364
365        let mut cache_control = base.clone();
366        cache_control.cache_control = Some(serde_json::json!({"type":"ephemeral"}));
367        assert_ne!(generate_chat_key(&base), generate_chat_key(&cache_control));
368
369        let mut extra = base.clone();
370        extra.extra_body.insert(
371            "provider_specific".to_string(),
372            serde_json::json!({"k":"v"}),
373        );
374        assert_ne!(generate_chat_key(&base), generate_chat_key(&extra));
375    }
376
377    #[test]
378    fn test_generate_chat_key_filters_volatile_extra_body_fields() {
379        let base = ChatCompletionRequest {
380            model: "gpt-4".to_string(),
381            messages: vec![create_user_message("hi")],
382            ..Default::default()
383        };
384
385        let mut with_request_id = base.clone();
386        with_request_id
387            .extra_body
388            .insert("request_id".to_string(), serde_json::json!("req_123"));
389
390        assert_eq!(
391            generate_chat_key(&base),
392            generate_chat_key(&with_request_id)
393        );
394    }
395
396    #[test]
397    fn test_generate_chat_key_includes_response_format_schema() {
398        let base = ChatCompletionRequest {
399            model: "gpt-4".to_string(),
400            messages: vec![create_user_message("Return JSON")],
401            response_format: Some(ResponseFormat {
402                format_type: "json_schema".to_string(),
403                json_schema: Some(serde_json::json!({
404                    "name": "answer",
405                    "schema": {
406                        "type": "object",
407                        "properties": {
408                            "answer": { "type": "string" }
409                        }
410                    }
411                })),
412                response_type: None,
413            }),
414            ..Default::default()
415        };
416
417        let mut changed_schema = base.clone();
418        changed_schema.response_format = Some(ResponseFormat {
419            format_type: "json_schema".to_string(),
420            json_schema: Some(serde_json::json!({
421                "schema": {
422                    "properties": {
423                        "answer": { "type": "number" }
424                    },
425                    "type": "object"
426                },
427                "name": "answer"
428            })),
429            response_type: None,
430        });
431
432        assert_ne!(generate_chat_key(&base), generate_chat_key(&changed_schema));
433    }
434
435    #[test]
436    fn test_generate_chat_key_preserves_response_format_schema_id_identity() {
437        let base = ChatCompletionRequest {
438            model: "gpt-4".to_string(),
439            messages: vec![create_user_message("Return JSON")],
440            response_format: Some(ResponseFormat {
441                format_type: "json_schema".to_string(),
442                json_schema: Some(serde_json::json!({
443                    "name": "answer",
444                    "schema": {
445                        "type": "object",
446                        "properties": {
447                            "id": { "type": "string" }
448                        }
449                    }
450                })),
451                response_type: None,
452            }),
453            ..Default::default()
454        };
455
456        let mut changed_id_schema = base.clone();
457        changed_id_schema.response_format = Some(ResponseFormat {
458            format_type: "json_schema".to_string(),
459            json_schema: Some(serde_json::json!({
460                "name": "answer",
461                "schema": {
462                    "type": "object",
463                    "properties": {
464                        "id": { "type": "integer" }
465                    }
466                }
467            })),
468            response_type: None,
469        });
470
471        assert_ne!(
472            generate_chat_key(&base),
473            generate_chat_key(&changed_id_schema)
474        );
475    }
476
477    #[test]
478    fn test_generate_chat_key_preserves_response_format_timestamp_identity() {
479        let base = ChatCompletionRequest {
480            model: "gpt-4".to_string(),
481            messages: vec![create_user_message("Return JSON")],
482            response_format: Some(ResponseFormat {
483                format_type: "json_schema".to_string(),
484                json_schema: Some(serde_json::json!({
485                    "name": "answer",
486                    "schema": {
487                        "type": "object",
488                        "properties": {
489                            "timestamp": { "type": "string" }
490                        }
491                    }
492                })),
493                response_type: None,
494            }),
495            ..Default::default()
496        };
497
498        let mut changed_timestamp_schema = base.clone();
499        changed_timestamp_schema.response_format = Some(ResponseFormat {
500            format_type: "json_schema".to_string(),
501            json_schema: Some(serde_json::json!({
502                "name": "answer",
503                "schema": {
504                    "type": "object",
505                    "properties": {
506                        "timestamp": { "type": "integer" }
507                    }
508                }
509            })),
510            response_type: None,
511        });
512
513        assert_ne!(
514            generate_chat_key(&base),
515            generate_chat_key(&changed_timestamp_schema)
516        );
517    }
518
519    #[test]
520    fn test_generate_chat_key_includes_tool_parameters_and_choice() {
521        let base = ChatCompletionRequest {
522            model: "gpt-4".to_string(),
523            messages: vec![create_user_message("Use the tool")],
524            tools: Some(vec![Tool {
525                tool_type: "function".to_string(),
526                function: Function {
527                    name: "search".to_string(),
528                    description: Some("Search".to_string()),
529                    parameters: Some(serde_json::json!({
530                        "type": "object",
531                        "properties": {
532                            "query": { "type": "string" }
533                        }
534                    })),
535                },
536            }]),
537            tool_choice: Some(ToolChoice::Auto("auto".to_string())),
538            ..Default::default()
539        };
540
541        let mut changed_parameters = base.clone();
542        changed_parameters.tools.as_mut().unwrap()[0]
543            .function
544            .parameters = Some(serde_json::json!({
545            "type": "object",
546            "properties": {
547                "query": { "type": "number" }
548            }
549        }));
550
551        let mut changed_choice = base.clone();
552        changed_choice.tool_choice = Some(ToolChoice::Specific(ToolChoiceFunction {
553            tool_type: "function".to_string(),
554            function: ToolChoiceFunctionSpec {
555                name: "search".to_string(),
556            },
557        }));
558
559        assert_ne!(
560            generate_chat_key(&base),
561            generate_chat_key(&changed_parameters)
562        );
563        assert_ne!(generate_chat_key(&base), generate_chat_key(&changed_choice));
564    }
565
566    #[test]
567    fn test_generate_chat_key_preserves_tool_parameter_id_identity() {
568        let base = ChatCompletionRequest {
569            model: "gpt-4".to_string(),
570            messages: vec![create_user_message("Use the tool")],
571            tools: Some(vec![Tool {
572                tool_type: "function".to_string(),
573                function: Function {
574                    name: "lookup".to_string(),
575                    description: Some("Lookup by ID".to_string()),
576                    parameters: Some(serde_json::json!({
577                        "type": "object",
578                        "properties": {
579                            "id": { "type": "string" }
580                        }
581                    })),
582                },
583            }]),
584            ..Default::default()
585        };
586
587        let mut changed_id_schema = base.clone();
588        changed_id_schema.tools.as_mut().unwrap()[0]
589            .function
590            .parameters = Some(serde_json::json!({
591            "type": "object",
592            "properties": {
593                "id": { "type": "integer" }
594            }
595        }));
596
597        assert_ne!(
598            generate_chat_key(&base),
599            generate_chat_key(&changed_id_schema)
600        );
601    }
602
603    // ==================== Embedding Key Generation Tests ====================
604
605    #[test]
606    fn test_generate_embedding_key_string() {
607        let request = EmbeddingRequest {
608            model: "text-embedding-ada-002".to_string(),
609            input: serde_json::json!("Hello world"),
610            user: None,
611            encoding_format: None,
612            dimensions: None,
613            input_type: None,
614            truncation: None,
615        };
616
617        let key = generate_embedding_key(&request);
618        assert!(key.as_str().starts_with("embed:text-embedding-ada-002:v4:"));
619    }
620
621    #[test]
622    fn test_generate_embedding_key_array() {
623        let request = EmbeddingRequest {
624            model: "text-embedding-ada-002".to_string(),
625            input: serde_json::json!(["Hello", "World"]),
626            user: None,
627            encoding_format: None,
628            dimensions: None,
629            input_type: None,
630            truncation: None,
631        };
632
633        let key = generate_embedding_key(&request);
634        assert!(key.as_str().starts_with("embed:text-embedding-ada-002:"));
635    }
636
637    #[test]
638    fn test_generate_embedding_key_consistency() {
639        let request = EmbeddingRequest {
640            model: "text-embedding-3-small".to_string(),
641            input: serde_json::json!("Test input"),
642            user: None,
643            encoding_format: None,
644            dimensions: None,
645            input_type: None,
646            truncation: None,
647        };
648
649        let key1 = generate_embedding_key(&request);
650        let key2 = generate_embedding_key(&request);
651        let mut different_options = request.clone();
652        different_options.truncation = Some(false);
653
654        assert_eq!(key1, key2);
655        assert_ne!(key1, generate_embedding_key(&different_options));
656    }
657
658    #[test]
659    fn test_generate_embedding_key_array_order_is_part_of_identity() {
660        let request1 = EmbeddingRequest {
661            model: "text-embedding-ada-002".to_string(),
662            input: serde_json::json!(["Alpha", "Beta"]),
663            user: None,
664            encoding_format: None,
665            dimensions: None,
666            input_type: None,
667            truncation: None,
668        };
669
670        let request2 = EmbeddingRequest {
671            model: "text-embedding-ada-002".to_string(),
672            input: serde_json::json!(["Beta", "Alpha"]),
673            user: None,
674            encoding_format: None,
675            dimensions: None,
676            input_type: None,
677            truncation: None,
678        };
679
680        let key1 = generate_embedding_key(&request1);
681        let key2 = generate_embedding_key(&request2);
682
683        assert_ne!(key1, key2);
684    }
685
686    // ==================== Generic Key Generation Tests ====================
687
688    #[test]
689    fn test_generate_key_from_content() {
690        let key = generate_key_from_content("test", "some content");
691        assert!(key.as_str().starts_with("test:"));
692    }
693
694    #[test]
695    fn test_generate_key_from_parts() {
696        let key = generate_key_from_parts("prefix", &["part1", "part2", "part3"]);
697        assert!(key.as_str().starts_with("prefix:"));
698    }
699
700    #[test]
701    fn test_generate_key_from_json() {
702        #[derive(Serialize)]
703        struct TestRequest {
704            field1: String,
705            field2: i32,
706        }
707
708        let request = TestRequest {
709            field1: "value".to_string(),
710            field2: 42,
711        };
712
713        let key = generate_key_from_json("test", &request);
714        assert!(key.as_str().starts_with("test:"));
715    }
716
717    // ==================== CacheKeyBuilder Tests ====================
718
719    #[test]
720    fn test_cache_key_builder_basic() {
721        let key = CacheKeyBuilder::new("chat")
722            .with_part("gpt-4")
723            .with_part("user-123")
724            .build();
725
726        assert!(key.as_str().starts_with("chat:"));
727    }
728
729    #[test]
730    fn test_cache_key_builder_with_nums() {
731        let key = CacheKeyBuilder::new("session")
732            .with_part("user")
733            .add_num(123)
734            .add_num(456)
735            .build();
736
737        assert!(key.as_str().starts_with("session:"));
738    }
739
740    #[test]
741    fn test_cache_key_builder_with_optional() {
742        let key1 = CacheKeyBuilder::new("test")
743            .with_part("base")
744            .add_optional(Some("optional"))
745            .build();
746
747        let key2 = CacheKeyBuilder::new("test")
748            .with_part("base")
749            .add_optional(None::<String>)
750            .build();
751
752        assert_ne!(key1, key2);
753    }
754
755    #[test]
756    fn test_cache_key_builder_explicit() {
757        let key = CacheKeyBuilder::new("chat")
758            .with_part("gpt-4")
759            .with_part("conversation-1")
760            .build_explicit();
761
762        assert_eq!(key.as_str(), "chat:gpt-4:conversation-1");
763    }
764
765    // ==================== JSON Normalization Tests ====================
766
767    #[test]
768    fn test_normalize_json_filters_timestamp() {
769        let json1 = r#"{"message": "hello", "timestamp": "2024-01-01"}"#;
770        let json2 = r#"{"message": "hello", "timestamp": "2024-12-31"}"#;
771
772        let norm1 = normalize_json_string(json1);
773        let norm2 = normalize_json_string(json2);
774
775        assert_eq!(norm1, norm2);
776    }
777
778    #[test]
779    fn test_normalize_json_sorts_keys() {
780        let json1 = r#"{"b": 2, "a": 1}"#;
781        let json2 = r#"{"a": 1, "b": 2}"#;
782
783        let norm1 = normalize_json_string(json1);
784        let norm2 = normalize_json_string(json2);
785
786        assert_eq!(norm1, norm2);
787    }
788
789    #[test]
790    fn test_is_non_deterministic_field() {
791        assert!(is_non_deterministic_field("timestamp"));
792        assert!(is_non_deterministic_field("request_id"));
793        assert!(is_non_deterministic_field("stream"));
794        assert!(!is_non_deterministic_field("model"));
795        assert!(!is_non_deterministic_field("messages"));
796    }
797}