1use serde_json::Value;
23
24const BUCKETS: u64 = 10_000;
26
27const FIELD_SEP: char = '\u{1}';
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32pub enum Arm {
33 Control,
35 Treatment,
37}
38
39impl Arm {
40 #[must_use]
41 pub fn as_str(self) -> &'static str {
42 match self {
43 Arm::Control => "control",
44 Arm::Treatment => "treatment",
45 }
46 }
47}
48
49#[must_use]
51pub fn bucket(key: &str) -> u64 {
52 let hash = blake3::hash(key.as_bytes());
53 let b = hash.as_bytes();
54 let n = u64::from_le_bytes([b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7]]);
55 n % BUCKETS
56}
57
58#[must_use]
61pub fn assign(key: &str, holdout: f64) -> Arm {
62 let h = holdout.clamp(0.0, 1.0);
63 if h <= 0.0 {
64 return Arm::Treatment;
65 }
66 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
67 let threshold = (h * BUCKETS as f64).round() as u64;
68 if bucket(key) < threshold {
69 Arm::Control
70 } else {
71 Arm::Treatment
72 }
73}
74
75#[must_use]
83pub fn compression_arm(key: &str, holdout: f64) -> Arm {
84 assign(&format!("compression{FIELD_SEP}{key}"), holdout)
85}
86
87#[must_use]
92pub fn conversation_key(doc: &Value) -> String {
93 if doc.get("contents").is_some() {
94 google_key(doc)
95 } else if doc.get("input").is_some() && doc.get("messages").is_none() {
96 openai_responses_key(doc)
97 } else if doc.get("system").is_some() {
98 anthropic_key(doc)
99 } else {
100 openai_chat_key(doc)
101 }
102}
103
104#[must_use]
109pub fn compression_holdout_arm(doc: &Value) -> Option<Arm> {
110 let fraction = lean_ctx::core::config::Config::load()
111 .proxy
112 .compression_holdout_fraction();
113 (fraction > 0.0).then(|| compression_arm(&conversation_key(doc), fraction))
114}
115
116#[cfg(feature = "http-server")]
117thread_local! {
118 static COMPRESSION_CONTROL: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
119}
120
121#[cfg(feature = "http-server")]
126pub(crate) struct CompressionControlScope(bool);
127
128#[cfg(feature = "http-server")]
129pub(crate) fn enter_compression_control(control: bool) -> CompressionControlScope {
130 CompressionControlScope(COMPRESSION_CONTROL.replace(control))
131}
132
133#[cfg(feature = "http-server")]
134impl Drop for CompressionControlScope {
135 fn drop(&mut self) {
136 COMPRESSION_CONTROL.set(self.0);
137 }
138}
139
140#[must_use]
143#[cfg(feature = "http-server")]
144pub(crate) fn in_compression_control() -> bool {
145 COMPRESSION_CONTROL.get()
146}
147
148#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
150pub struct Cohorts {
151 pub output: Option<Arm>,
153 pub compression: Option<Arm>,
155}
156
157fn flatten_text(v: &Value) -> String {
160 match v {
161 Value::String(s) => s.clone(),
162 Value::Array(items) => items.iter().map(flatten_text).collect::<Vec<_>>().join(" "),
163 Value::Object(map) => {
164 if let Some(Value::String(t)) = map.get("text") {
165 t.clone()
166 } else if let Some(inner) = map.get("content") {
167 flatten_text(inner)
168 } else if let Some(parts) = map.get("parts") {
169 flatten_text(parts)
170 } else {
171 String::new()
172 }
173 }
174 _ => String::new(),
175 }
176}
177
178fn first_message_text(messages: Option<&Value>, role: &str, content_field: &str) -> String {
182 messages
183 .and_then(Value::as_array)
184 .and_then(|arr| {
185 arr.iter()
186 .find(|m| m.get("role").and_then(Value::as_str) == Some(role))
187 })
188 .and_then(|m| m.get(content_field))
189 .map(flatten_text)
190 .unwrap_or_default()
191}
192
193#[must_use]
195pub fn anthropic_key(doc: &Value) -> String {
196 let system = doc.get("system").map(flatten_text).unwrap_or_default();
197 let first_user = first_message_text(doc.get("messages"), "user", "content");
198 format!("{system}{FIELD_SEP}{first_user}")
199}
200
201#[must_use]
204pub fn openai_chat_key(doc: &Value) -> String {
205 let messages = doc.get("messages");
206 let mut system = first_message_text(messages, "system", "content");
207 if system.is_empty() {
208 system = first_message_text(messages, "developer", "content");
209 }
210 let first_user = first_message_text(messages, "user", "content");
211 format!("{system}{FIELD_SEP}{first_user}")
212}
213
214#[must_use]
216pub fn openai_responses_key(doc: &Value) -> String {
217 let system = doc
218 .get("instructions")
219 .map(flatten_text)
220 .unwrap_or_default();
221 let first_user = match doc.get("input") {
223 Some(Value::String(s)) => s.clone(),
224 other => first_message_text(other, "user", "content"),
225 };
226 format!("{system}{FIELD_SEP}{first_user}")
227}
228
229#[must_use]
232pub fn google_key(doc: &Value) -> String {
233 let system = doc
234 .get("systemInstruction")
235 .map(flatten_text)
236 .unwrap_or_default();
237 let first_user = first_message_text(doc.get("contents"), "user", "parts");
238 format!("{system}{FIELD_SEP}{first_user}")
239}
240
241pub fn assign_holdout_by_task(task_id: &str, holdout_rate: f64) -> bool {
244 use std::hash::{Hash, Hasher};
245 let mut hasher = std::collections::hash_map::DefaultHasher::new();
246 task_id.hash(&mut hasher);
247 let hash = hasher.finish();
248 let normalized = (hash % 10000) as f64 / 10000.0;
249 normalized < holdout_rate
250}
251#[cfg(test)]
252mod tests {
253 use super::*;
254 use serde_json::json;
255
256 #[test]
257 fn holdout_zero_is_all_treatment() {
258 assert_eq!(assign("any key", 0.0), Arm::Treatment);
259 assert_eq!(assign("any key", -1.0), Arm::Treatment);
260 }
261
262 #[test]
263 fn holdout_one_is_all_control() {
264 assert_eq!(assign("any key", 1.0), Arm::Control);
265 assert_eq!(assign("another", 2.0), Arm::Control);
266 }
267
268 #[test]
269 fn assignment_is_deterministic_and_stable() {
270 let k = "system\u{1}first user message";
272 let a = assign(k, 0.5);
273 for _ in 0..20 {
274 assert_eq!(assign(k, 0.5), a);
275 }
276 }
277
278 #[test]
279 fn fraction_is_approximately_honoured() {
280 let control = (0..5000)
282 .filter(|i| assign(&format!("conv-{i}"), 0.3) == Arm::Control)
283 .count();
284 let frac = control as f64 / 5000.0;
285 assert!((0.27..0.33).contains(&frac), "got {frac}");
286 }
287
288 #[test]
289 fn anthropic_key_uses_system_and_first_user() {
290 let doc = json!({
291 "system": "You are helpful.",
292 "messages": [
293 {"role": "user", "content": "Hello there"},
294 {"role": "assistant", "content": "Hi"},
295 {"role": "user", "content": "later turn"}
296 ]
297 });
298 assert_eq!(anthropic_key(&doc), "You are helpful.\u{1}Hello there");
299 }
300
301 #[test]
302 fn anthropic_key_flattens_block_arrays() {
303 let doc = json!({
304 "system": [{"type": "text", "text": "Sys A"}, {"type": "text", "text": "Sys B"}],
305 "messages": [{"role": "user", "content": [{"type": "text", "text": "U1"}]}]
306 });
307 assert_eq!(anthropic_key(&doc), "Sys A Sys B\u{1}U1");
308 }
309
310 #[test]
311 fn openai_chat_key_prefers_system_then_developer() {
312 let doc = json!({
313 "messages": [
314 {"role": "developer", "content": "Dev rules"},
315 {"role": "user", "content": "Q"}
316 ]
317 });
318 assert_eq!(openai_chat_key(&doc), "Dev rules\u{1}Q");
319 }
320
321 #[test]
322 fn google_key_uses_system_instruction_and_contents() {
323 let doc = json!({
324 "systemInstruction": {"parts": [{"text": "Be terse"}]},
325 "contents": [{"role": "user", "parts": [{"text": "First Q"}]}]
326 });
327 assert_eq!(google_key(&doc), "Be terse\u{1}First Q");
328 }
329
330 #[test]
331 fn responses_key_handles_string_and_array_input() {
332 let s = json!({"instructions": "Sys", "input": "just a string"});
333 assert_eq!(openai_responses_key(&s), "Sys\u{1}just a string");
334 let a = json!({
335 "instructions": "Sys",
336 "input": [{"role": "user", "content": "arr q"}]
337 });
338 assert_eq!(openai_responses_key(&a), "Sys\u{1}arr q");
339 }
340}