systemprompt_runtime/trace/models/
ai.rs1use chrono::{DateTime, Utc};
8use serde::{Deserialize, Serialize};
9use systemprompt_identifiers::{AiRequestId, TraceId, UserId};
10
11#[derive(Debug, Clone)]
12pub struct AiRequestFilter {
13 pub limit: i64,
14 pub since: Option<DateTime<Utc>>,
15 pub until: Option<DateTime<Utc>>,
16 pub before: Option<RequestCursor>,
17 pub model: Option<String>,
18 pub provider: Option<String>,
19 pub user: Option<String>,
20}
21
22impl AiRequestFilter {
23 pub const fn new(limit: i64) -> Self {
24 Self {
25 limit,
26 since: None,
27 until: None,
28 before: None,
29 model: None,
30 provider: None,
31 user: None,
32 }
33 }
34
35 pub const fn with_since(mut self, since: DateTime<Utc>) -> Self {
36 self.since = Some(since);
37 self
38 }
39
40 pub const fn with_until(mut self, until: DateTime<Utc>) -> Self {
41 self.until = Some(until);
42 self
43 }
44
45 pub fn with_before(mut self, before: RequestCursor) -> Self {
46 self.before = Some(before);
47 self
48 }
49
50 systemprompt_models::builder_methods! {
51 with_model(model) -> String,
52 with_provider(provider) -> String,
53 with_user(user) -> String,
54 }
55}
56
57#[derive(Debug, Clone, PartialEq, Eq)]
65pub struct RequestCursor {
66 pub created_at: DateTime<Utc>,
67 pub id: AiRequestId,
68}
69
70impl RequestCursor {
71 pub const SEPARATOR: char = '@';
72}
73
74#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
75pub enum RequestCursorError {
76 #[error("cursor is missing the `@` between the timestamp and the request id")]
77 MissingSeparator,
78 #[error("cursor has an empty request id")]
79 EmptyId,
80 #[error("cursor timestamp `{stamp}` is not RFC 3339: {source}")]
81 InvalidTimestamp {
82 stamp: String,
83 source: chrono::ParseError,
84 },
85}
86
87impl std::str::FromStr for RequestCursor {
88 type Err = RequestCursorError;
89
90 fn from_str(raw: &str) -> Result<Self, Self::Err> {
91 let (stamp, id) = raw
92 .trim()
93 .split_once(Self::SEPARATOR)
94 .ok_or(RequestCursorError::MissingSeparator)?;
95 if id.is_empty() {
96 return Err(RequestCursorError::EmptyId);
97 }
98 let created_at = DateTime::parse_from_rfc3339(stamp)
99 .map_err(|source| RequestCursorError::InvalidTimestamp {
100 stamp: stamp.to_owned(),
101 source,
102 })?
103 .with_timezone(&Utc);
104 Ok(Self {
105 created_at,
106 id: AiRequestId::new(id),
107 })
108 }
109}
110
111impl std::fmt::Display for RequestCursor {
112 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
113 write!(
114 f,
115 "{}{}{}",
116 self.created_at
117 .to_rfc3339_opts(chrono::SecondsFormat::Micros, true),
118 Self::SEPARATOR,
119 self.id.as_str()
120 )
121 }
122}
123
124#[derive(Debug, Clone, Serialize, Deserialize)]
125pub struct AiRequestListItem {
126 pub id: AiRequestId,
127 pub created_at: DateTime<Utc>,
128 pub trace_id: Option<TraceId>,
129 pub user_id: UserId,
130 pub actor_kind: String,
131 pub actor_id: String,
132 pub provider: Option<String>,
133 pub model: Option<String>,
134 pub input_tokens: Option<i32>,
135 pub output_tokens: Option<i32>,
136 pub cache_read_tokens: Option<i32>,
137 pub cache_creation_tokens: Option<i32>,
138 pub reasoning_tokens: Option<i32>,
139 pub cost_microdollars: i64,
140 pub latency_ms: Option<i32>,
141 pub status: String,
142}
143
144#[derive(Debug, Clone, Serialize, Deserialize)]
145pub struct AiRequestDetail {
146 pub id: AiRequestId,
147 pub user_id: UserId,
148 pub actor_kind: String,
149 pub actor_id: String,
150 pub provider: Option<String>,
151 pub model: Option<String>,
152 pub input_tokens: Option<i32>,
153 pub output_tokens: Option<i32>,
154 pub cost_microdollars: i64,
155 pub latency_ms: Option<i32>,
156 pub status: String,
157 pub error_message: Option<String>,
158}
159
160#[derive(Debug, Clone, Default, Serialize, Deserialize)]
161pub struct AiRequestStats {
162 pub total_requests: i64,
163 pub total_input_tokens: i64,
164 pub total_output_tokens: i64,
165 pub total_cost_microdollars: i64,
166 pub avg_latency_ms: i64,
167 pub by_provider: Vec<ProviderStatsRow>,
168 pub by_model: Vec<ModelStatsRow>,
169}
170
171#[derive(Debug, Clone, Serialize, Deserialize)]
172pub struct ProviderStatsRow {
173 pub provider: String,
174 pub request_count: i64,
175 pub total_tokens: i64,
176 pub total_cost_microdollars: i64,
177 pub avg_latency_ms: i64,
178}
179
180#[derive(Debug, Clone, Serialize, Deserialize)]
181pub struct ModelStatsRow {
182 pub model: String,
183 pub provider: String,
184 pub request_count: i64,
185 pub total_tokens: i64,
186 pub total_cost_microdollars: i64,
187 pub avg_latency_ms: i64,
188}
189
190#[derive(Debug, Clone, Serialize, Deserialize)]
191pub struct AiRequestInfo {
192 pub id: AiRequestId,
193 pub provider: Option<String>,
194 pub model: Option<String>,
195 pub max_tokens: Option<i32>,
196 pub input_tokens: Option<i32>,
197 pub output_tokens: Option<i32>,
198 pub cost_microdollars: i64,
199 pub latency_ms: Option<i32>,
200}
201
202#[derive(Debug, Clone, Copy, PartialEq, Eq)]
205pub struct AuditPage {
206 pub offset: i64,
207 pub limit: i64,
208}
209
210impl AuditPage {
211 pub const ALL: Self = Self {
212 offset: 0,
213 limit: 0,
214 };
215
216 pub const fn sql_limit(self) -> Option<i64> {
217 if self.limit > 0 {
218 Some(self.limit)
219 } else {
220 None
221 }
222 }
223}
224
225#[derive(Debug, Clone, Serialize, Deserialize)]
226pub struct ConversationMessage {
227 pub role: String,
228 pub content: String,
229 pub sequence_number: i32,
230}