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