terraphim_usage 1.20.3

Usage tracking and analytics for Terraphim AI
Documentation
//! LLM usage metering and pricing for Terraphim AI.
//!
//! Tracks token consumption across providers (OpenRouter, Ollama, …), applies
//! per-model pricing tables, and persists usage records. Optional `cli` and
//! `providers` feature flags unlock the reporting CLI and provider adapters.
#[cfg(feature = "cli")]
pub mod cli;
pub mod formatter;
pub mod pricing;
#[cfg(feature = "providers")]
pub mod providers;
#[cfg(feature = "persistence")]
pub mod store;

use serde::{Deserialize, Serialize};
use thiserror::Error;

#[derive(Error, Debug)]
pub enum UsageError {
    #[error("Provider {0} not found")]
    ProviderNotFound(String),

    #[error("Failed to fetch usage from {provider}: {source}")]
    FetchFailed {
        provider: String,
        source: Box<dyn std::error::Error + Send + Sync>,
    },

    #[error("Authentication failed for {provider}: {message}")]
    AuthFailed { provider: String, message: String },

    #[error("Rate limit exceeded for {provider}")]
    RateLimited { provider: String },

    #[error("Storage error: {0}")]
    StorageError(String),

    #[error("Serialization error: {0}")]
    SerializationError(#[from] serde_json::Error),
}

pub type Result<T> = std::result::Result<T, UsageError>;

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProviderUsage {
    pub provider_id: String,
    pub display_name: String,
    pub plan: Option<String>,
    pub lines: Vec<MetricLine>,
    pub fetched_at: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum MetricLine {
    Text {
        label: String,
        value: String,
        color: Option<String>,
        subtitle: Option<String>,
    },
    Progress {
        label: String,
        used: f64,
        limit: f64,
        format: ProgressFormat,
        resets_at: Option<String>,
        period_duration_ms: Option<u64>,
        color: Option<String>,
    },
    Badge {
        label: String,
        text: String,
        color: Option<String>,
        subtitle: Option<String>,
    },
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum ProgressFormat {
    Percent,
    Dollars,
    Count { suffix: String },
}

pub trait UsageProvider: Send + Sync {
    fn id(&self) -> &str;
    fn display_name(&self) -> &str;
    fn fetch_usage(
        &self,
    ) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<ProviderUsage>> + Send + '_>>;
}

pub struct UsageRegistry {
    providers: std::collections::HashMap<String, Box<dyn UsageProvider>>,
}

impl UsageRegistry {
    pub fn new() -> Self {
        Self {
            providers: std::collections::HashMap::new(),
        }
    }

    pub fn register(&mut self, provider: Box<dyn UsageProvider>) {
        let id = provider.id().to_string();
        self.providers.insert(id, provider);
    }

    pub fn get(&self, id: &str) -> Option<&dyn UsageProvider> {
        self.providers.get(id).map(|p| p.as_ref())
    }

    pub fn all(&self) -> Vec<&dyn UsageProvider> {
        self.providers.values().map(|p| p.as_ref()).collect()
    }

    pub fn ids(&self) -> Vec<&str> {
        self.providers.keys().map(|k| k.as_str()).collect()
    }
}

impl Default for UsageRegistry {
    fn default() -> Self {
        Self::new()
    }
}