Skip to main content

systemprompt_cli/commands/analytics/costs/
mod.rs

1//! Cost analytics: spend summary, trends over time, and breakdown by model or
2//! agent.
3//!
4//! Defines the [`CostsCommands`] subcommand tree and the typed output shapes
5//! ([`CostSummaryOutput`], [`CostTrendsOutput`], [`CostBreakdownOutput`])
6//! rendered by the `analytics costs` commands.
7//!
8//! Copyright (c) systemprompt.io — Business Source License 1.1.
9//! See <https://systemprompt.io> for licensing details.
10
11mod breakdown;
12pub mod summary;
13mod trends;
14
15use anyhow::Result;
16use clap::{Args, Subcommand};
17use schemars::JsonSchema;
18use serde::{Deserialize, Serialize};
19
20use crate::context::CommandContext;
21use crate::shared::render_result;
22
23/// `analytics costs` with no subcommand runs the summary.
24///
25/// That keeps the bare command runnable rather than a usage error. The
26/// summary's own arguments are flattened here, which leaves their clap defaults
27/// the single source of truth.
28#[derive(Debug, Args)]
29pub struct CostsArgs {
30    #[command(subcommand)]
31    pub cmd: Option<CostsCommands>,
32
33    #[command(flatten)]
34    pub summary: summary::SummaryArgs,
35}
36
37#[derive(Debug, Subcommand)]
38pub enum CostsCommands {
39    #[command(about = "Cost summary", alias = "list")]
40    Summary(summary::SummaryArgs),
41
42    #[command(about = "Cost trends over time")]
43    Trends(trends::TrendsArgs),
44
45    #[command(about = "Cost breakdown by model/agent")]
46    Breakdown(breakdown::BreakdownArgs),
47}
48
49#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
50pub struct CostSummaryOutput {
51    pub period: String,
52    pub total_cost_microdollars: i64,
53    pub total_requests: i64,
54    pub total_tokens: i64,
55    pub reasoning_tokens: i64,
56    pub cache_read_tokens: i64,
57    pub cache_creation_tokens: i64,
58    pub avg_cost_per_request_microdollars: f64,
59    pub change_percent: Option<f64>,
60    pub auto_widened_to: Option<String>,
61}
62
63#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
64pub struct CostTrendPoint {
65    pub timestamp: String,
66    pub cost_microdollars: i64,
67    pub request_count: i64,
68    pub tokens: i64,
69}
70
71#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
72pub struct CostTrendsOutput {
73    pub period: String,
74    pub group_by: String,
75    pub points: Vec<CostTrendPoint>,
76    pub total_cost_microdollars: i64,
77}
78
79#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
80pub struct CostBreakdownItem {
81    pub name: String,
82    pub cost_microdollars: i64,
83    pub request_count: i64,
84    pub tokens: i64,
85    pub percentage: f64,
86}
87
88#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
89pub struct CostBreakdownOutput {
90    pub period: String,
91    pub breakdown_by: String,
92    pub items: Vec<CostBreakdownItem>,
93    pub total_cost_microdollars: i64,
94}
95
96pub async fn execute(args: CostsArgs, ctx: &CommandContext) -> Result<()> {
97    let db_ctx = ctx.database().await?;
98    match args.cmd.unwrap_or(CostsCommands::Summary(args.summary)) {
99        CostsCommands::Summary(args) => {
100            let result = summary::execute_with_pool(args, &db_ctx, &ctx.cli).await?;
101            render_result(&result, &ctx.cli);
102            Ok(())
103        },
104        CostsCommands::Trends(args) => {
105            let result = trends::execute_with_pool(args, &db_ctx, &ctx.cli).await?;
106            render_result(&result, &ctx.cli);
107            Ok(())
108        },
109        CostsCommands::Breakdown(args) => {
110            let result = breakdown::execute_with_pool(args, &db_ctx, &ctx.cli).await?;
111            render_result(&result, &ctx.cli);
112            Ok(())
113        },
114    }
115}