pub mod billing;
pub mod channel;
pub mod consistency;
pub mod contract;
pub mod identity;
pub mod perf;
pub mod stream;
use crate::client::Client;
use crate::i18n::Lang;
use crate::report::{BillingRound, ProbeResult};
use crate::util::Rng;
use std::collections::BTreeMap;
use std::sync::Mutex;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Depth {
Fast,
Balanced,
Forensic,
}
impl Depth {
pub fn parse(s: &str) -> Option<Self> {
match s.trim().to_ascii_lowercase().as_str() {
"fast" => Some(Self::Fast),
"balanced" | "default" => Some(Self::Balanced),
"forensic" | "deep" => Some(Self::Forensic),
_ => None,
}
}
pub fn as_str(&self) -> &'static str {
match self {
Self::Fast => "fast",
Self::Balanced => "balanced",
Self::Forensic => "forensic",
}
}
pub fn repeats(&self) -> usize {
match self {
Self::Fast => 1,
Self::Balanced => 3,
Self::Forensic => 6,
}
}
pub fn tier_questions(&self) -> usize {
match self {
Self::Fast => 1,
Self::Balanced => 3,
Self::Forensic => 5,
}
}
}
#[derive(Debug, Clone)]
pub struct PerfSample {
pub probe: String,
pub ttft_ms: Option<u64>,
pub latency_ms: u64,
pub output_tokens: u32,
}
impl PerfSample {
pub fn tps(&self) -> Option<f64> {
let ttft = self.ttft_ms? as f64;
let gen_ms = self.latency_ms as f64 - ttft;
if gen_ms <= 0.0 || self.output_tokens == 0 {
return None;
}
Some(self.output_tokens as f64 / (gen_ms / 1000.0))
}
}
pub struct Ctx {
pub client: Client,
pub depth: Depth,
pub lang: Lang,
pub claimed_model: String,
pub rng: Mutex<Rng>,
pub perf: Mutex<Vec<PerfSample>>,
pub billing: Mutex<Vec<BillingRound>>,
pub headers: Mutex<Vec<BTreeMap<String, String>>>,
pub message_ids: Mutex<Vec<String>>,
pub raw_bodies: Mutex<Vec<String>>,
pub reachable: Mutex<bool>,
}
impl Ctx {
pub fn new(client: Client, depth: Depth, lang: Lang, claimed_model: String) -> Self {
Self::with_rng(client, depth, lang, claimed_model, Rng::new())
}
pub fn with_rng(
client: Client,
depth: Depth,
lang: Lang,
claimed_model: String,
rng: Rng,
) -> Self {
Self {
client,
depth,
lang,
claimed_model,
rng: Mutex::new(rng),
perf: Mutex::new(Vec::new()),
billing: Mutex::new(Vec::new()),
headers: Mutex::new(Vec::new()),
message_ids: Mutex::new(Vec::new()),
raw_bodies: Mutex::new(Vec::new()),
reachable: Mutex::new(true),
}
}
pub fn observe(&self, raw: &crate::client::RawResponse, id: &str) {
self.headers.lock().unwrap().push(raw.headers.clone());
if !id.is_empty() {
self.message_ids.lock().unwrap().push(id.to_string());
}
let mut bodies = self.raw_bodies.lock().unwrap();
if bodies.len() < 12 {
bodies.push(crate::util::truncate(&raw.body, 4000));
}
}
pub fn add_perf(&self, sample: PerfSample) {
self.perf.lock().unwrap().push(sample);
}
pub fn is_reachable(&self) -> bool {
*self.reachable.lock().unwrap()
}
pub fn set_reachable(&self, v: bool) {
*self.reachable.lock().unwrap() = v;
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Subject {
Endpoint,
Model,
}
pub type ProbeFuture<'a> = futures_util::future::BoxFuture<'a, Vec<ProbeResult>>;
pub struct ProbeSpec {
pub id: &'static str,
pub subject: Subject,
pub results: usize,
pub always: bool,
pub run: for<'a> fn(&'a Ctx) -> ProbeFuture<'a>,
}
pub trait Probe: Send + Sync {
fn id(&self) -> &str;
fn subject(&self) -> Subject {
Subject::Model
}
fn results(&self) -> usize {
1
}
fn run<'a>(&'a self, ctx: &'a Ctx) -> ProbeFuture<'a>;
}
macro_rules! one {
($f:path) => {
(|ctx: &Ctx| Box::pin(async move { vec![$f(ctx).await] }) as ProbeFuture<'_>)
as for<'a> fn(&'a Ctx) -> ProbeFuture<'a>
};
}
macro_rules! many {
($f:path) => {
(|ctx: &Ctx| Box::pin($f(ctx)) as ProbeFuture<'_>) as for<'a> fn(&'a Ctx) -> ProbeFuture<'a>
};
}
pub fn registry() -> Vec<ProbeSpec> {
use Subject::{Endpoint, Model};
let spec = |id, subject, results, run| ProbeSpec {
id,
subject,
results,
always: false,
run,
};
vec![
ProbeSpec {
id: "preflight",
subject: Endpoint,
results: 1,
always: true,
run: one!(contract::preflight),
},
spec("model_catalog", Endpoint, 1, one!(contract::model_catalog)),
spec(
"response_schema",
Endpoint,
1,
one!(contract::response_schema),
),
spec("model_echo", Endpoint, 1, one!(contract::model_echo)),
spec(
"missing_version",
Endpoint,
1,
one!(contract::missing_version),
),
spec("missing_auth", Endpoint, 1, one!(contract::missing_auth)),
spec("invalid_model", Endpoint, 1, one!(contract::invalid_model)),
spec(
"error_envelope",
Endpoint,
1,
one!(contract::error_envelope),
),
spec(
"stop_reason_enum",
Endpoint,
1,
one!(contract::stop_reason_enum),
),
spec(
"max_tokens_truncation",
Model,
1,
one!(contract::max_tokens_truncation),
),
spec("stop_sequence", Model, 1, one!(contract::stop_sequence)),
spec(
"system_adherence",
Model,
1,
one!(contract::system_adherence),
),
spec("sse_format", Endpoint, 1, one!(stream::sse_format)),
spec(
"stream_not_empty",
Endpoint,
1,
one!(stream::stream_not_empty),
),
spec("stream_usage", Endpoint, 1, one!(stream::stream_usage)),
spec("billing", Endpoint, 7, many!(billing::run)),
spec("identity", Model, 8, many!(identity::run)),
spec(
"signature_drift",
Model,
1,
one!(consistency::signature_drift),
),
spec("cache_replay", Model, 1, one!(consistency::cache_replay)),
spec(
"request_id_unique",
Endpoint,
1,
one!(consistency::request_id_unique),
),
spec("perf", Model, 4, many!(perf::run)),
spec("channel", Endpoint, 3, many!(channel::run)),
]
}
#[derive(Clone, Default)]
pub struct Selection {
pub subjects: Vec<Subject>,
pub only: Vec<String>,
pub skip: Vec<String>,
pub extra: Vec<std::sync::Arc<dyn Probe>>,
}
impl std::fmt::Debug for Selection {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Selection")
.field("subjects", &self.subjects)
.field("only", &self.only)
.field("skip", &self.skip)
.field(
"extra",
&self.extra.iter().map(|p| p.id()).collect::<Vec<_>>(),
)
.finish()
}
}
impl Selection {
pub fn all() -> Self {
Self::default()
}
pub fn model_only() -> Self {
Selection {
subjects: vec![Subject::Model],
..Default::default()
}
}
pub fn with(mut self, p: std::sync::Arc<dyn Probe>) -> Self {
self.extra.push(p);
self
}
fn keeps(&self, spec: &ProbeSpec) -> bool {
if spec.always {
return true;
}
if self.skip.iter().any(|s| s == spec.id) {
return false;
}
if !self.subjects.is_empty() && !self.subjects.contains(&spec.subject) {
return false;
}
if !self.only.is_empty() && !self.only.iter().any(|s| s == spec.id) {
return false;
}
true
}
pub fn resolve(&self) -> Vec<ProbeSpec> {
registry().into_iter().filter(|s| self.keeps(s)).collect()
}
pub fn resolve_extra(&self) -> Vec<std::sync::Arc<dyn Probe>> {
self.extra
.iter()
.filter(|p| !self.skip.iter().any(|s| s == p.id()))
.cloned()
.collect()
}
}
pub enum Event<'a> {
Started {
id: &'a str,
done: usize,
total: usize,
},
Finished {
result: &'a ProbeResult,
done: usize,
total: usize,
},
}
#[derive(Clone)]
pub struct Cancel {
flag: std::sync::Arc<std::sync::atomic::AtomicBool>,
notify: std::sync::Arc<tokio::sync::Notify>,
}
impl Default for Cancel {
fn default() -> Self {
Cancel {
flag: std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)),
notify: std::sync::Arc::new(tokio::sync::Notify::new()),
}
}
}
impl Cancel {
pub fn new() -> Self {
Self::default()
}
pub fn cancel(&self) {
self.flag.store(true, std::sync::atomic::Ordering::Relaxed);
self.notify.notify_waiters();
}
pub fn is_cancelled(&self) -> bool {
self.flag.load(std::sync::atomic::Ordering::Relaxed)
}
pub async fn wait(&self) {
if self.is_cancelled() {
return;
}
self.notify.notified().await;
}
}
#[derive(Debug, Clone, Copy)]
pub struct Pace {
pub min: std::time::Duration,
pub max: std::time::Duration,
}
pub async fn run_selected(
ctx: &Ctx,
specs: &[ProbeSpec],
cancel: &Cancel,
on_event: &mut (dyn FnMut(Event<'_>) + Send),
) -> Vec<ProbeResult> {
run_paced(ctx, specs, cancel, None, on_event).await
}
pub async fn run_paced(
ctx: &Ctx,
specs: &[ProbeSpec],
cancel: &Cancel,
pace: Option<Pace>,
on_event: &mut (dyn FnMut(Event<'_>) + Send),
) -> Vec<ProbeResult> {
run_with_extra(ctx, specs, &[], cancel, pace, on_event).await
}
enum Step<'a> {
Built(&'a ProbeSpec),
Custom(&'a std::sync::Arc<dyn Probe>),
}
impl Step<'_> {
fn id(&self) -> &str {
match self {
Step::Built(s) => s.id,
Step::Custom(p) => p.id(),
}
}
fn results(&self) -> usize {
match self {
Step::Built(s) => s.results,
Step::Custom(p) => p.results(),
}
}
fn run<'a>(&'a self, ctx: &'a Ctx) -> ProbeFuture<'a> {
match self {
Step::Built(s) => (s.run)(ctx),
Step::Custom(p) => p.run(ctx),
}
}
}
pub async fn run_with_extra(
ctx: &Ctx,
specs: &[ProbeSpec],
extra: &[std::sync::Arc<dyn Probe>],
cancel: &Cancel,
pace: Option<Pace>,
on_event: &mut (dyn FnMut(Event<'_>) + Send),
) -> Vec<ProbeResult> {
let steps: Vec<Step<'_>> = specs
.iter()
.map(Step::Built)
.chain(extra.iter().map(Step::Custom))
.collect();
run_steps(ctx, &steps, cancel, pace, on_event).await
}
async fn run_steps(
ctx: &Ctx,
specs: &[Step<'_>],
cancel: &Cancel,
pace: Option<Pace>,
on_event: &mut (dyn FnMut(Event<'_>) + Send),
) -> Vec<ProbeResult> {
let total: usize = specs.iter().map(|s| s.results()).sum();
let mut out: Vec<ProbeResult> = Vec::new();
let mut first = true;
for spec in specs {
if cancel.is_cancelled() {
break;
}
if let (false, Some(p)) = (first, pace) {
let span = p.max.saturating_sub(p.min);
let jitter = if span.is_zero() {
std::time::Duration::ZERO
} else {
let r = ctx.rng.lock().unwrap().next_u64();
std::time::Duration::from_millis(r % (span.as_millis() as u64).max(1))
};
let wait = p.min + jitter;
tokio::select! {
_ = tokio::time::sleep(wait) => {}
_ = cancel.wait() => break,
}
}
first = false;
on_event(Event::Started {
id: spec.id(),
done: out.len(),
total,
});
for r in spec.run(ctx).await {
out.push(r);
let done = out.len();
on_event(Event::Finished {
result: out.last().unwrap(),
done,
total,
});
}
if !ctx.is_reachable() {
break;
}
}
out
}
pub fn probe_count() -> usize {
registry().iter().map(|s| s.results).sum()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn depth_parses_and_scales_repeats_monotonically() {
assert_eq!(Depth::parse("FAST"), Some(Depth::Fast));
assert_eq!(Depth::parse("deep"), Some(Depth::Forensic));
assert_eq!(Depth::parse("nonsense"), None);
assert!(Depth::Fast.repeats() < Depth::Balanced.repeats());
assert!(Depth::Balanced.repeats() < Depth::Forensic.repeats());
}
#[test]
fn tps_excludes_the_wait_for_first_token() {
let s = PerfSample {
probe: "p".into(),
ttft_ms: Some(1000),
latency_ms: 3000,
output_tokens: 100,
};
assert_eq!(s.tps(), Some(50.0));
}
#[test]
fn tps_is_none_when_the_sample_cannot_support_it() {
let base = PerfSample {
probe: "p".into(),
ttft_ms: Some(500),
latency_ms: 1500,
output_tokens: 10,
};
assert!(base.tps().is_some());
let mut s = base.clone();
s.ttft_ms = None;
assert!(s.tps().is_none());
let mut s = base.clone();
s.output_tokens = 0;
assert!(s.tps().is_none());
let mut s = base.clone();
s.latency_ms = 500;
assert!(s.tps().is_none());
}
}