Skip to main content

systemprompt_cli/commands/analytics/requests/
mod.rs

1//! AI request analytics: aggregate stats, individual listings, trends, and
2//! model usage.
3//!
4//! Defines the [`RequestsCommands`] subcommand tree and the typed output shapes
5//! ([`RequestStatsOutput`], [`RequestTrendsOutput`], [`ModelsOutput`]) rendered
6//! by the `analytics requests` commands.
7//!
8//! Copyright (c) systemprompt.io — Business Source License 1.1.
9//! See <https://systemprompt.io> for licensing details.
10
11mod list;
12mod models;
13mod stats;
14mod trends;
15
16use anyhow::Result;
17use clap::{Args, Subcommand};
18use schemars::JsonSchema;
19use serde::{Deserialize, Serialize};
20
21use crate::context::CommandContext;
22use crate::shared::render_result;
23
24/// `analytics requests` with no subcommand runs the stats aggregate.
25///
26/// That keeps the bare command runnable rather than a usage error. The stats
27/// arguments are flattened here, which leaves their clap defaults the single
28/// source of truth.
29#[derive(Debug, Args)]
30pub struct RequestsArgs {
31    #[command(subcommand)]
32    pub cmd: Option<RequestsCommands>,
33
34    #[command(flatten)]
35    pub stats: stats::StatsArgs,
36}
37
38#[derive(Debug, Subcommand)]
39pub enum RequestsCommands {
40    #[command(
41        about = "Dashboard request metrics: time range, model filter, cache-hit rate, CSV export. For a quick operational aggregate, use `infra logs request stats`"
42    )]
43    Stats(stats::StatsArgs),
44
45    #[command(
46        about = "Dashboard list of AI requests with time range, model filter, and CSV export. For a quick operational list, use `infra logs request list`"
47    )]
48    List(list::ListArgs),
49
50    #[command(about = "AI request trends over time")]
51    Trends(trends::TrendsArgs),
52
53    #[command(about = "Model usage breakdown")]
54    Models(models::ModelsArgs),
55}
56
57#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
58pub struct RequestStatsOutput {
59    pub period: String,
60    pub total_requests: i64,
61    pub total_tokens: i64,
62    pub input_tokens: i64,
63    pub output_tokens: i64,
64    pub reasoning_tokens: i64,
65    pub cache_read_tokens: i64,
66    pub cache_creation_tokens: i64,
67    pub total_cost_microdollars: i64,
68    pub avg_latency_ms: i64,
69    pub cache_hit_rate: f64,
70}
71
72#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
73pub struct RequestTrendPoint {
74    pub timestamp: String,
75    pub request_count: i64,
76    pub total_tokens: i64,
77    pub cost_microdollars: i64,
78    pub avg_latency_ms: i64,
79}
80
81#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
82pub struct RequestTrendsOutput {
83    pub period: String,
84    pub group_by: String,
85    pub points: Vec<RequestTrendPoint>,
86}
87
88#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
89pub struct ModelUsageRow {
90    pub provider: String,
91    pub model: String,
92    pub request_count: i64,
93    pub total_tokens: i64,
94    pub total_cost_microdollars: i64,
95    pub avg_latency_ms: i64,
96    pub percentage: f64,
97}
98
99#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
100pub struct ModelsOutput {
101    pub period: String,
102    pub models: Vec<ModelUsageRow>,
103    pub total_requests: i64,
104}
105
106pub async fn execute(args: RequestsArgs, ctx: &CommandContext) -> Result<()> {
107    let db_ctx = ctx.database().await?;
108    match args.cmd.unwrap_or(RequestsCommands::Stats(args.stats)) {
109        RequestsCommands::Stats(args) => {
110            let result = stats::execute_with_pool(args, &db_ctx, &ctx.cli).await?;
111            render_result(&result, &ctx.cli);
112            Ok(())
113        },
114        RequestsCommands::List(args) => {
115            let result = list::execute_with_pool(args, &db_ctx, &ctx.cli).await?;
116            render_result(&result, &ctx.cli);
117            Ok(())
118        },
119        RequestsCommands::Trends(args) => {
120            let result = trends::execute_with_pool(args, &db_ctx, &ctx.cli).await?;
121            render_result(&result, &ctx.cli);
122            Ok(())
123        },
124        RequestsCommands::Models(args) => {
125            let result = models::execute_with_pool(args, &db_ctx, &ctx.cli).await?;
126            render_result(&result, &ctx.cli);
127            Ok(())
128        },
129    }
130}