use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use std::time::Duration;
#[derive(Debug, Clone)]
pub struct LatencyStats {
pub min: Duration,
pub max: Duration,
pub avg: Duration,
pub p50: Duration,
pub p95: Duration,
pub p99: Duration,
pub throughput_ops_sec: f64,
}
impl LatencyStats {
pub fn from_durations(durations: &[Duration]) -> Option<Self> {
if durations.is_empty() {
return None;
}
let mut sorted = durations.to_vec();
sorted.sort();
let n = sorted.len();
let total: Duration = sorted.iter().sum();
let avg = total / n as u32;
Some(Self {
min: sorted[0],
max: sorted[n - 1],
avg,
p50: sorted[n / 2],
p95: sorted[n * 95 / 100],
p99: sorted[n * 99 / 100],
throughput_ops_sec: n as f64 / total.as_secs_f64(),
})
}
pub fn print(&self, operation: &str, blocks_per_op: usize) {
println!("\n{} Latency Statistics:", operation);
println!(" min: {:>12?}", self.min);
println!(" avg: {:>12?}", self.avg);
println!(" p50: {:>12?}", self.p50);
println!(" p95: {:>12?}", self.p95);
println!(" p99: {:>12?}", self.p99);
println!(" max: {:>12?}", self.max);
println!(" throughput: {:.2} ops/sec", self.throughput_ops_sec);
println!(
" throughput: {:.2} blocks/sec",
self.throughput_ops_sec * blocks_per_op as f64
);
}
}
#[derive(Debug, Clone, Serialize)]
pub struct TimeBucketStats {
pub bucket_start_sec: u64,
pub bucket_end_sec: u64,
pub count: usize,
pub latency_min_us: u64,
pub latency_p50_us: u64,
pub latency_p95_us: u64,
pub latency_max_us: u64,
}
pub fn compute_time_bucket_stats(
items: &[(Duration, Duration)],
bucket_size_secs: u64,
) -> Vec<TimeBucketStats> {
if items.is_empty() || bucket_size_secs == 0 {
return Vec::new();
}
let max_completion = items
.iter()
.map(|&(_, ct)| ct)
.max()
.unwrap_or(Duration::ZERO);
let num_buckets = (max_completion.as_secs() / bucket_size_secs) + 1;
let mut bucket_latencies: Vec<Vec<Duration>> = vec![Vec::new(); num_buckets as usize];
for &(latency, completion_time) in items {
let bucket_idx = (completion_time.as_secs() / bucket_size_secs) as usize;
if bucket_idx < bucket_latencies.len() {
bucket_latencies[bucket_idx].push(latency);
}
}
bucket_latencies
.iter()
.enumerate()
.filter_map(|(idx, latencies)| {
if latencies.is_empty() {
return None;
}
let stats = LatencyStats::from_durations(latencies)?;
Some(TimeBucketStats {
bucket_start_sec: idx as u64 * bucket_size_secs,
bucket_end_sec: (idx as u64 + 1) * bucket_size_secs,
count: latencies.len(),
latency_min_us: stats.min.as_micros() as u64,
latency_p50_us: stats.p50.as_micros() as u64,
latency_p95_us: stats.p95.as_micros() as u64,
latency_max_us: stats.max.as_micros() as u64,
})
})
.collect()
}
pub fn print_time_bucket_report(buckets: &[TimeBucketStats]) {
if buckets.is_empty() {
println!(" No time bucket data available");
return;
}
println!(
" {:>8} {:>8} {:>12} {:>12} {:>12} {:>12}",
"Time(s)", "Count", "Min(ms)", "P50(ms)", "P95(ms)", "Max(ms)"
);
println!(" {}", "-".repeat(68));
for bucket in buckets {
println!(
" {:>3}-{:<4} {:>8} {:>12.1} {:>12.1} {:>12.1} {:>12.1}",
bucket.bucket_start_sec,
bucket.bucket_end_sec,
bucket.count,
bucket.latency_min_us as f64 / 1000.0,
bucket.latency_p50_us as f64 / 1000.0,
bucket.latency_p95_us as f64 / 1000.0,
bucket.latency_max_us as f64 / 1000.0,
);
}
}
#[derive(Debug, Clone, Serialize)]
pub struct LatencySample {
pub latency_us: u64,
pub completion_time_ms: u64,
pub success: bool,
}
#[derive(Debug, Clone, Serialize)]
pub struct ChatMessage {
pub role: String,
pub content: String,
}
#[derive(Debug, Serialize)]
pub struct ChatCompletionRequest {
pub model: String,
pub messages: Vec<ChatMessage>,
#[serde(skip_serializing_if = "Option::is_none")]
pub max_tokens: Option<u32>,
}
#[derive(Debug, Deserialize)]
struct ModelsResponse {
data: Vec<ModelInfo>,
}
#[derive(Debug, Deserialize)]
struct ModelInfo {
id: String,
}
pub async fn fetch_model_name(frontend_url: &str) -> Result<String> {
let client = reqwest::Client::new();
let url = format!("{}/v1/models", frontend_url);
println!(" Auto-detecting model from {}...", url);
let response = client
.get(&url)
.send()
.await
.context("Failed to connect to frontend /v1/models endpoint")?;
if !response.status().is_success() {
anyhow::bail!("Models endpoint returned status: {}", response.status());
}
let models: ModelsResponse = response
.json()
.await
.context("Failed to parse models response")?;
match models.data.len() {
0 => anyhow::bail!("No models found at endpoint. Is a backend running?"),
1 => {
let model_id = models.data[0].id.clone();
println!(" Auto-detected model: {}", model_id);
Ok(model_id)
}
n => {
println!(" Multiple models available ({}):", n);
for m in &models.data {
println!(" - {}", m.id);
}
anyhow::bail!("Multiple models available. Please specify --model explicitly.")
}
}
}