tokmesh-core 0.1.5

Core library for tokmesh — session parsing, scanning, aggregation, pricing
Documentation
use super::cache;
use super::litellm::ModelPricing;
use serde::Deserialize;
use std::collections::HashMap;

const CACHE_FILENAME: &str = "pricing-models-dev.json";
const MODELS_DEV_URL: &str = "https://models.dev/api.json";
const MAX_RETRIES: u32 = 3;
const INITIAL_BACKOFF_MS: u64 = 200;
const PER_MILLION: f64 = 1_000_000.0;

#[derive(Deserialize)]
struct Provider {
    #[serde(default)]
    models: HashMap<String, Model>,
}

#[derive(Deserialize)]
struct Model {
    id: Option<String>,
    cost: Option<ModelCost>,
}

#[derive(Deserialize)]
struct ModelCost {
    input: Option<f64>,
    output: Option<f64>,
    cache_read: Option<f64>,
    cache_write: Option<f64>,
}

pub type PricingDataset = HashMap<String, ModelPricing>;

pub fn load_cached() -> Option<PricingDataset> {
    cache::load_cache(CACHE_FILENAME)
}

pub fn load_cached_any_age() -> Option<PricingDataset> {
    cache::load_cache_any_age(CACHE_FILENAME)
}

pub(crate) fn parse_dataset(content: &str) -> Result<PricingDataset, serde_json::Error> {
    let providers: HashMap<String, Provider> = serde_json::from_str(content)?;
    Ok(map_providers(providers))
}

pub async fn fetch() -> Result<PricingDataset, reqwest::Error> {
    fetch_inner(MODELS_DEV_URL, true).await
}

async fn fetch_inner(url: &str, use_cache: bool) -> Result<PricingDataset, reqwest::Error> {
    if use_cache {
        if let Some(cached) = load_cached() {
            return Ok(cached);
        }
    }

    let client = reqwest::Client::builder()
        .timeout(std::time::Duration::from_secs(30))
        .connect_timeout(std::time::Duration::from_secs(10))
        .build()?;

    let mut last_error: Option<reqwest::Error> = None;

    for attempt in 0..MAX_RETRIES {
        match client.get(url).send().await {
            Ok(response) => {
                let status = response.status();

                if status.is_server_error() || status == reqwest::StatusCode::TOO_MANY_REQUESTS {
                    eprintln!(
                        "[tokmesh] models.dev HTTP {} (attempt {}/{})",
                        status,
                        attempt + 1,
                        MAX_RETRIES
                    );
                    if attempt == MAX_RETRIES - 1 {
                        return Err(response.error_for_status().unwrap_err());
                    }
                    let _ = response.bytes().await;
                    tokio::time::sleep(std::time::Duration::from_millis(
                        INITIAL_BACKOFF_MS * (1 << attempt),
                    ))
                    .await;
                    continue;
                }

                if !status.is_success() {
                    eprintln!("[tokmesh] models.dev HTTP {}", status);
                    return Err(response.error_for_status().unwrap_err());
                }

                let content = response.text().await?;
                match parse_dataset(&content) {
                    Ok(data) => {
                        if let Err(e) = cache::save_cache(CACHE_FILENAME, &data) {
                            eprintln!(
                                "[tokmesh] Warning: Failed to cache models.dev pricing at {}: {}",
                                cache::get_cache_path(CACHE_FILENAME).display(),
                                e
                            );
                        }
                        return Ok(data);
                    }
                    Err(e) => {
                        eprintln!("[tokmesh] models.dev JSON parse failed: {}", e);
                        return Ok(HashMap::new());
                    }
                }
            }
            Err(e) => {
                eprintln!(
                    "[tokmesh] models.dev network error (attempt {}/{}): {}",
                    attempt + 1,
                    MAX_RETRIES,
                    crate::pricing::describe_error(&e)
                );
                last_error = Some(e);
                if attempt < MAX_RETRIES - 1 {
                    tokio::time::sleep(std::time::Duration::from_millis(
                        INITIAL_BACKOFF_MS * (1 << attempt),
                    ))
                    .await;
                }
            }
        }
    }

    match last_error {
        Some(e) => Err(e),
        None => Ok(HashMap::new()),
    }
}

fn map_providers(providers: HashMap<String, Provider>) -> PricingDataset {
    let mut result = HashMap::new();

    for (provider_id, provider) in providers {
        for (model_key, model) in provider.models {
            let model_id = model.id.as_deref().unwrap_or(&model_key);
            let Some(pricing) = model.cost.and_then(cost_to_pricing) else {
                continue;
            };
            result.insert(format!("{provider_id}/{model_id}").to_lowercase(), pricing);
        }
    }

    result
}

fn cost_to_pricing(cost: ModelCost) -> Option<ModelPricing> {
    let input = per_token(cost.input?)?;
    let output = per_token(cost.output?)?;

    Some(ModelPricing {
        input_cost_per_token: Some(input),
        output_cost_per_token: Some(output),
        cache_read_input_token_cost: cost.cache_read.and_then(per_token),
        cache_creation_input_token_cost: cost.cache_write.and_then(per_token),
        ..Default::default()
    })
}

fn per_token(value: f64) -> Option<f64> {
    value
        .is_finite()
        .then_some(value)
        .filter(|v| *v >= 0.0)
        .map(|v| v / PER_MILLION)
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::io::{Read, Write};
    use std::net::TcpListener;
    use std::thread;

    fn retryable_status_server(status_line: &'static str) -> String {
        let listener = TcpListener::bind("127.0.0.1:0").unwrap();
        let url = format!("http://{}", listener.local_addr().unwrap());

        thread::spawn(move || {
            for _ in 0..MAX_RETRIES {
                let Ok((mut stream, _)) = listener.accept() else {
                    return;
                };
                let mut buffer = [0; 1024];
                let _ = stream.read(&mut buffer);
                let response =
                    format!("{status_line}\r\nContent-Length: 0\r\nConnection: close\r\n\r\n");
                let _ = stream.write_all(response.as_bytes());
            }
        });

        url
    }

    #[tokio::test]
    async fn fetch_returns_error_after_retryable_http_statuses() {
        let url = retryable_status_server("HTTP/1.1 503 Service Unavailable");

        let result = fetch_inner(&url, false).await;

        assert!(result.is_err());
    }
}