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)]
39 pub reasoning_tokens: i32,
40 pub cost_dollars: f64,
41 pub latency_ms: i64,
42 pub task_id: Option<TaskId>,
43 pub trace_id: Option<TraceId>,
44 pub messages: Vec<MessageRow>,
45 pub tool_calls: Vec<AuditToolCall>,
46}
47
48#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
49pub struct AuditToolCall {
50 pub tool_name: String,
51 pub tool_input: String,
52 pub sequence: i32,
53}
54
55crate::define_pool_command!(AuditArgs => (), with_config);
56
57async fn execute_with_pool_inner(
58 args: AuditArgs,
59 pool: &Arc<sqlx::PgPool>,
60 config: &CliConfig,
61) -> Result<()> {
62 let service = TraceQueryService::new(Arc::clone(pool));
63
64 let row = service.find_ai_request_for_audit(&args.id).await?;
65
66 let Some(row) = row else {
67 render_result(¬_found_output(&args.id), config);
68 return Ok(());
69 };
70
71 let request_id = row.id;
72 let (messages, tool_calls) = tokio::try_join!(
73 service.list_audit_messages(&request_id),
74 service.list_audit_tool_calls(&request_id),
75 )?;
76
77 let output = AuditOutput {
78 request_id,
79 status: row.status,
80 error_message: row.error_message,
81 provider: row.provider,
82 model: row.model,
83 requested_model: row.requested_model,
84 input_tokens: row.input_tokens.unwrap_or(0),
85 output_tokens: row.output_tokens.unwrap_or(0),
86 cache_read_tokens: row.cache_read_tokens.unwrap_or(0),
87 reasoning_tokens: row.reasoning_tokens.unwrap_or(0),
88 cache_creation_tokens: row.cache_creation_tokens.unwrap_or(0),
89 cost_dollars: row.cost_microdollars as f64 / 1_000_000.0,
90 latency_ms: i64::from(row.latency_ms.unwrap_or(0)),
91 task_id: row.task_id,
92 trace_id: row.trace_id.map(TraceId::new),
93 messages: messages
94 .into_iter()
95 .map(|m| MessageRow {
96 sequence: m.sequence_number,
97 role: m.role,
98 content: m.content,
99 })
100 .collect(),
101 tool_calls: tool_calls
102 .into_iter()
103 .map(|t| AuditToolCall {
104 tool_name: t.tool_name,
105 tool_input: t.tool_input,
106 sequence: t.sequence_number,
107 })
108 .collect(),
109 };
110
111 render_result(&build_audit(&output), config);
112
113 Ok(())
114}
115
116#[must_use]
117pub fn build_audit(output: &AuditOutput) -> CommandOutput {
118 let title = if output.status == "completed" {
119 "AI Request Audit".to_owned()
120 } else {
121 format!("AI Request Audit — {}", output.status.to_uppercase())
122 };
123 CommandOutput::card_value(title, output)
124}
125
126#[must_use]
127pub fn not_found_output(id: &str) -> CommandOutput {
128 use systemprompt_models::artifacts::NoticeLine;
129 CommandOutput::message(vec![
130 NoticeLine::new("warning", format!("No AI request found for: {id}")),
131 NoticeLine::new(
132 "info",
133 "Tip: Use 'systemprompt infra logs request list' to see recent requests",
134 ),
135 NoticeLine::new(
136 "info",
137 "Use 'systemprompt infra logs trace list' to see recent traces",
138 ),
139 ])
140}