collet 0.1.0

Relentless agentic coding orchestrator with zero-drop agent loops
Documentation
use std::path::PathBuf;

use anyhow::Result;
use tracing::info;

use super::model::ModelRegistry;

const LITELLM_URL: &str =
    "https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json";

/// Path to cached registry file: `~/.collet/cache/models.json`.
fn cache_path() -> Option<PathBuf> {
    dirs::home_dir().map(|home| home.join(".collet").join("cache").join("models.json"))
}

/// Load registry from local cache. Returns empty registry if no cache exists.
pub fn load_cached() -> ModelRegistry {
    let Some(path) = cache_path() else {
        return ModelRegistry::empty();
    };
    match std::fs::read(&path) {
        Ok(data) => ModelRegistry::parse(&data),
        Err(_) => ModelRegistry::empty(),
    }
}

/// Load from cache if present and fresh (< 24 hours old), otherwise fetch
/// from LiteLLM and update the cache.  Always returns a valid registry;
/// falls back to the stale cache on network error.
pub fn load_or_fetch_blocking() -> ModelRegistry {
    if let Some(path) = cache_path()
        && let Ok(meta) = std::fs::metadata(&path)
    {
        let age = meta
            .modified()
            .ok()
            .and_then(|m| m.elapsed().ok())
            .unwrap_or(std::time::Duration::MAX);
        if age < std::time::Duration::from_secs(24 * 3600)
            && let Ok(data) = std::fs::read(&path)
        {
            let reg = ModelRegistry::parse(&data);
            if !reg.is_empty() {
                return reg;
            }
        }
    }
    // Cache is absent or stale — attempt a live fetch.
    fetch_and_cache_blocking().unwrap_or_else(|_| load_cached())
}

/// Fetch from GitHub, save to cache, return parsed registry.
///
/// Runs the blocking HTTP request on a dedicated thread to avoid
/// panicking when called inside a tokio async runtime (`#[tokio::main]`).
pub fn fetch_and_cache_blocking() -> Result<ModelRegistry> {
    info!("fetching model registry from {LITELLM_URL}");

    // Spawn a dedicated thread so reqwest::blocking can create its own
    // runtime without conflicting with the outer tokio runtime.
    let bytes = std::thread::spawn(|| -> Result<Vec<u8>> {
        Ok(reqwest::blocking::get(LITELLM_URL)?.bytes()?.to_vec())
    })
    .join()
    .map_err(|_| anyhow::anyhow!("registry fetch thread panicked"))??;

    if let Some(path) = cache_path() {
        if let Some(parent) = path.parent() {
            std::fs::create_dir_all(parent)?;
        }
        std::fs::write(&path, &bytes)?;
        info!("cached model registry to {}", path.display());
    }

    Ok(ModelRegistry::parse(&bytes))
}