Skip to main content

systemprompt_cli/commands/infrastructure/logs/request/
mod.rs

1//! `infra logs request` subcommands for inspecting AI provider requests.
2//!
3//! Exposes [`RequestCommands`] (list, show, stats) and the row types
4//! ([`RequestListRow`], [`RequestShowOutput`]) returned to the renderer.
5
6mod list;
7mod show;
8mod stats;
9
10use anyhow::Result;
11use clap::Subcommand;
12use schemars::JsonSchema;
13use serde::{Deserialize, Serialize};
14use systemprompt_runtime::DatabaseContext;
15
16use super::types::{MessageRow, ToolCallRow};
17use crate::CliConfig;
18use crate::shared::{CommandOutput, render_result};
19use systemprompt_models::artifacts::NoticeLine;
20
21pub use stats::{RequestStatsOutput, build_request_stats};
22
23const REQUEST_LIST_COLUMNS: [&str; 8] = [
24    "request_id",
25    "timestamp",
26    "provider",
27    "model",
28    "tokens",
29    "cost",
30    "latency_ms",
31    "status",
32];
33
34#[must_use]
35pub fn build_request_list(rows: &[RequestListRow]) -> CommandOutput {
36    if rows.is_empty() {
37        return CommandOutput::message(vec![NoticeLine::new("info", "No AI requests found")]);
38    }
39    CommandOutput::table_of(REQUEST_LIST_COLUMNS.to_vec(), rows).with_title("AI Requests")
40}
41
42#[must_use]
43pub fn build_request_show(detail: &RequestShowOutput) -> CommandOutput {
44    CommandOutput::card_value("AI Request Details", detail)
45}
46
47#[must_use]
48pub fn request_show_not_found(request_id: &str) -> CommandOutput {
49    CommandOutput::message(vec![
50        NoticeLine::new("warning", format!("AI request not found: {request_id}")),
51        NoticeLine::new(
52            "info",
53            "Tip: Use 'systemprompt infra logs request list' to see recent requests",
54        ),
55    ])
56}
57
58#[derive(Debug, Subcommand)]
59pub enum RequestCommands {
60    #[command(
61        about = "Operational list of recent AI requests. For dashboard metrics (time range, model filter, CSV export), use `analytics requests list`",
62        after_help = "EXAMPLES:\n  systemprompt infra logs request list\n  systemprompt infra \
63                      logs request list --model gpt-4 --since 1h"
64    )]
65    List(list::ListArgs),
66
67    #[command(
68        about = "Quick single-request view by request id (messages, linked MCP calls, status/error)",
69        after_help = "EXAMPLES:\n  systemprompt infra logs request show abc123\n  systemprompt \
70                      infra logs request show abc123 --messages --tools"
71    )]
72    Show(show::ShowArgs),
73
74    #[command(
75        about = "Operational request aggregate with by-provider / by-model breakdown. For range/model-filtered dashboards with export, use `analytics requests stats`",
76        after_help = "EXAMPLES:\n  systemprompt infra logs request stats\n  systemprompt infra \
77                      logs request stats --since 24h"
78    )]
79    Stats(stats::StatsArgs),
80}
81
82#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
83pub struct RequestListRow {
84    pub request_id: String,
85    pub timestamp: String,
86    pub provider: String,
87    pub model: String,
88    pub tokens: String,
89    pub cost: String,
90    #[serde(skip_serializing_if = "Option::is_none")]
91    pub latency_ms: Option<i64>,
92    pub status: String,
93}
94
95#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
96pub struct RequestShowOutput {
97    pub request_id: String,
98    pub provider: String,
99    pub model: String,
100    pub input_tokens: i32,
101    pub output_tokens: i32,
102    pub cost_dollars: f64,
103    pub latency_ms: i64,
104    pub status: String,
105    #[serde(skip_serializing_if = "Option::is_none")]
106    pub error_message: Option<String>,
107    pub messages: Vec<MessageRow>,
108    pub linked_mcp_calls: Vec<ToolCallRow>,
109}
110
111pub async fn execute(command: RequestCommands, config: &CliConfig) -> Result<()> {
112    match command {
113        RequestCommands::List(args) => {
114            let result = list::execute(args, config).await?;
115            render_result(&result);
116            Ok(())
117        },
118        RequestCommands::Show(args) => {
119            let result = show::execute(args, config).await?;
120            render_result(&result);
121            Ok(())
122        },
123        RequestCommands::Stats(args) => stats::execute(args, config).await,
124    }
125}
126
127pub async fn execute_with_pool(
128    command: RequestCommands,
129    db_ctx: &DatabaseContext,
130    config: &CliConfig,
131) -> Result<()> {
132    match command {
133        RequestCommands::List(args) => {
134            let result = list::execute_with_pool(args, db_ctx, config).await?;
135            render_result(&result);
136            Ok(())
137        },
138        RequestCommands::Show(args) => {
139            let result = show::execute_with_pool(args, db_ctx, config).await?;
140            render_result(&result);
141            Ok(())
142        },
143        RequestCommands::Stats(args) => stats::execute_with_pool(args, db_ctx, config).await,
144    }
145}