systemprompt_cli/commands/infrastructure/logs/
audit.rs1use std::sync::Arc;
7
8use anyhow::Result;
9use clap::Args;
10use schemars::JsonSchema;
11use serde::{Deserialize, Serialize};
12use systemprompt_identifiers::{AiRequestId, TaskId, TraceId};
13use systemprompt_logging::TraceQueryService;
14
15use super::types::MessageRow;
16use crate::CliConfig;
17use crate::shared::{CommandOutput, render_result};
18
19#[derive(Debug, Args)]
20pub struct AuditArgs {
21 #[arg(help = "AI request ID, task ID, or trace ID")]
22 pub id: String,
23}
24
25#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
26pub struct AuditOutput {
27 pub request_id: AiRequestId,
28 pub status: String,
29 #[serde(skip_serializing_if = "Option::is_none")]
30 pub error_message: Option<String>,
31 pub provider: Option<String>,
32 pub model: Option<String>,
33 pub requested_model: Option<String>,
34 pub input_tokens: i32,
35 pub output_tokens: i32,
36 pub cache_read_tokens: i32,
37 pub cache_creation_tokens: i32,
38 #[serde(default)]
41 pub reasoning_tokens: i32,
42 pub cost_dollars: f64,
43 pub latency_ms: i64,
44 pub task_id: Option<TaskId>,
45 pub trace_id: Option<TraceId>,
46 pub messages: Vec<MessageRow>,
47 pub tool_calls: Vec<AuditToolCall>,
48}
49
50#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
51pub struct AuditToolCall {
52 pub tool_name: String,
53 pub tool_input: String,
54 pub sequence: i32,
55}
56
57crate::define_pool_command!(AuditArgs => (), with_config);
58
59async fn execute_with_pool_inner(
60 args: AuditArgs,
61 pool: &Arc<sqlx::PgPool>,
62 config: &CliConfig,
63) -> Result<()> {
64 let service = TraceQueryService::new(Arc::clone(pool));
65
66 let row = service.find_ai_request_for_audit(&args.id).await?;
67
68 let Some(row) = row else {
69 render_result(¬_found_output(&args.id), config);
70 return Ok(());
71 };
72
73 let request_id = row.id;
74 let (messages, tool_calls) = tokio::try_join!(
75 service.list_audit_messages(&request_id),
76 service.list_audit_tool_calls(&request_id),
77 )?;
78
79 let output = AuditOutput {
80 request_id,
81 status: row.status,
82 error_message: row.error_message,
83 provider: row.provider,
84 model: row.model,
85 requested_model: row.requested_model,
86 input_tokens: row.input_tokens.unwrap_or(0),
87 output_tokens: row.output_tokens.unwrap_or(0),
88 cache_read_tokens: row.cache_read_tokens.unwrap_or(0),
89 reasoning_tokens: row.reasoning_tokens.unwrap_or(0),
90 cache_creation_tokens: row.cache_creation_tokens.unwrap_or(0),
91 cost_dollars: row.cost_microdollars as f64 / 1_000_000.0,
92 latency_ms: i64::from(row.latency_ms.unwrap_or(0)),
93 task_id: row.task_id,
94 trace_id: row.trace_id.map(TraceId::new),
95 messages: messages
96 .into_iter()
97 .map(|m| MessageRow {
98 sequence: m.sequence_number,
99 role: m.role,
100 content: m.content,
101 })
102 .collect(),
103 tool_calls: tool_calls
104 .into_iter()
105 .map(|t| AuditToolCall {
106 tool_name: t.tool_name,
107 tool_input: t.tool_input,
108 sequence: t.sequence_number,
109 })
110 .collect(),
111 };
112
113 render_result(&build_audit(&output), config);
114
115 Ok(())
116}
117
118#[must_use]
119pub fn build_audit(output: &AuditOutput) -> CommandOutput {
120 let title = if output.status == "completed" {
123 "AI Request Audit".to_owned()
124 } else {
125 format!("AI Request Audit — {}", output.status.to_uppercase())
126 };
127 CommandOutput::card_value(title, output)
128}
129
130#[must_use]
131pub fn not_found_output(id: &str) -> CommandOutput {
132 use systemprompt_models::artifacts::NoticeLine;
133 CommandOutput::message(vec![
134 NoticeLine::new("warning", format!("No AI request found for: {id}")),
135 NoticeLine::new(
136 "info",
137 "Tip: Use 'systemprompt infra logs request list' to see recent requests",
138 ),
139 NoticeLine::new(
140 "info",
141 "Use 'systemprompt infra logs trace list' to see recent traces",
142 ),
143 ])
144}