klieo-runlog 3.5.0

Tier 2 observability — RunLog aggregate + replay engine for klieo agents.
Documentation
//! Model-id rewrite helpers used by the price-table lookup fallback chain.
//!
//! The price table keys family aliases with `-x` suffixes and bare model
//! names. Real-world model strings carry per-revision suffixes
//! (`claude-sonnet-4-6`), Anthropic-style date stamps
//! (`claude-3-5-sonnet-20241022` / `claude-3-5-sonnet-2024-11-20`), and
//! Ollama-style colon tags (`qwen2.5:14b`). These helpers strip those
//! suffixes so the same `Rates` entry serves every variant of a model
//! family.
//!
//! See the parent [`crate::pricing`] module docs for the precedence chain
//! these helpers participate in.

/// If `model` ends with `-<digits>`, return the alias with `-x` substituted.
/// Otherwise `None`.
///
/// Note: this never strips date suffixes — those are handled separately by
/// [`strip_trailing_date`] earlier in the precedence chain so a date like
/// `-20241022` is fully removed rather than rewritten to `-x`.
pub(super) fn family_alias(model: &str) -> Option<String> {
    let (head, tail) = model.rsplit_once('-')?;
    if !tail.is_empty() && tail.bytes().all(|b| b.is_ascii_digit()) {
        // Reject pure-digit tails that are actually date stamps; those are
        // handled by strip_trailing_date instead.
        if is_date_like(tail) {
            return None;
        }
        Some(format!("{head}-x"))
    } else {
        None
    }
}

/// If `model` ends with an ISO date `-YYYY-MM-DD` or a compact date
/// `-YYYYMMDD`, return `model` with that suffix removed. Otherwise `None`.
///
/// Char-based detection — no regex dependency.
pub(super) fn strip_trailing_date(model: &str) -> Option<String> {
    // Compact YYYYMMDD: a trailing '-' followed by exactly 8 ascii digits.
    if let Some((head, tail)) = model.rsplit_once('-') {
        if is_compact_date(tail) {
            return Some(head.to_string());
        }
    }
    // ISO YYYY-MM-DD: rsplit on '-' three times and validate the trailing
    // 4/2/2 digit segments. `rsplitn(4, '-')` returns at most 4 elements with
    // the rightmost first; we need exactly 4 so the head is non-empty.
    let parts: Vec<&str> = model.rsplitn(4, '-').collect();
    if parts.len() == 4 {
        let day = parts[0];
        let month = parts[1];
        let year = parts[2];
        let head = parts[3];
        if year.len() == 4
            && month.len() == 2
            && day.len() == 2
            && !head.is_empty()
            && year.bytes().all(|b| b.is_ascii_digit())
            && month.bytes().all(|b| b.is_ascii_digit())
            && day.bytes().all(|b| b.is_ascii_digit())
        {
            return Some(head.to_string());
        }
    }
    None
}

/// True iff `tail` is exactly 8 ASCII digits — the compact-date `YYYYMMDD`
/// shape. Shared between [`strip_trailing_date`] and [`is_date_like`] so the
/// rule lives in one place.
fn is_compact_date(tail: &str) -> bool {
    tail.len() == 8 && tail.bytes().all(|b| b.is_ascii_digit())
}

fn is_date_like(tail: &str) -> bool {
    // Only the compact 8-digit form can be confused with a family-alias
    // version tag; ISO dates contain '-' and never reach family_alias.
    is_compact_date(tail)
}