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, Group, 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 seed: u64,
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_seed(client, depth, lang, claimed_model, Rng::new().next_u64())
}
pub fn with_seed(
client: Client,
depth: Depth,
lang: Lang,
claimed_model: String,
seed: u64,
) -> Self {
Self {
client,
depth,
lang,
claimed_model,
seed,
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 rng_for(&self, step_id: &str) -> Rng {
let mut h: u64 = 0xcbf2_9ce4_8422_2325;
for b in step_id.as_bytes() {
h ^= *b as u64;
h = h.wrapping_mul(0x0000_0100_0000_01b3);
}
Rng::from_seed(self.seed ^ h)
}
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 group: Group,
pub subject: Subject,
pub results: usize,
pub always: bool,
pub exclusive: 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 exclusive(&self) -> bool {
false
}
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 Group::{Consistency, Contract, Identity, Perf, Stream};
use Subject::{Endpoint, Model};
let spec = |id, group, subject, results, run| ProbeSpec {
id,
group,
subject,
results,
always: false,
exclusive: false,
run,
};
vec![
ProbeSpec {
id: "preflight",
group: Contract,
subject: Endpoint,
results: 1,
always: true,
exclusive: true,
run: one!(contract::preflight),
},
spec(
"model_catalog",
Contract,
Endpoint,
1,
one!(contract::model_catalog),
),
spec(
"response_schema",
Contract,
Endpoint,
1,
one!(contract::response_schema),
),
spec(
"model_echo",
Contract,
Endpoint,
1,
one!(contract::model_echo),
),
spec(
"missing_version",
Contract,
Endpoint,
1,
one!(contract::missing_version),
),
spec(
"missing_auth",
Contract,
Endpoint,
1,
one!(contract::missing_auth),
),
spec(
"invalid_model",
Contract,
Endpoint,
1,
one!(contract::invalid_model),
),
spec(
"error_envelope",
Contract,
Endpoint,
1,
one!(contract::error_envelope),
),
spec(
"stop_reason_enum",
Contract,
Endpoint,
1,
one!(contract::stop_reason_enum),
),
spec(
"max_tokens_truncation",
Contract,
Model,
1,
one!(contract::max_tokens_truncation),
),
spec(
"stop_sequence",
Contract,
Model,
1,
one!(contract::stop_sequence),
),
spec(
"system_adherence",
Contract,
Model,
1,
one!(contract::system_adherence),
),
spec("sse_format", Stream, Endpoint, 1, one!(stream::sse_format)),
spec(
"stream_not_empty",
Stream,
Endpoint,
1,
one!(stream::stream_not_empty),
),
spec(
"stream_usage",
Stream,
Endpoint,
1,
one!(stream::stream_usage),
),
spec("billing", Group::Billing, Endpoint, 7, many!(billing::run)),
spec("self_id", Identity, Model, 1, one!(identity::self_id)),
spec(
"meta_creator",
Identity,
Model,
1,
one!(identity::meta_creator),
),
spec(
"context_claim",
Identity,
Model,
1,
one!(identity::context_claim),
),
spec(
"cutoff_claim",
Identity,
Model,
1,
one!(identity::cutoff_claim),
),
spec(
"world_knowledge",
Identity,
Model,
1,
one!(identity::world_knowledge),
),
spec(
"capability",
Identity,
Model,
2,
many!(identity::capability),
),
spec("verbosity", Identity, Model, 1, one!(identity::verbosity)),
spec(
"signature_drift",
Consistency,
Model,
1,
one!(consistency::signature_drift),
),
spec(
"cache_replay",
Consistency,
Model,
1,
one!(consistency::cache_replay),
),
spec(
"request_id_unique",
Consistency,
Endpoint,
1,
one!(consistency::request_id_unique),
),
ProbeSpec {
id: "perf",
group: Perf,
subject: Model,
results: 4,
always: false,
exclusive: true,
run: many!(perf::run),
},
spec("channel", Group::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 replaced: 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("replaced", &self.replaced)
.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 turbo() -> Self {
Selection {
only: ["self_id", "capability", "verbosity", "cache_replay", "perf"]
.iter()
.map(|s| s.to_string())
.collect(),
..Default::default()
}
}
pub fn plus<I, S>(mut self, ids: I) -> Self
where
I: IntoIterator<Item = S>,
S: AsRef<str>,
{
if !self.only.is_empty() {
self.only
.extend(ids.into_iter().map(|s| s.as_ref().to_string()));
}
self
}
pub fn minus<I, S>(mut self, ids: I) -> Self
where
I: IntoIterator<Item = S>,
S: AsRef<str>,
{
self.skip
.extend(ids.into_iter().map(|s| s.as_ref().to_string()));
self
}
pub fn with(mut self, p: std::sync::Arc<dyn Probe>) -> Self {
self.extra.push(p);
self
}
pub fn replacing(mut self, id: &str, p: std::sync::Arc<dyn Probe>) -> Self {
self.replaced.push(id.to_string());
self.extra.push(p);
self
}
fn names(spec: &ProbeSpec, pattern: &str) -> bool {
pattern == spec.id || pattern == spec.group.key()
}
fn keeps(&self, spec: &ProbeSpec) -> bool {
if spec.always {
return true;
}
if self.skip.iter().any(|s| Self::names(spec, s))
|| self.replaced.iter().any(|s| Self::names(spec, s))
{
return false;
}
if !self.subjects.is_empty() && !self.subjects.contains(&spec.subject) {
return false;
}
if !self.only.is_empty() && !self.only.iter().any(|s| Self::names(spec, s)) {
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, Schedule::paced(pace), on_event).await
}
#[derive(Debug, Clone, Copy, Default)]
pub struct Schedule {
pub pace: Option<Pace>,
pub concurrency: usize,
}
impl Schedule {
pub fn sequential() -> Self {
Schedule::default()
}
pub fn paced(pace: Option<Pace>) -> Self {
Schedule {
pace,
concurrency: 0,
}
}
pub fn concurrent(n: usize) -> Self {
Schedule {
pace: None,
concurrency: n,
}
}
fn overlaps(&self) -> bool {
self.pace.is_none() && self.concurrency > 1
}
}
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 exclusive(&self) -> bool {
match self {
Step::Built(s) => s.exclusive,
Step::Custom(p) => p.exclusive(),
}
}
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,
schedule: Schedule,
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, schedule, on_event).await
}
async fn run_steps(
ctx: &Ctx,
specs: &[Step<'_>],
cancel: &Cancel,
schedule: Schedule,
on_event: &mut (dyn FnMut(Event<'_>) + Send),
) -> Vec<ProbeResult> {
let total: usize = specs.iter().map(|s| s.results()).sum();
let mut collected: Vec<Vec<ProbeResult>> = vec![Vec::new(); specs.len()];
let mut done = 0usize;
let mut first = true;
let mut i = 0usize;
while i < specs.len() {
if cancel.is_cancelled() {
break;
}
let wave = if schedule.overlaps() && !specs[i].exclusive() {
specs[i..]
.iter()
.take_while(|s| !s.exclusive())
.count()
.max(1)
} else {
1
};
if let (false, Some(p)) = (first, schedule.pace) {
let span = p.max.saturating_sub(p.min);
let jitter = if span.is_zero() {
std::time::Duration::ZERO
} else {
let r = ctx.rng_for("__pace").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;
use futures_util::StreamExt;
let end = i + wave;
let cap = if wave == 1 {
1
} else {
schedule.concurrency.max(1)
};
let mut running = futures_util::stream::FuturesUnordered::new();
let mut next = i;
loop {
while next < end && running.len() < cap {
let at = next;
on_event(Event::Started {
id: specs[at].id(),
done,
total,
});
running.push(async move { (at, specs[at].run(ctx).await) });
next += 1;
}
let Some((at, results)) = running.next().await else {
break;
};
for r in results {
collected[at].push(r);
done += 1;
on_event(Event::Finished {
result: collected[at].last().unwrap(),
done,
total,
});
}
}
if !ctx.is_reachable() {
break;
}
i += wave;
}
collected.into_iter().flatten().collect()
}
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 the_clock_reading_and_the_gate_run_alone() {
let exclusive: Vec<&str> = registry()
.iter()
.filter(|s| s.exclusive)
.map(|s| s.id)
.collect();
assert_eq!(exclusive, vec!["preflight", "perf"]);
}
#[test]
fn a_step_s_questions_depend_on_the_seed_and_its_own_id_only() {
let ctx = |seed| {
Ctx::with_seed(
Client::with_http(crate::client::Endpoint::default(), reqwest::Client::new()),
Depth::Fast,
Lang::En,
"m".into(),
seed,
)
};
let a = ctx(0xC0FFEE);
let b = ctx(0xC0FFEE);
let first = a.rng_for("capability").hex(8);
let _ = a.rng_for("cache_replay").hex(8);
let _ = a.rng_for("stop_sequence").hex(8);
assert_eq!(a.rng_for("capability").hex(8), first);
assert_eq!(b.rng_for("capability").hex(8), first);
assert_ne!(a.rng_for("cache_replay").hex(8), first);
assert_ne!(ctx(0xC0FFEF).rng_for("capability").hex(8), first);
}
#[test]
fn a_schedule_overlaps_only_when_it_can() {
assert!(!Schedule::sequential().overlaps());
assert!(!Schedule::concurrent(1).overlaps());
assert!(Schedule::concurrent(4).overlaps());
let paced = Schedule {
pace: Some(Pace {
min: std::time::Duration::from_secs(20),
max: std::time::Duration::from_secs(180),
}),
concurrency: 8,
};
assert!(!paced.overlaps());
}
#[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());
}
}