1use 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
13pub const CHAT_KEY_PREFIX: &str = "chat";
15pub const EMBEDDING_KEY_PREFIX: &str = "embed";
17pub const COMPLETION_KEY_PREFIX: &str = "completion";
19
20pub fn generate_chat_key(request: &ChatCompletionRequest) -> CacheKey {
25 generate_chat_key_with_user(request, None)
26}
27
28pub 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
75pub fn generate_embedding_key(request: &EmbeddingRequest) -> CacheKey {
77 generate_embedding_key_with_user(request, None, None)
78}
79
80pub 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
107pub 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
121pub 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
131pub 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#[cfg(test)]
149fn normalize_json_string(json: &str) -> String {
150 super::key_policy::canonical_json_str(json)
151}
152
153#[cfg(test)]
155fn is_non_deterministic_field(field: &str) -> bool {
156 super::key_policy::is_non_deterministic_field(field)
157}
158
159pub struct CacheKeyBuilder {
161 parts: Vec<String>,
162 prefix: String,
163}
164
165impl CacheKeyBuilder {
166 pub fn new(prefix: impl Into<String>) -> Self {
168 Self {
169 parts: Vec::new(),
170 prefix: prefix.into(),
171 }
172 }
173
174 pub fn with_part(mut self, part: impl Into<String>) -> Self {
176 self.parts.push(part.into());
177 self
178 }
179
180 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 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 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 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 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 #[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(¶llel));
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 #[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 #[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 #[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 #[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}