pondus 0.5.0

Opinionated AI model benchmark aggregator
use anyhow::Result;
use serde::Deserialize;
use std::collections::HashMap;
use std::path::PathBuf;

#[derive(Debug, Deserialize, Default)]
pub struct Config {
    #[serde(default)]
    pub sources: HashMap<String, SourceConfig>,
    #[serde(default)]
    pub cache: CacheConfig,
    #[serde(default)]
    pub alias: AliasConfig,
    #[serde(skip)]
    pub aa_api_key: Option<String>,
}

#[derive(Debug, Deserialize, Default)]
pub struct SourceConfig {
    pub api_key: Option<String>,
    pub agent_browser_path: Option<String>,
}

#[derive(Debug, Deserialize)]
pub struct CacheConfig {
    #[serde(default = "default_ttl")]
    pub ttl_hours: u64,
}

impl Default for CacheConfig {
    fn default() -> Self {
        Self {
            ttl_hours: default_ttl(),
        }
    }
}

fn default_ttl() -> u64 {
    24
}

#[derive(Debug, Deserialize, Default)]
pub struct AliasConfig {
    pub path: Option<String>,
}

impl Config {
    pub fn load() -> Result<Self> {
        let path = config_path();
        let mut config = if path.exists() {
            let content = std::fs::read_to_string(&path)?;
            let mut config: Config = toml::from_str(&content)?;
            config.aa_api_key = aa_api_key_from_content(&content);
            config
        } else {
            Config::default()
        };

        if let Ok(env_api_key) = std::env::var("AA_API_KEY")
            && !env_api_key.trim().is_empty()
        {
            config.aa_api_key = Some(env_api_key);
        }

        Ok(config)
    }

    pub fn agent_browser_path(&self) -> &str {
        self.sources
            .get("seal")
            .or_else(|| self.sources.get("swe-rebench"))
            .and_then(|s| s.agent_browser_path.as_deref())
            .unwrap_or("agent-browser")
    }

    pub fn aa_api_key(&self) -> Option<&str> {
        self.aa_api_key.as_deref().or_else(|| {
            self.sources
                .get("artificial-analysis")
                .or_else(|| self.sources.get("artificial_analysis"))
                .and_then(|s| s.api_key.as_deref())
        })
    }
}

fn aa_api_key_from_content(content: &str) -> Option<String> {
    let value: toml::Value = toml::from_str(content).ok()?;
    value
        .get("artificial-analysis")
        .and_then(|v| v.get("api_key"))
        .and_then(|v| v.as_str())
        .map(ToOwned::to_owned)
}

fn config_path() -> PathBuf {
    dirs::config_dir()
        .unwrap_or_else(|| PathBuf::from("."))
        .join("pondus")
        .join("config.toml")
}