llm-cost-ops 0.1.1

Core library for cost operations on LLM deployments
Documentation
// Report types and generation

use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};

use super::{ExportData, ExportResult};

/// Report types
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ReportType {
    Cost,
    Usage,
    Forecast,
    Audit,
    Budget,
    Summary,
}

impl std::fmt::Display for ReportType {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            ReportType::Cost => write!(f, "cost"),
            ReportType::Usage => write!(f, "usage"),
            ReportType::Forecast => write!(f, "forecast"),
            ReportType::Audit => write!(f, "audit"),
            ReportType::Budget => write!(f, "budget"),
            ReportType::Summary => write!(f, "summary"),
        }
    }
}

/// Report request
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReportRequest {
    pub report_type: ReportType,
    pub start_date: DateTime<Utc>,
    pub end_date: DateTime<Utc>,
    pub organization_id: Option<String>,
    pub filters: ReportFilters,
}

#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ReportFilters {
    pub provider: Option<String>,
    pub model: Option<String>,
    pub user_id: Option<String>,
    pub resource_type: Option<String>,
}

/// Report response
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReportResponse {
    pub id: String,
    pub report_type: ReportType,
    pub generated_at: DateTime<Utc>,
    pub data: ExportData,
    pub summary: ReportSummary,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReportSummary {
    pub total_records: usize,
    pub date_range: DateRange,
    pub aggregates: std::collections::HashMap<String, serde_json::Value>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DateRange {
    pub start: DateTime<Utc>,
    pub end: DateTime<Utc>,
}

/// Cost report
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CostReport {
    pub provider: String,
    pub model: String,
    pub organization_id: String,
    pub total_cost: f64,
    pub currency: String,
    pub period_start: DateTime<Utc>,
    pub period_end: DateTime<Utc>,
}

/// Usage report
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UsageReport {
    pub provider: String,
    pub model: String,
    pub organization_id: String,
    pub total_tokens: u64,
    pub total_requests: u64,
    pub period_start: DateTime<Utc>,
    pub period_end: DateTime<Utc>,
}

/// Forecast report
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ForecastReport {
    pub forecast_date: DateTime<Utc>,
    pub predicted_cost: f64,
    pub confidence_interval: (f64, f64),
    pub trend: String,
}

/// Audit report
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AuditReport {
    pub event_type: String,
    pub user_id: String,
    pub resource_type: String,
    pub action: String,
    pub timestamp: DateTime<Utc>,
    pub status: String,
}

/// Report generator
pub struct ReportGenerator;

impl ReportGenerator {
    pub fn new() -> Self {
        Self
    }

    pub async fn generate(&self, request: ReportRequest) -> ExportResult<ReportResponse> {
        match request.report_type {
            ReportType::Cost => self.generate_cost_report(request).await,
            ReportType::Usage => self.generate_usage_report(request).await,
            ReportType::Forecast => self.generate_forecast_report(request).await,
            ReportType::Audit => self.generate_audit_report(request).await,
            ReportType::Budget => self.generate_budget_report(request).await,
            ReportType::Summary => self.generate_summary_report(request).await,
        }
    }

    async fn generate_cost_report(&self, request: ReportRequest) -> ExportResult<ReportResponse> {
        let mut data = ExportData::new(vec![
            "Date".to_string(),
            "Provider".to_string(),
            "Model".to_string(),
            "Organization".to_string(),
            "Cost (USD)".to_string(),
            "Requests".to_string(),
        ]);

        // Add metadata
        data.add_metadata("report_type", "cost");
        data.add_metadata("generated_at", Utc::now().to_rfc3339());

        let summary = ReportSummary {
            total_records: data.row_count(),
            date_range: DateRange {
                start: request.start_date,
                end: request.end_date,
            },
            aggregates: std::collections::HashMap::new(),
        };

        Ok(ReportResponse {
            id: uuid::Uuid::new_v4().to_string(),
            report_type: ReportType::Cost,
            generated_at: Utc::now(),
            data,
            summary,
        })
    }

    async fn generate_usage_report(&self, request: ReportRequest) -> ExportResult<ReportResponse> {
        let mut data = ExportData::new(vec![
            "Date".to_string(),
            "Provider".to_string(),
            "Model".to_string(),
            "Total Tokens".to_string(),
            "Input Tokens".to_string(),
            "Output Tokens".to_string(),
            "Requests".to_string(),
        ]);

        data.add_metadata("report_type", "usage");
        data.add_metadata("generated_at", Utc::now().to_rfc3339());

        let summary = ReportSummary {
            total_records: data.row_count(),
            date_range: DateRange {
                start: request.start_date,
                end: request.end_date,
            },
            aggregates: std::collections::HashMap::new(),
        };

        Ok(ReportResponse {
            id: uuid::Uuid::new_v4().to_string(),
            report_type: ReportType::Usage,
            generated_at: Utc::now(),
            data,
            summary,
        })
    }

    async fn generate_forecast_report(
        &self,
        request: ReportRequest,
    ) -> ExportResult<ReportResponse> {
        let mut data = ExportData::new(vec![
            "Forecast Date".to_string(),
            "Predicted Cost".to_string(),
            "Lower Bound".to_string(),
            "Upper Bound".to_string(),
            "Confidence".to_string(),
            "Trend".to_string(),
        ]);

        data.add_metadata("report_type", "forecast");
        data.add_metadata("generated_at", Utc::now().to_rfc3339());

        let summary = ReportSummary {
            total_records: data.row_count(),
            date_range: DateRange {
                start: request.start_date,
                end: request.end_date,
            },
            aggregates: std::collections::HashMap::new(),
        };

        Ok(ReportResponse {
            id: uuid::Uuid::new_v4().to_string(),
            report_type: ReportType::Forecast,
            generated_at: Utc::now(),
            data,
            summary,
        })
    }

    async fn generate_audit_report(&self, request: ReportRequest) -> ExportResult<ReportResponse> {
        let mut data = ExportData::new(vec![
            "Timestamp".to_string(),
            "Event Type".to_string(),
            "User ID".to_string(),
            "Resource Type".to_string(),
            "Action".to_string(),
            "Status".to_string(),
            "IP Address".to_string(),
        ]);

        data.add_metadata("report_type", "audit");
        data.add_metadata("generated_at", Utc::now().to_rfc3339());

        let summary = ReportSummary {
            total_records: data.row_count(),
            date_range: DateRange {
                start: request.start_date,
                end: request.end_date,
            },
            aggregates: std::collections::HashMap::new(),
        };

        Ok(ReportResponse {
            id: uuid::Uuid::new_v4().to_string(),
            report_type: ReportType::Audit,
            generated_at: Utc::now(),
            data,
            summary,
        })
    }

    async fn generate_budget_report(&self, request: ReportRequest) -> ExportResult<ReportResponse> {
        let mut data = ExportData::new(vec![
            "Organization".to_string(),
            "Budget Limit".to_string(),
            "Current Spend".to_string(),
            "Remaining".to_string(),
            "Utilization %".to_string(),
            "Status".to_string(),
        ]);

        data.add_metadata("report_type", "budget");
        data.add_metadata("generated_at", Utc::now().to_rfc3339());

        let summary = ReportSummary {
            total_records: data.row_count(),
            date_range: DateRange {
                start: request.start_date,
                end: request.end_date,
            },
            aggregates: std::collections::HashMap::new(),
        };

        Ok(ReportResponse {
            id: uuid::Uuid::new_v4().to_string(),
            report_type: ReportType::Budget,
            generated_at: Utc::now(),
            data,
            summary,
        })
    }

    async fn generate_summary_report(
        &self,
        request: ReportRequest,
    ) -> ExportResult<ReportResponse> {
        let mut data = ExportData::new(vec![
            "Metric".to_string(),
            "Value".to_string(),
            "Change".to_string(),
            "Trend".to_string(),
        ]);

        data.add_metadata("report_type", "summary");
        data.add_metadata("generated_at", Utc::now().to_rfc3339());

        let summary = ReportSummary {
            total_records: data.row_count(),
            date_range: DateRange {
                start: request.start_date,
                end: request.end_date,
            },
            aggregates: std::collections::HashMap::new(),
        };

        Ok(ReportResponse {
            id: uuid::Uuid::new_v4().to_string(),
            report_type: ReportType::Summary,
            generated_at: Utc::now(),
            data,
            summary,
        })
    }
}

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

#[cfg(test)]
mod tests {
    use super::*;

    #[tokio::test]
    async fn test_generate_cost_report() {
        let generator = ReportGenerator::new();
        let request = ReportRequest {
            report_type: ReportType::Cost,
            start_date: Utc::now() - chrono::Duration::days(7),
            end_date: Utc::now(),
            organization_id: Some("test_org".to_string()),
            filters: ReportFilters::default(),
        };

        let result = generator.generate(request).await;
        assert!(result.is_ok());

        let response = result.unwrap();
        assert_eq!(response.report_type, ReportType::Cost);
    }

    #[tokio::test]
    async fn test_generate_usage_report() {
        let generator = ReportGenerator::new();
        let request = ReportRequest {
            report_type: ReportType::Usage,
            start_date: Utc::now() - chrono::Duration::days(30),
            end_date: Utc::now(),
            organization_id: None,
            filters: ReportFilters::default(),
        };

        let result = generator.generate(request).await;
        assert!(result.is_ok());
    }
}