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 total_cost_microdollars: i64,
65    pub avg_latency_ms: i64,
66    pub cache_hit_rate: f64,
67}
68
69#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
70pub struct RequestTrendPoint {
71    pub timestamp: String,
72    pub request_count: i64,
73    pub total_tokens: i64,
74    pub cost_microdollars: i64,
75    pub avg_latency_ms: i64,
76}
77
78#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
79pub struct RequestTrendsOutput {
80    pub period: String,
81    pub group_by: String,
82    pub points: Vec<RequestTrendPoint>,
83}
84
85#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
86pub struct ModelUsageRow {
87    pub provider: String,
88    pub model: String,
89    pub request_count: i64,
90    pub total_tokens: i64,
91    pub total_cost_microdollars: i64,
92    pub avg_latency_ms: i64,
93    pub percentage: f64,
94}
95
96#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
97pub struct ModelsOutput {
98    pub period: String,
99    pub models: Vec<ModelUsageRow>,
100    pub total_requests: i64,
101}
102
103pub async fn execute(args: RequestsArgs, ctx: &CommandContext) -> Result<()> {
104    let db_ctx = ctx.database().await?;
105    match args.cmd.unwrap_or(RequestsCommands::Stats(args.stats)) {
106        RequestsCommands::Stats(args) => {
107            let result = stats::execute_with_pool(args, &db_ctx, &ctx.cli).await?;
108            render_result(&result, &ctx.cli);
109            Ok(())
110        },
111        RequestsCommands::List(args) => {
112            let result = list::execute_with_pool(args, &db_ctx, &ctx.cli).await?;
113            render_result(&result, &ctx.cli);
114            Ok(())
115        },
116        RequestsCommands::Trends(args) => {
117            let result = trends::execute_with_pool(args, &db_ctx, &ctx.cli).await?;
118            render_result(&result, &ctx.cli);
119            Ok(())
120        },
121        RequestsCommands::Models(args) => {
122            let result = models::execute_with_pool(args, &db_ctx, &ctx.cli).await?;
123            render_result(&result, &ctx.cli);
124            Ok(())
125        },
126    }
127}