use colored::*;
use cstats_core::api::{
AnthropicUsageStats, ApiClient, CostBreakdown, RateLimitInfo, UsageSummary,
};
use cstats_core::{config::Config, Result};
use crate::{OutputFormat, StatsArgs, StatsPeriod};
pub async fn stats_command(args: StatsArgs, format: OutputFormat, config: Config) -> Result<()> {
if !config.has_anthropic_api_key() {
eprintln!("{}", "Error: No Anthropic API key configured".red());
eprintln!("Please configure your API key by either:");
eprintln!(" 1. Running: cstats init");
eprintln!(" 2. Running: cstats config set-api-key");
eprintln!(" 3. Setting: export ANTHROPIC_API_KEY=\"sk-ant-...\"");
return Ok(());
}
let client = if args.no_cache {
ApiClient::new(config.api)?
} else {
ApiClient::from_config_with_cache(config).await?
};
if client.anthropic().is_none() {
eprintln!("{}", "Error: Anthropic API client not configured".red());
return Ok(());
}
match args.period {
StatsPeriod::Daily => {
let stats = client.fetch_daily_usage_stats().await?;
display_usage_stats(&stats, "Daily Usage (Last 24 hours)", format, args.detailed)?;
}
StatsPeriod::Weekly => {
let stats = client.fetch_weekly_usage_stats().await?;
display_usage_stats(&stats, "Weekly Usage (Last 7 days)", format, args.detailed)?;
}
StatsPeriod::Monthly => {
let stats = client.fetch_monthly_usage_stats().await?;
display_usage_stats(
&stats,
"Monthly Usage (Last 30 days)",
format,
args.detailed,
)?;
}
StatsPeriod::Summary => {
let summary = client.get_usage_summary().await?;
display_usage_summary(&summary, format)?;
}
}
if args.rate_limit {
match client.fetch_rate_limit_info().await {
Ok(rate_limit) => display_rate_limit(&rate_limit, format)?,
Err(e) => eprintln!(
"{} {}",
"Warning: Could not fetch rate limit info:".yellow(),
e
),
}
}
if args.billing {
match client.fetch_current_month_billing().await {
Ok(billing) => display_billing(&billing, format)?,
Err(e) => eprintln!(
"{} {}",
"Warning: Could not fetch billing info:".yellow(),
e
),
}
}
Ok(())
}
fn display_usage_stats(
stats: &AnthropicUsageStats,
title: &str,
format: OutputFormat,
detailed: bool,
) -> Result<()> {
match format {
OutputFormat::Json => {
println!("{}", serde_json::to_string_pretty(stats)?);
}
OutputFormat::Yaml => {
println!("{}", serde_json::to_string_pretty(stats)?);
}
OutputFormat::Text => {
println!("\n{} {}", "📊".bold(), title.bold().cyan());
println!(
"Tokens: {} ({}↓ {}↑) | Cost: ${:.4}",
format_number(stats.token_usage.total_tokens).yellow(),
format_number(stats.token_usage.input_tokens),
format_number(stats.token_usage.output_tokens),
stats.costs.total_cost_usd
);
let success_rate = if stats.api_calls.total_calls > 0 {
(stats.api_calls.successful_calls as f64 / stats.api_calls.total_calls as f64)
* 100.0
} else {
0.0
};
if stats.api_calls.total_calls > 0 {
println!(
"Calls: {} ({} ok, {} fail) | {:.1}% success | {}ms avg",
stats.api_calls.total_calls,
stats.api_calls.successful_calls.to_string().green(),
stats.api_calls.failed_calls.to_string().red(),
success_rate,
stats.api_calls.avg_response_time_ms as u64
);
}
if detailed && !stats.model_usage.is_empty() {
println!("\nModels:");
for model in &stats.model_usage {
println!(
" {} → {} requests, {} tokens, ${:.4}",
model.model.yellow(),
model.requests,
format_number(model.tokens.total_tokens),
model.cost.cost_usd
);
}
}
}
}
Ok(())
}
fn display_usage_summary(summary: &UsageSummary, format: OutputFormat) -> Result<()> {
match format {
OutputFormat::Json => {
println!("{}", serde_json::to_string_pretty(summary)?);
}
OutputFormat::Yaml => {
println!("{}", serde_json::to_string_pretty(summary)?);
}
OutputFormat::Text => {
println!("\n{} {}", "📊".bold(), "Usage Summary".bold().cyan());
println!(
"Daily: {} tokens | {} calls | ${:.4}",
format_number(summary.daily.token_usage.total_tokens).yellow(),
summary.daily.api_calls.total_calls,
summary.daily.costs.total_cost_usd
);
println!(
"Weekly: {} tokens | {} calls | ${:.4}",
format_number(summary.weekly.token_usage.total_tokens).yellow(),
summary.weekly.api_calls.total_calls,
summary.weekly.costs.total_cost_usd
);
println!(
"Monthly: {} tokens | {} calls | ${:.4} (est: ${:.4}/mo)",
format_number(summary.monthly.token_usage.total_tokens).yellow(),
summary.monthly.api_calls.total_calls,
summary.monthly.costs.total_cost_usd,
summary.monthly.costs.estimated_monthly_cost_usd
);
}
}
Ok(())
}
fn display_rate_limit(rate_limit: &RateLimitInfo, format: OutputFormat) -> Result<()> {
match format {
OutputFormat::Json => {
println!("{}", serde_json::to_string_pretty(rate_limit)?);
}
OutputFormat::Yaml => {
println!("{}", serde_json::to_string_pretty(rate_limit)?);
}
OutputFormat::Text => {
print!("\n{} Rate Limits: ", "⏱️ ".bold());
print!(
"{}/{} req/min",
rate_limit.requests_remaining, rate_limit.requests_per_minute
);
if let Some(tpm) = rate_limit.tokens_per_minute {
print!(" | {}", format_number(tpm as u64));
if let Some(remaining) = rate_limit.tokens_remaining {
print!("/{} tokens", format_number(remaining as u64));
} else {
print!(" tokens/min");
}
}
println!();
}
}
Ok(())
}
fn display_billing(billing: &CostBreakdown, format: OutputFormat) -> Result<()> {
match format {
OutputFormat::Json => {
println!("{}", serde_json::to_string_pretty(billing)?);
}
OutputFormat::Yaml => {
println!("{}", serde_json::to_string_pretty(billing)?);
}
OutputFormat::Text => {
println!(
"\n{} Billing: ${:.4} current | ${:.4} estimated/mo",
"💳".bold(),
billing.total_cost_usd,
billing.estimated_monthly_cost_usd
);
if !billing.by_model.is_empty() {
for (model, cost) in &billing.by_model {
println!(
" {} → ${:.4} (in: ${:.4}, out: ${:.4})",
model.yellow(),
cost.cost_usd,
cost.input_cost_usd,
cost.output_cost_usd
);
}
}
}
}
Ok(())
}
fn format_number(n: u64) -> String {
let s = n.to_string();
let mut result = String::new();
for (i, c) in s.chars().rev().enumerate() {
if i > 0 && i % 3 == 0 {
result.push(',');
}
result.push(c);
}
result.chars().rev().collect()
}