codex-helper-core 0.12.1

Core library for codex-helper.
Documentation
use std::collections::HashMap;

use serde::{Deserialize, Serialize};

use crate::state::FinishedRequest;

#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct WindowStats {
    pub total: usize,
    pub ok_2xx: usize,
    pub err_429: usize,
    pub err_4xx: usize,
    pub err_5xx: usize,
    pub p50_ms: Option<u64>,
    pub p95_ms: Option<u64>,
    pub avg_attempts: Option<f64>,
    pub retry_rate: Option<f64>,
    pub top_provider: Option<(String, usize)>,
    pub top_config: Option<(String, usize)>,
}

pub fn compute_window_stats<F>(
    recent: &[FinishedRequest],
    now_ms: u64,
    window_ms: u64,
    mut include: F,
) -> WindowStats
where
    F: FnMut(&FinishedRequest) -> bool,
{
    fn percentile(mut v: Vec<u64>, p: f64) -> Option<u64> {
        if v.is_empty() {
            return None;
        }
        let n = v.len();
        let idx = ((p * (n.saturating_sub(1) as f64)).ceil() as usize).min(n - 1);
        let (_, nth, _) = v.select_nth_unstable(idx);
        Some(*nth)
    }

    let cutoff = now_ms.saturating_sub(window_ms);
    let mut out = WindowStats::default();
    let mut ok_lat = Vec::new();
    let mut attempts_sum: u64 = 0;
    let mut retry_cnt: u64 = 0;

    let mut by_provider: HashMap<String, usize> = HashMap::new();
    let mut by_config: HashMap<String, usize> = HashMap::new();

    for r in recent.iter() {
        if r.ended_at_ms < cutoff {
            continue;
        }
        if !include(r) {
            continue;
        }
        out.total += 1;

        let attempts = r.retry.as_ref().map(|x| x.attempts).unwrap_or(1);
        attempts_sum = attempts_sum.saturating_add(attempts as u64);
        if attempts > 1 {
            retry_cnt = retry_cnt.saturating_add(1);
        }

        if r.status_code == 429 {
            out.err_429 += 1;
        } else if (400..500).contains(&r.status_code) {
            out.err_4xx += 1;
        } else if (500..600).contains(&r.status_code) {
            out.err_5xx += 1;
        }

        if (200..300).contains(&r.status_code) {
            out.ok_2xx += 1;
            ok_lat.push(r.duration_ms);

            if let Some(pid) = r.provider_id.as_deref()
                && !pid.trim().is_empty()
            {
                *by_provider.entry(pid.to_string()).or_insert(0) += 1;
            }
            if let Some(cfg) = r.config_name.as_deref()
                && !cfg.trim().is_empty()
            {
                *by_config.entry(cfg.to_string()).or_insert(0) += 1;
            }
        }
    }

    out.p50_ms = percentile(ok_lat.clone(), 0.50);
    out.p95_ms = percentile(ok_lat, 0.95);
    if out.total > 0 {
        out.avg_attempts = Some(attempts_sum as f64 / out.total as f64);
        out.retry_rate = Some(retry_cnt as f64 / out.total as f64);
    }

    out.top_provider = by_provider.into_iter().max_by_key(|(_, v)| *v);
    out.top_config = by_config.into_iter().max_by_key(|(_, v)| *v);
    out
}