lean_ctx/proxy/
holdout.rs1use serde_json::Value;
18
19const BUCKETS: u64 = 10_000;
21
22const FIELD_SEP: char = '\u{1}';
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub enum Arm {
28 Control,
30 Treatment,
32}
33
34impl Arm {
35 #[must_use]
36 pub fn as_str(self) -> &'static str {
37 match self {
38 Arm::Control => "control",
39 Arm::Treatment => "treatment",
40 }
41 }
42}
43
44#[must_use]
46pub fn bucket(key: &str) -> u64 {
47 let hash = blake3::hash(key.as_bytes());
48 let b = hash.as_bytes();
49 let n = u64::from_le_bytes([b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7]]);
50 n % BUCKETS
51}
52
53#[must_use]
56pub fn assign(key: &str, holdout: f64) -> Arm {
57 let h = holdout.clamp(0.0, 1.0);
58 if h <= 0.0 {
59 return Arm::Treatment;
60 }
61 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
62 let threshold = (h * BUCKETS as f64).round() as u64;
63 if bucket(key) < threshold {
64 Arm::Control
65 } else {
66 Arm::Treatment
67 }
68}
69
70fn flatten_text(v: &Value) -> String {
73 match v {
74 Value::String(s) => s.clone(),
75 Value::Array(items) => items.iter().map(flatten_text).collect::<Vec<_>>().join(" "),
76 Value::Object(map) => {
77 if let Some(Value::String(t)) = map.get("text") {
78 t.clone()
79 } else if let Some(inner) = map.get("content") {
80 flatten_text(inner)
81 } else if let Some(parts) = map.get("parts") {
82 flatten_text(parts)
83 } else {
84 String::new()
85 }
86 }
87 _ => String::new(),
88 }
89}
90
91fn first_message_text(messages: Option<&Value>, role: &str, content_field: &str) -> String {
95 messages
96 .and_then(Value::as_array)
97 .and_then(|arr| {
98 arr.iter()
99 .find(|m| m.get("role").and_then(Value::as_str) == Some(role))
100 })
101 .and_then(|m| m.get(content_field))
102 .map(flatten_text)
103 .unwrap_or_default()
104}
105
106#[must_use]
108pub fn anthropic_key(doc: &Value) -> String {
109 let system = doc.get("system").map(flatten_text).unwrap_or_default();
110 let first_user = first_message_text(doc.get("messages"), "user", "content");
111 format!("{system}{FIELD_SEP}{first_user}")
112}
113
114#[must_use]
117pub fn openai_chat_key(doc: &Value) -> String {
118 let messages = doc.get("messages");
119 let mut system = first_message_text(messages, "system", "content");
120 if system.is_empty() {
121 system = first_message_text(messages, "developer", "content");
122 }
123 let first_user = first_message_text(messages, "user", "content");
124 format!("{system}{FIELD_SEP}{first_user}")
125}
126
127#[must_use]
129pub fn openai_responses_key(doc: &Value) -> String {
130 let system = doc
131 .get("instructions")
132 .map(flatten_text)
133 .unwrap_or_default();
134 let first_user = match doc.get("input") {
136 Some(Value::String(s)) => s.clone(),
137 other => first_message_text(other, "user", "content"),
138 };
139 format!("{system}{FIELD_SEP}{first_user}")
140}
141
142#[must_use]
145pub fn google_key(doc: &Value) -> String {
146 let system = doc
147 .get("systemInstruction")
148 .map(flatten_text)
149 .unwrap_or_default();
150 let first_user = first_message_text(doc.get("contents"), "user", "parts");
151 format!("{system}{FIELD_SEP}{first_user}")
152}
153
154#[cfg(test)]
155mod tests {
156 use super::*;
157 use serde_json::json;
158
159 #[test]
160 fn holdout_zero_is_all_treatment() {
161 assert_eq!(assign("any key", 0.0), Arm::Treatment);
162 assert_eq!(assign("any key", -1.0), Arm::Treatment);
163 }
164
165 #[test]
166 fn holdout_one_is_all_control() {
167 assert_eq!(assign("any key", 1.0), Arm::Control);
168 assert_eq!(assign("another", 2.0), Arm::Control);
169 }
170
171 #[test]
172 fn assignment_is_deterministic_and_stable() {
173 let k = "system\u{1}first user message";
175 let a = assign(k, 0.5);
176 for _ in 0..20 {
177 assert_eq!(assign(k, 0.5), a);
178 }
179 }
180
181 #[test]
182 fn fraction_is_approximately_honoured() {
183 let control = (0..5000)
185 .filter(|i| assign(&format!("conv-{i}"), 0.3) == Arm::Control)
186 .count();
187 let frac = control as f64 / 5000.0;
188 assert!((0.27..0.33).contains(&frac), "got {frac}");
189 }
190
191 #[test]
192 fn anthropic_key_uses_system_and_first_user() {
193 let doc = json!({
194 "system": "You are helpful.",
195 "messages": [
196 {"role": "user", "content": "Hello there"},
197 {"role": "assistant", "content": "Hi"},
198 {"role": "user", "content": "later turn"}
199 ]
200 });
201 assert_eq!(anthropic_key(&doc), "You are helpful.\u{1}Hello there");
202 }
203
204 #[test]
205 fn anthropic_key_flattens_block_arrays() {
206 let doc = json!({
207 "system": [{"type": "text", "text": "Sys A"}, {"type": "text", "text": "Sys B"}],
208 "messages": [{"role": "user", "content": [{"type": "text", "text": "U1"}]}]
209 });
210 assert_eq!(anthropic_key(&doc), "Sys A Sys B\u{1}U1");
211 }
212
213 #[test]
214 fn openai_chat_key_prefers_system_then_developer() {
215 let doc = json!({
216 "messages": [
217 {"role": "developer", "content": "Dev rules"},
218 {"role": "user", "content": "Q"}
219 ]
220 });
221 assert_eq!(openai_chat_key(&doc), "Dev rules\u{1}Q");
222 }
223
224 #[test]
225 fn google_key_uses_system_instruction_and_contents() {
226 let doc = json!({
227 "systemInstruction": {"parts": [{"text": "Be terse"}]},
228 "contents": [{"role": "user", "parts": [{"text": "First Q"}]}]
229 });
230 assert_eq!(google_key(&doc), "Be terse\u{1}First Q");
231 }
232
233 #[test]
234 fn responses_key_handles_string_and_array_input() {
235 let s = json!({"instructions": "Sys", "input": "just a string"});
236 assert_eq!(openai_responses_key(&s), "Sys\u{1}just a string");
237 let a = json!({
238 "instructions": "Sys",
239 "input": [{"role": "user", "content": "arr q"}]
240 });
241 assert_eq!(openai_responses_key(&a), "Sys\u{1}arr q");
242 }
243}