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