use std::sync::Arc;
use tokenmiser_providers::{ChatResponse, ProviderRegistry};
use tracing::warn;
use crate::{
judge::judge, log_sample, ShadowConfig, ShadowEnqueue, ShadowSample, WinRateAggregator,
};
pub struct ShadowScheduler {
cfg: ShadowConfig,
registry: Arc<ProviderRegistry>,
aggregator: Arc<WinRateAggregator>,
}
impl ShadowScheduler {
pub fn new(
cfg: ShadowConfig,
registry: Arc<ProviderRegistry>,
aggregator: Arc<WinRateAggregator>,
) -> Arc<Self> {
Arc::new(Self {
cfg,
registry,
aggregator,
})
}
pub fn aggregator(&self) -> &Arc<WinRateAggregator> {
&self.aggregator
}
pub fn maybe_enqueue(self: &Arc<Self>, e: ShadowEnqueue) {
if !roll(self.cfg.sample_rate) {
return;
}
let me = Arc::clone(self);
tokio::spawn(async move {
if let Err(e) = me.run(e).await {
warn!(error = %e, "shadow comparison failed");
}
});
}
async fn run(self: Arc<Self>, e: ShadowEnqueue) -> anyhow::Result<()> {
let cheap_text = extract_text(&e.cheap_response);
let mut frontier_req = e.req.clone();
frontier_req.model = self.cfg.frontier_model.clone();
let (frontier_provider, frontier_real) = self
.registry
.resolve(&self.cfg.frontier_model)
.map_err(|err| anyhow::anyhow!("frontier resolve: {err}"))?;
frontier_req.model = frontier_real.clone();
let frontier_resp = frontier_provider
.complete(&frontier_req)
.await
.map_err(|err| anyhow::anyhow!("frontier call: {err}"))?;
let frontier_text = extract_text(&frontier_resp);
let user_prompt = e
.req
.messages
.iter()
.find(|m| m.role == "user")
.and_then(|m| match &m.content {
serde_json::Value::String(s) => Some(s.clone()),
_ => None,
})
.unwrap_or_default();
let verdict = judge(
&self.registry,
&self.cfg.judge_model,
&user_prompt,
&cheap_text,
&frontier_text,
)
.await?;
let sample = ShadowSample {
segment: e.segment,
cheap_model: e.cheap_model,
frontier_model: self.cfg.frontier_model.clone(),
verdict,
};
log_sample(&sample);
self.aggregator.record(&sample);
Ok(())
}
}
fn roll(rate: f32) -> bool {
if rate <= 0.0 {
return false;
}
if rate >= 1.0 {
return true;
}
let nanos = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.subsec_nanos())
.unwrap_or(0);
let r = (nanos as f32) / (u32::MAX as f32);
r < rate
}
fn extract_text(resp: &ChatResponse) -> String {
resp.choices
.first()
.map(|c| match &c.message.content {
serde_json::Value::String(s) => s.clone(),
other => other.to_string(),
})
.unwrap_or_default()
}
#[allow(dead_code)]
fn _arc_typecheck(_: Arc<ShadowScheduler>) {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn roll_extremes() {
assert!(roll(1.0));
assert!(!roll(0.0));
}
}