magi-code 0.77.1

Repository-aware CLI coding agent for terminal work
Documentation
use super::{
    models_dev::effective_custom_provider_models_dev_namespace,
    types::{ModelCatalogEntry, ModelId},
};
use crate::{
    config::{CustomProviderConfig, McPaths, read_settings},
    http_body::{DEFAULT_BOUNDED_BODY_MAX_BYTES, read_bounded_file_to_string},
};
use chrono::{DateTime, Duration, Utc};
use serde::{Deserialize, Serialize};
use std::path::PathBuf;

// Cache schema 8 invalidates catalogs built before Codex service-tier metadata. Enrichment version 3 invalidates catalogs built before provider reasoning metadata parsing.
pub(super) const CACHE_SCHEMA_VERSION: u32 = 8;
pub(super) const ENRICHMENT_VERSION: u32 = 3;

pub(super) const CATALOG_TTL_HOURS: i64 = 24;

#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
pub(super) struct CatalogCacheFingerprint {
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub(super) enrichment_version: Option<u32>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub(super) models_dev_provider: Option<String>,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub(super) extra_models: Vec<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub(super) codex_client_version: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub(super) struct CachedCatalog {
    pub(super) schema_version: u32,
    pub(super) provider: String,
    #[serde(default)]
    pub(super) cache_fingerprint: CatalogCacheFingerprint,
    pub(super) fetched_at: DateTime<Utc>,
    pub(super) expires_at: DateTime<Utc>,
    pub(super) entries: Vec<ModelCatalogEntry>,
}

pub(crate) fn catalog_cache_path(paths: &McPaths, provider: &str) -> Option<PathBuf> {
    if provider
        .chars()
        .all(|ch| ch.is_ascii_lowercase() || ch.is_ascii_digit() || ch == '-')
    {
        Some(
            paths
                .cache
                .join("model-catalog")
                .join(format!("{provider}.json")),
        )
    } else {
        None
    }
}

pub(super) fn built_in_catalog_cache_fingerprint(provider: &str) -> CatalogCacheFingerprint {
    CatalogCacheFingerprint {
        enrichment_version: Some(ENRICHMENT_VERSION),
        codex_client_version: (provider == crate::providers::OPENAI_CODEX_PROVIDER)
            .then(|| crate::providers::CODEX_MODEL_CATALOG_CLIENT_VERSION.to_string()),
        ..Default::default()
    }
}

pub(super) fn read_catalog_cache(paths: &McPaths, provider: &str) -> Option<CachedCatalog> {
    read_catalog_cache_with_fingerprint(
        paths,
        provider,
        &built_in_catalog_cache_fingerprint(provider),
    )
}

pub(super) fn read_custom_provider_catalog_cache(
    paths: &McPaths,
    provider: &str,
    custom: &CustomProviderConfig,
) -> Option<CachedCatalog> {
    let fingerprint = custom_provider_catalog_cache_fingerprint(provider, custom).ok()?;
    read_catalog_cache_with_fingerprint(paths, provider, &fingerprint)
}

pub(super) fn read_catalog_cache_for_configured_provider(
    paths: &McPaths,
    provider: &str,
) -> Option<CachedCatalog> {
    // These are the only built-in catalogs. Keep cache validation separate from provider
    // transport implementations; provider-specific identity metadata is populated above.
    if matches!(provider, "openai-codex" | "anthropic") {
        return read_catalog_cache(paths, provider);
    }
    let settings = read_settings(paths).ok()?;
    let custom = settings.custom_providers.get(provider)?;
    read_custom_provider_catalog_cache(paths, provider, custom)
}

pub(super) fn metadata_entries_for_provider(
    paths: &McPaths,
    provider: &str,
) -> Option<Vec<ModelCatalogEntry>> {
    read_catalog_cache_for_configured_provider(paths, provider).map(|cache| cache.entries)
}

pub(super) fn read_catalog_cache_with_fingerprint(
    paths: &McPaths,
    provider: &str,
    expected_fingerprint: &CatalogCacheFingerprint,
) -> Option<CachedCatalog> {
    let path = catalog_cache_path(paths, provider)?;
    let text = read_bounded_file_to_string(path, DEFAULT_BOUNDED_BODY_MAX_BYTES)?;
    let cache: CachedCatalog = serde_json::from_str(&text).ok()?;
    if cache.schema_version != CACHE_SCHEMA_VERSION
        || cache.provider != provider
        || cache.cache_fingerprint != *expected_fingerprint
        || cache.entries.is_empty()
    {
        return None;
    }
    if cache.entries.iter().any(|entry| {
        entry.provider != provider
            || ModelId::from_parts(provider, &entry.model)
                .map(|model_id| entry.id != model_id.to_string())
                .unwrap_or(true)
    }) {
        return None;
    }
    Some(cache)
}

pub(super) fn custom_provider_catalog_cache_fingerprint(
    provider: &str,
    custom: &CustomProviderConfig,
) -> anyhow::Result<CatalogCacheFingerprint> {
    Ok(CatalogCacheFingerprint {
        enrichment_version: Some(ENRICHMENT_VERSION),
        models_dev_provider: Some(
            effective_custom_provider_models_dev_namespace(provider, custom).to_string(),
        ),
        extra_models: crate::config::normalized_extra_models(&custom.extra_models)?,
        ..Default::default()
    })
}

pub(crate) fn write_catalog_cache(
    paths: &McPaths,
    provider: &str,
    entries: &[ModelCatalogEntry],
) -> anyhow::Result<()> {
    write_catalog_cache_with_fingerprint(
        paths,
        provider,
        &built_in_catalog_cache_fingerprint(provider),
        entries,
    )
}

#[cfg(test)]
pub(crate) fn write_catalog_cache_for_configured_provider(
    paths: &McPaths,
    provider: &str,
    entries: &[ModelCatalogEntry],
) -> anyhow::Result<()> {
    if provider == "openai-codex" {
        return write_catalog_cache(paths, provider, entries);
    }
    let settings = read_settings(paths)?;
    let Some(custom) = settings.custom_providers.get(provider) else {
        return write_catalog_cache(paths, provider, entries);
    };
    write_custom_provider_catalog_cache(paths, provider, custom, entries)
}

pub(super) fn write_custom_provider_catalog_cache(
    paths: &McPaths,
    provider: &str,
    custom: &CustomProviderConfig,
    entries: &[ModelCatalogEntry],
) -> anyhow::Result<()> {
    let fingerprint = custom_provider_catalog_cache_fingerprint(provider, custom)?;
    write_catalog_cache_with_fingerprint(paths, provider, &fingerprint, entries)
}

pub(super) fn write_catalog_cache_with_fingerprint(
    paths: &McPaths,
    provider: &str,
    cache_fingerprint: &CatalogCacheFingerprint,
    entries: &[ModelCatalogEntry],
) -> anyhow::Result<()> {
    let path = catalog_cache_path(paths, provider)
        .ok_or_else(|| anyhow::anyhow!("invalid model catalog provider cache name"))?;
    let now = Utc::now();
    let cache = CachedCatalog {
        schema_version: CACHE_SCHEMA_VERSION,
        provider: provider.to_string(),
        cache_fingerprint: cache_fingerprint.clone(),
        fetched_at: now,
        expires_at: now + Duration::hours(CATALOG_TTL_HOURS),
        entries: entries.to_vec(),
    };
    crate::persistence::atomic_write(&path, serde_json::to_string_pretty(&cache)?.as_bytes())
}