use super::discovery::EndpointInfo;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum CacheClassification {
Hit,
Partial,
Drop,
NotSupported,
Invalid,
}
impl CacheClassification {
#[must_use]
pub(crate) fn as_str(self) -> &'static str {
match self {
Self::Hit => "hit",
Self::Partial => "partial",
Self::Drop => "drop",
Self::NotSupported => "not_supported",
Self::Invalid => "invalid",
}
}
}
#[must_use]
#[expect(clippy::cast_precision_loss)]
pub(crate) fn classify_round(cached: u64, expected_cached: u64) -> CacheClassification {
if expected_cached == 0 {
return CacheClassification::NotSupported;
}
let ratio = cached as f64 / expected_cached as f64;
if ratio >= 0.9 {
CacheClassification::Hit
} else if ratio < 0.1 {
CacheClassification::Drop
} else {
CacheClassification::Partial
}
}
#[must_use]
pub(crate) fn ttl_bucket(
classifications: &[CacheClassification],
nominal_gaps_secs: &[f64],
) -> String {
let ladder: Vec<(CacheClassification, f64)> = classifications
.iter()
.zip(nominal_gaps_secs)
.map(|(c, g)| (*c, *g))
.collect();
if ladder.is_empty() {
return "not measured".to_string();
}
let usable: Vec<CacheClassification> = ladder
.iter()
.map(|(c, _)| *c)
.filter(|c| *c != CacheClassification::Invalid)
.collect();
if usable.is_empty() {
return "not measured".to_string();
}
if usable
.iter()
.all(|c| *c == CacheClassification::NotSupported)
{
return "does not cache".to_string();
}
if let Some(k) = ladder
.iter()
.position(|(c, _)| *c == CacheClassification::Drop)
{
if ladder[k].1 == 0.0 {
return "immediate drop".to_string();
}
for (j, (c, _)) in ladder.iter().enumerate().take(k).rev() {
if matches!(c, CacheClassification::Hit | CacheClassification::Partial) {
return format_bucket(ladder[j].1, ladder[k].1);
}
}
return format_bucket(0.0, ladder[k].1);
}
for (j, (c, _)) in ladder.iter().enumerate().rev() {
if matches!(c, CacheClassification::Hit | CacheClassification::Partial) {
return format_bucket(ladder[j].1, f64::INFINITY);
}
}
"not measured".to_string()
}
#[must_use]
pub(crate) fn cache_hold_result(
any_cached_observation: bool,
classifications: &[CacheClassification],
nominal_gaps_secs: &[f64],
) -> String {
if classifications.is_empty() {
return "not measured".to_string();
}
if classifications
.iter()
.all(|c| *c == CacheClassification::Invalid)
{
return "not measured".to_string();
}
if !any_cached_observation {
return "does not cache".to_string();
}
let usable: Vec<CacheClassification> = classifications
.iter()
.copied()
.filter(|c| *c != CacheClassification::Invalid)
.collect();
if usable
.iter()
.all(|c| *c == CacheClassification::NotSupported)
{
return "not measured".to_string();
}
ttl_bucket(classifications, nominal_gaps_secs)
}
#[must_use]
pub(crate) fn format_bucket(lo: f64, hi: f64) -> String {
if hi.is_infinite() {
format!("≥{}", format_duration(lo))
} else if lo == 0.0 {
if hi == 0.0 {
"0s".to_string()
} else {
format!("≤{}", format_duration(hi))
}
} else {
format!("({}, {}]", format_duration(lo), format_duration(hi))
}
}
#[must_use]
#[expect(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
fn format_duration(secs: f64) -> String {
if secs == 0.0 {
"0s".to_string()
} else if secs < 60.0 {
format!("{}s", secs.round() as u64)
} else if secs < 3600.0 {
format!("{}m", (secs / 60.0).round() as u64)
} else {
format!("{}h", (secs / 3600.0).round() as u64)
}
}
#[must_use]
pub(crate) fn expected_cached_for_round(
base_cached: u64,
prompt_tokens_round: u64,
prompt_tokens_base: u64,
) -> u64 {
base_cached.saturating_add(prompt_tokens_round.saturating_sub(prompt_tokens_base))
}
#[must_use]
pub(crate) fn verify_pinned(serving_provider: Option<&str>, endpoint: &EndpointInfo) -> bool {
match serving_provider {
Some(p) => p == endpoint.name || p == endpoint.provider_name,
None => false,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn classify_round_thresholds() {
assert_eq!(classify_round(9, 10), CacheClassification::Hit); assert_eq!(classify_round(899, 1000), CacheClassification::Partial); assert_eq!(classify_round(1, 10), CacheClassification::Partial); assert_eq!(classify_round(99, 1000), CacheClassification::Drop); assert_eq!(classify_round(0, 1000), CacheClassification::Drop);
assert_eq!(classify_round(5, 0), CacheClassification::NotSupported);
}
#[test]
fn ttl_bucket_derivation() {
use CacheClassification as C;
let classes = [C::Hit, C::Hit, C::Drop, C::Hit];
let gaps = [0.0, 5.0, 30.0, 120.0];
assert_eq!(ttl_bucket(&classes, &gaps), "(5s, 30s]");
let classes = [C::Hit, C::Hit, C::Hit, C::Hit, C::Hit];
let gaps = [0.0, 5.0, 30.0, 120.0, 1800.0];
assert_eq!(ttl_bucket(&classes, &gaps), "≥30m");
let classes = [C::Hit, C::Hit, C::Hit];
let gaps = [0.0, 5.0, 600.0];
assert_eq!(ttl_bucket(&classes, &gaps), "≥10m");
let classes = [C::Drop, C::Hit, C::Hit];
let gaps = [0.0, 5.0, 30.0];
assert_eq!(ttl_bucket(&classes, &gaps), "immediate drop");
let classes = [C::Hit, C::Drop, C::Hit];
let gaps = [0.0, 5.0, 30.0];
assert_eq!(ttl_bucket(&classes, &gaps), "≤5s");
let classes = [C::NotSupported, C::NotSupported, C::NotSupported];
let gaps = [0.0, 5.0, 30.0];
assert_eq!(ttl_bucket(&classes, &gaps), "does not cache");
let classes = [C::NotSupported, C::Invalid, C::NotSupported];
let gaps = [0.0, 5.0, 30.0];
assert_eq!(ttl_bucket(&classes, &gaps), "does not cache");
let classes = [C::Invalid, C::Invalid, C::Invalid];
let gaps = [0.0, 5.0, 30.0];
assert_eq!(ttl_bucket(&classes, &gaps), "not measured");
assert_eq!(ttl_bucket(&[], &[]), "not measured");
let classes = [C::Hit, C::Invalid, C::Drop, C::Hit];
let gaps = [0.0, 5.0, 30.0, 120.0];
assert_eq!(ttl_bucket(&classes, &gaps), "≤30s");
let classes = [C::Invalid, C::Invalid, C::Drop];
let gaps = [0.0, 5.0, 30.0];
assert_eq!(ttl_bucket(&classes, &gaps), "≤30s");
let classes = [C::Hit, C::Hit, C::Partial, C::Drop];
let gaps = [0.0, 5.0, 30.0, 120.0];
assert_eq!(ttl_bucket(&classes, &gaps), "(30s, 2m]");
let classes = [C::Hit, C::Invalid, C::Invalid];
let gaps = [0.0, 5.0, 30.0];
assert_eq!(ttl_bucket(&classes, &gaps), "≥0s");
let classes = [C::Hit, C::Hit, C::Invalid, C::Invalid];
let gaps = [0.0, 5.0, 30.0, 120.0];
assert_eq!(ttl_bucket(&classes, &gaps), "≥5s");
let classes = [C::Hit, C::Drop, C::Hit];
let gaps = [0.0, 0.0, 30.0];
assert_eq!(ttl_bucket(&classes, &gaps), "immediate drop");
}
#[test]
fn cache_hold_result_outcomes() {
use CacheClassification as C;
assert_eq!(cache_hold_result(true, &[], &[]), "not measured");
assert_eq!(cache_hold_result(false, &[], &[]), "not measured");
let classes = [C::Drop, C::NotSupported, C::Drop];
let gaps = [0.0, 5.0, 30.0];
assert_eq!(cache_hold_result(false, &classes, &gaps), "does not cache");
let classes = [C::Invalid, C::Invalid, C::Invalid];
let gaps = [0.0, 5.0, 30.0];
assert_eq!(cache_hold_result(false, &classes, &gaps), "not measured");
let classes = [C::Invalid, C::Drop, C::Invalid];
let gaps = [0.0, 5.0, 30.0];
assert_eq!(cache_hold_result(false, &classes, &gaps), "does not cache");
let classes = [C::Hit, C::Hit, C::Drop];
let gaps = [0.0, 5.0, 30.0];
assert_eq!(cache_hold_result(true, &classes, &gaps), "(5s, 30s]");
let classes = [C::NotSupported, C::NotSupported, C::NotSupported];
let gaps = [0.0, 5.0, 30.0];
assert_eq!(cache_hold_result(true, &classes, &gaps), "not measured");
}
#[test]
fn format_bucket_ranges() {
assert_eq!(format_bucket(0.0, 0.0), "0s");
assert_eq!(format_bucket(0.0, 5.0), "≤5s");
assert_eq!(format_bucket(0.0, 0.5), "≤1s"); assert_eq!(format_bucket(1800.0, f64::INFINITY), "≥30m");
assert_eq!(format_bucket(0.0, f64::INFINITY), "≥0s");
assert_eq!(format_bucket(300.0, 600.0), "(5m, 10m]");
assert_eq!(format_bucket(5.0, 30.0), "(5s, 30s]");
assert_eq!(format_bucket(3600.0, 7200.0), "(1h, 2h]");
}
#[test]
fn expected_cached_saturates() {
assert_eq!(expected_cached_for_round(1000, 1200, 800), 1400); assert_eq!(expected_cached_for_round(1000, 800, 1200), 1000); assert_eq!(expected_cached_for_round(0, 500, 500), 0);
}
#[test]
fn verify_pinned_matches_name_or_provider_name() {
let endpoint = EndpointInfo {
tag: "acme/fp8".to_string(),
name: "Acme Cloud".to_string(),
provider_name: "Acme".to_string(),
context_length: Some(200_000),
quantization: Some("fp8".to_string()),
status: Some("0".to_string()),
supports_implicit_caching: Some(true),
pricing: None,
};
assert!(verify_pinned(Some("Acme Cloud"), &endpoint)); assert!(verify_pinned(Some("Acme"), &endpoint)); assert!(!verify_pinned(Some("Other"), &endpoint)); assert!(!verify_pinned(None, &endpoint)); assert!(!verify_pinned(Some("acme"), &endpoint));
}
#[test]
fn classification_as_str() {
use CacheClassification as C;
assert_eq!(C::Hit.as_str(), "hit");
assert_eq!(C::Partial.as_str(), "partial");
assert_eq!(C::Drop.as_str(), "drop");
assert_eq!(C::NotSupported.as_str(), "not_supported");
assert_eq!(C::Invalid.as_str(), "invalid");
}
}