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 client_kind: String,
133 pub client_attestation: String,
134 pub provider: Option<String>,
135 pub model: Option<String>,
136 pub input_tokens: Option<i32>,
137 pub output_tokens: Option<i32>,
138 pub cache_read_tokens: Option<i32>,
139 pub cache_creation_tokens: Option<i32>,
140 pub reasoning_tokens: Option<i32>,
141 pub cost_microdollars: i64,
142 pub latency_ms: Option<i32>,
143 pub status: String,
144 pub finish_reason: Option<String>,
145}
146
147#[derive(Debug, Clone, Serialize, Deserialize)]
148pub struct AiRequestDetail {
149 pub id: AiRequestId,
150 pub user_id: UserId,
151 pub actor_kind: String,
152 pub actor_id: String,
153 pub provider: Option<String>,
154 pub model: Option<String>,
155 pub input_tokens: Option<i32>,
156 pub output_tokens: Option<i32>,
157 pub cost_microdollars: i64,
158 pub latency_ms: Option<i32>,
159 pub status: String,
160 pub error_message: Option<String>,
161 pub client_kind: String,
162 pub client_attestation: String,
163 pub client_evidence: Option<AiRequestClientEvidence>,
164}
165
166#[derive(Debug, Clone, Serialize, Deserialize)]
169pub struct AiRequestClientEvidence {
170 pub kind_source: String,
171 pub attested_host: Option<String>,
172 pub declared_client: Option<String>,
173 pub native_marker: Option<String>,
174 pub ua_product: Option<String>,
175 pub ua_version: Option<String>,
176 pub sdk_lang: Option<String>,
177 pub sdk_package_version: Option<String>,
178 pub sdk_runtime: Option<String>,
179 pub sdk_runtime_version: Option<String>,
180 pub sdk_os: Option<String>,
181 pub sdk_arch: Option<String>,
182}
183
184#[derive(Debug, Clone, Default, Serialize, Deserialize)]
185pub struct AiRequestStats {
186 pub total_requests: i64,
187 pub total_input_tokens: i64,
188 pub total_output_tokens: i64,
189 pub total_cost_microdollars: i64,
190 pub avg_latency_ms: i64,
191 pub by_provider: Vec<ProviderStatsRow>,
192 pub by_model: Vec<ModelStatsRow>,
193}
194
195#[derive(Debug, Clone, Serialize, Deserialize)]
196pub struct ProviderStatsRow {
197 pub provider: String,
198 pub request_count: i64,
199 pub total_tokens: i64,
200 pub total_cost_microdollars: i64,
201 pub avg_latency_ms: i64,
202}
203
204#[derive(Debug, Clone, Serialize, Deserialize)]
205pub struct ModelStatsRow {
206 pub model: String,
207 pub provider: String,
208 pub request_count: i64,
209 pub total_tokens: i64,
210 pub total_cost_microdollars: i64,
211 pub avg_latency_ms: i64,
212}
213
214#[derive(Debug, Clone, Serialize, Deserialize)]
215pub struct AiRequestInfo {
216 pub id: AiRequestId,
217 pub provider: Option<String>,
218 pub model: Option<String>,
219 pub max_tokens: Option<i32>,
220 pub input_tokens: Option<i32>,
221 pub output_tokens: Option<i32>,
222 pub cost_microdollars: i64,
223 pub latency_ms: Option<i32>,
224}
225
226#[derive(Debug, Clone, Copy, PartialEq, Eq)]
229pub struct AuditPage {
230 pub offset: i64,
231 pub limit: i64,
232}
233
234impl AuditPage {
235 pub const ALL: Self = Self {
236 offset: 0,
237 limit: 0,
238 };
239
240 pub const fn sql_limit(self) -> Option<i64> {
241 if self.limit > 0 {
242 Some(self.limit)
243 } else {
244 None
245 }
246 }
247}
248
249#[derive(Debug, Clone, Serialize, Deserialize)]
250pub struct ConversationMessage {
251 pub role: String,
252 pub content: String,
253 pub sequence_number: i32,
254}