tokenmiser_quality/
scheduler.rs1use std::sync::Arc;
11
12use tokenmiser_providers::{ChatResponse, ProviderRegistry};
13use tracing::warn;
14
15use crate::{
16 judge::judge, log_sample, ShadowConfig, ShadowEnqueue, ShadowSample, WinRateAggregator,
17};
18
19pub struct ShadowScheduler {
20 cfg: ShadowConfig,
21 registry: Arc<ProviderRegistry>,
22 aggregator: Arc<WinRateAggregator>,
23}
24
25impl ShadowScheduler {
26 pub fn new(
27 cfg: ShadowConfig,
28 registry: Arc<ProviderRegistry>,
29 aggregator: Arc<WinRateAggregator>,
30 ) -> Arc<Self> {
31 Arc::new(Self {
32 cfg,
33 registry,
34 aggregator,
35 })
36 }
37
38 pub fn aggregator(&self) -> &Arc<WinRateAggregator> {
39 &self.aggregator
40 }
41
42 pub fn maybe_enqueue(self: &Arc<Self>, e: ShadowEnqueue) {
45 if !roll(self.cfg.sample_rate) {
46 return;
47 }
48 let me = Arc::clone(self);
49 tokio::spawn(async move {
50 if let Err(e) = me.run(e).await {
51 warn!(error = %e, "shadow comparison failed");
52 }
53 });
54 }
55
56 async fn run(self: Arc<Self>, e: ShadowEnqueue) -> anyhow::Result<()> {
57 let cheap_text = extract_text(&e.cheap_response);
58
59 let mut frontier_req = e.req.clone();
61 frontier_req.model = self.cfg.frontier_model.clone();
62 let (frontier_provider, frontier_real) = self
63 .registry
64 .resolve(&self.cfg.frontier_model)
65 .map_err(|err| anyhow::anyhow!("frontier resolve: {err}"))?;
66 frontier_req.model = frontier_real.clone();
67 let frontier_resp = frontier_provider
68 .complete(&frontier_req)
69 .await
70 .map_err(|err| anyhow::anyhow!("frontier call: {err}"))?;
71 let frontier_text = extract_text(&frontier_resp);
72
73 let user_prompt = e
75 .req
76 .messages
77 .iter()
78 .find(|m| m.role == "user")
79 .and_then(|m| match &m.content {
80 serde_json::Value::String(s) => Some(s.clone()),
81 _ => None,
82 })
83 .unwrap_or_default();
84
85 let verdict = judge(
86 &self.registry,
87 &self.cfg.judge_model,
88 &user_prompt,
89 &cheap_text,
90 &frontier_text,
91 )
92 .await?;
93
94 let sample = ShadowSample {
95 segment: e.segment,
96 cheap_model: e.cheap_model,
97 frontier_model: self.cfg.frontier_model.clone(),
98 verdict,
99 };
100 log_sample(&sample);
101 self.aggregator.record(&sample);
102
103 Ok(())
104 }
105}
106
107fn roll(rate: f32) -> bool {
108 if rate <= 0.0 {
109 return false;
110 }
111 if rate >= 1.0 {
112 return true;
113 }
114 let nanos = std::time::SystemTime::now()
117 .duration_since(std::time::UNIX_EPOCH)
118 .map(|d| d.subsec_nanos())
119 .unwrap_or(0);
120 let r = (nanos as f32) / (u32::MAX as f32);
121 r < rate
122}
123
124fn extract_text(resp: &ChatResponse) -> String {
125 resp.choices
126 .first()
127 .map(|c| match &c.message.content {
128 serde_json::Value::String(s) => s.clone(),
129 other => other.to_string(),
130 })
131 .unwrap_or_default()
132}
133
134#[allow(dead_code)]
137fn _arc_typecheck(_: Arc<ShadowScheduler>) {}
138
139#[cfg(test)]
140mod tests {
141 use super::*;
142
143 #[test]
144 fn roll_extremes() {
145 assert!(roll(1.0));
146 assert!(!roll(0.0));
147 }
148}