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