1use sha2::{Digest, Sha256};
4use tokenmiser_providers::ChatRequest;
5
6fn temperature_bucket(t: Option<f32>) -> u8 {
9 let t = t.unwrap_or(1.0).clamp(0.0, 2.0);
10 (t * 10.0).round() as u8
11}
12
13pub fn exact_key(req: &ChatRequest, tenant: &str) -> String {
14 let mut hasher = Sha256::new();
15 hasher.update(req.model.as_bytes());
16 hasher.update(b"\x00");
17
18 for m in &req.messages {
19 hasher.update(m.role.as_bytes());
20 hasher.update(b"\x00");
21 if let Ok(s) = serde_json::to_string(&m.content) {
22 hasher.update(s.as_bytes());
23 }
24 hasher.update(b"\x01");
25 }
26 hasher.update(b"\x00");
27
28 if let Some(t) = req.extra.get("tools") {
29 if let Ok(s) = serde_json::to_string(t) {
30 hasher.update(s.as_bytes());
31 }
32 }
33 hasher.update(b"\x00");
34
35 hasher.update([temperature_bucket(req.temperature)]);
36 hasher.update(b"\x00");
37
38 if let Some(mt) = req.max_tokens {
42 hasher.update(mt.to_le_bytes());
43 }
44 hasher.update(b"\x00");
45 if let Some(tp) = req.top_p {
46 hasher.update(tp.to_le_bytes());
47 }
48 hasher.update(b"\x00");
49
50 const KEY_IGNORED_EXTRA: &[&str] = &[
55 "tools", "stream_options", "user", "metadata", "store", "logprobs", "top_logprobs",
62 ];
63 for (k, v) in req.extra.iter() {
64 if KEY_IGNORED_EXTRA.contains(&k.as_str()) {
65 continue;
66 }
67 hasher.update(k.as_bytes());
68 hasher.update(b"\x02");
69 if let Ok(s) = serde_json::to_string(v) {
70 hasher.update(s.as_bytes());
71 }
72 hasher.update(b"\x01");
73 }
74 hasher.update(b"\x00");
75
76 hasher.update(tenant.as_bytes());
77
78 hex::encode(hasher.finalize())
79}
80
81#[cfg(test)]
82mod tests {
83 use super::*;
84 use tokenmiser_providers::ChatMessage;
85
86 fn req(model: &str, content: &str, temp: Option<f32>) -> ChatRequest {
87 ChatRequest {
88 model: model.into(),
89 messages: vec![ChatMessage {
90 role: "user".into(),
91 content: serde_json::Value::String(content.into()),
92 extra: Default::default(),
93 }],
94 temperature: temp,
95 max_tokens: None,
96 top_p: None,
97 stream: None,
98 extra: Default::default(),
99 }
100 }
101
102 #[test]
103 fn same_input_same_key() {
104 let a = exact_key(&req("gpt-5", "hi", Some(0.0)), "tenant-1");
105 let b = exact_key(&req("gpt-5", "hi", Some(0.0)), "tenant-1");
106 assert_eq!(a, b);
107 }
108
109 #[test]
110 fn different_tenant_different_key() {
111 let a = exact_key(&req("gpt-5", "hi", Some(0.0)), "tenant-1");
112 let b = exact_key(&req("gpt-5", "hi", Some(0.0)), "tenant-2");
113 assert_ne!(a, b);
114 }
115
116 #[test]
117 fn nearby_temperatures_bucket_together() {
118 let a = exact_key(&req("gpt-5", "hi", Some(0.00)), "t");
119 let b = exact_key(&req("gpt-5", "hi", Some(0.04)), "t");
120 assert_eq!(a, b);
121 }
122
123 #[test]
124 fn distant_temperatures_split() {
125 let a = exact_key(&req("gpt-5", "hi", Some(0.0)), "t");
126 let b = exact_key(&req("gpt-5", "hi", Some(0.7)), "t");
127 assert_ne!(a, b);
128 }
129
130 #[test]
131 fn max_tokens_and_top_p_split_the_key() {
132 let mut a = req("gpt-5", "hi", Some(0.0));
133 let mut b = req("gpt-5", "hi", Some(0.0));
134 a.max_tokens = Some(5);
135 b.max_tokens = Some(4096);
136 assert_ne!(exact_key(&a, "t"), exact_key(&b, "t"));
137
138 let mut c = req("gpt-5", "hi", Some(0.0));
139 let mut d = req("gpt-5", "hi", Some(0.0));
140 c.top_p = Some(0.1);
141 d.top_p = Some(1.0);
142 assert_ne!(exact_key(&c, "t"), exact_key(&d, "t"));
143 }
144
145 #[test]
146 fn answer_shaping_extra_params_split_the_key() {
147 let mut a = req("gpt-5", "hi", Some(0.0));
148 let b = req("gpt-5", "hi", Some(0.0));
149 a.extra.insert(
150 "response_format".into(),
151 serde_json::json!({"type": "json_object"}),
152 );
153 assert_ne!(exact_key(&a, "t"), exact_key(&b, "t"));
154
155 for (k, v) in [
156 ("n", serde_json::json!(2)),
157 ("stop", serde_json::json!(["\n"])),
158 ("seed", serde_json::json!(7)),
159 ] {
160 let mut with = req("gpt-5", "hi", Some(0.0));
161 with.extra.insert(k.into(), v);
162 assert_ne!(
163 exact_key(&with, "t"),
164 exact_key(&req("gpt-5", "hi", Some(0.0)), "t"),
165 "`{k}` must participate in the cache key"
166 );
167 }
168 }
169
170 #[test]
171 fn stream_and_transport_noise_do_not_split_the_key() {
172 let plain = req("gpt-5", "hi", Some(0.0));
173 let mut streaming = req("gpt-5", "hi", Some(0.0));
174 streaming.stream = Some(true);
175 streaming.extra.insert(
176 "stream_options".into(),
177 serde_json::json!({"include_usage": true}),
178 );
179 streaming
180 .extra
181 .insert("user".into(), serde_json::json!("abc"));
182 assert_eq!(exact_key(&plain, "t"), exact_key(&streaming, "t"));
183 }
184}