Skip to main content

mini_chat_sdk/
audit_models.rs

1use serde::{Deserialize, Serialize};
2use time::OffsetDateTime;
3use uuid::Uuid;
4
5/// Requester type for audit events.
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
7#[serde(rename_all = "snake_case")]
8pub enum RequesterType {
9    User,
10    System,
11}
12
13/// Attachment kind metadata.
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
15#[serde(rename_all = "snake_case")]
16pub enum AttachmentKind {
17    Document,
18    Image,
19}
20
21/// Metadata for a file attachment included in a turn.
22#[derive(Debug, Clone, Serialize, Deserialize)]
23pub struct AttachmentMetadata {
24    pub attachment_id: Uuid,
25    pub attachment_kind: AttachmentKind,
26    #[serde(default, skip_serializing_if = "Option::is_none")]
27    pub filename: Option<String>,
28    #[serde(default, skip_serializing_if = "Option::is_none")]
29    pub content_type: Option<String>,
30    #[serde(default, skip_serializing_if = "Option::is_none")]
31    pub size_bytes: Option<u64>,
32    #[serde(default, skip_serializing_if = "Option::is_none")]
33    pub image_used_in_turn: Option<bool>,
34    #[serde(default, skip_serializing_if = "Option::is_none")]
35    pub doc_summary: Option<String>,
36}
37
38/// Token usage reported by the provider for audit purposes.
39#[derive(Debug, Clone, Serialize, Deserialize)]
40pub struct AuditUsageTokens {
41    pub input_tokens: u64,
42    pub output_tokens: u64,
43    #[serde(default, skip_serializing_if = "Option::is_none")]
44    pub model: Option<String>,
45}
46
47/// Latency measurements for a turn.
48#[derive(Debug, Clone, Default, Serialize, Deserialize)]
49pub struct LatencyMs {
50    #[serde(default, skip_serializing_if = "Option::is_none")]
51    pub ttft_ms: Option<u64>,
52    #[serde(default, skip_serializing_if = "Option::is_none")]
53    pub total_ms: Option<u64>,
54}
55
56/// License-level policy decision.
57#[derive(Debug, Clone, Serialize, Deserialize)]
58pub struct LicenseDecision {
59    pub feature: String,
60    pub decision: String,
61}
62
63/// Quota scope for quota decisions.
64#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
65#[serde(rename_all = "snake_case")]
66pub enum QuotaScope {
67    Tokens,
68    WebSearch,
69}
70
71/// Quota-level policy decision.
72#[derive(Debug, Clone, Serialize, Deserialize)]
73pub struct QuotaDecision {
74    pub decision: String,
75    #[serde(default, skip_serializing_if = "Option::is_none")]
76    pub quota_scope: Option<QuotaScope>,
77    #[serde(default, skip_serializing_if = "Option::is_none")]
78    pub downgrade_from: Option<String>,
79    #[serde(default, skip_serializing_if = "Option::is_none")]
80    pub downgrade_reason: Option<String>,
81}
82
83/// Combined policy decisions for a turn.
84#[derive(Debug, Clone, Serialize, Deserialize)]
85pub struct PolicyDecisions {
86    #[serde(default, skip_serializing_if = "Option::is_none")]
87    pub license: Option<LicenseDecision>,
88    pub quota: QuotaDecision,
89}
90
91/// Tool call counts for a turn.
92#[derive(Debug, Clone, Default, Serialize, Deserialize)]
93pub struct ToolCalls {
94    #[serde(default, skip_serializing_if = "Option::is_none")]
95    pub file_search_calls: Option<u64>,
96    #[serde(default, skip_serializing_if = "Option::is_none")]
97    pub web_search_calls: Option<u64>,
98}
99
100/// Discriminator for [`TurnAuditEvent`].
101#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
102#[serde(rename_all = "snake_case")]
103pub enum TurnAuditEventType {
104    TurnCompleted,
105    TurnFailed,
106}
107
108impl std::fmt::Display for TurnAuditEventType {
109    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
110        match self {
111            Self::TurnCompleted => f.write_str("turn_completed"),
112            Self::TurnFailed => f.write_str("turn_failed"),
113        }
114    }
115}
116
117/// Full turn audit event — emitted after a turn completes or fails.
118#[derive(Debug, Clone, Serialize, Deserialize)]
119pub struct TurnAuditEvent {
120    pub event_type: TurnAuditEventType,
121    #[serde(with = "time::serde::rfc3339")]
122    pub timestamp: OffsetDateTime,
123    pub tenant_id: Uuid,
124    pub requester_type: RequesterType,
125    #[serde(default, skip_serializing_if = "Option::is_none")]
126    pub trace_id: Option<String>,
127
128    pub user_id: Uuid,
129    pub chat_id: Uuid,
130    pub turn_id: Uuid,
131    pub request_id: Uuid,
132    pub selected_model: String,
133    pub effective_model: String,
134    #[serde(default, skip_serializing_if = "Option::is_none")]
135    pub policy_version_applied: Option<u64>,
136    pub usage: AuditUsageTokens,
137    pub latency_ms: LatencyMs,
138    pub policy_decisions: PolicyDecisions,
139    #[serde(default, skip_serializing_if = "Option::is_none")]
140    pub error_code: Option<String>,
141    /// User prompt text. The caller MUST redact secret patterns and truncate
142    /// to 8 KiB before setting this field (see DESIGN.md "Audit content handling").
143    #[serde(default, skip_serializing_if = "Option::is_none")]
144    pub prompt: Option<String>,
145    /// Assistant response text. The caller MUST redact secret patterns and
146    /// truncate to 8 KiB before setting this field (see DESIGN.md "Audit content handling").
147    #[serde(default, skip_serializing_if = "Option::is_none")]
148    pub response: Option<String>,
149    #[serde(default, skip_serializing_if = "Vec::is_empty")]
150    pub attachments: Vec<AttachmentMetadata>,
151    #[serde(default, skip_serializing_if = "Option::is_none")]
152    pub tool_calls: Option<ToolCalls>,
153}
154
155/// Discriminator for [`TurnMutationAuditEvent`].
156#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
157#[serde(rename_all = "snake_case")]
158pub enum TurnMutationAuditEventType {
159    TurnRetry,
160    TurnEdit,
161}
162
163impl std::fmt::Display for TurnMutationAuditEventType {
164    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
165        match self {
166            Self::TurnRetry => f.write_str("turn_retry"),
167            Self::TurnEdit => f.write_str("turn_edit"),
168        }
169    }
170}
171
172/// Shared audit event structure for turn mutations (retry, edit).
173///
174/// Both retry and edit carry the same fields: the acting user, the chat,
175/// the original request being replaced, and the new request that replaces it.
176/// `event_type` distinguishes the two.
177#[derive(Debug, Clone, Serialize, Deserialize)]
178pub struct TurnMutationAuditEvent {
179    pub event_type: TurnMutationAuditEventType,
180    #[serde(with = "time::serde::rfc3339")]
181    pub timestamp: OffsetDateTime,
182    pub tenant_id: Uuid,
183    pub requester_type: RequesterType,
184    #[serde(default, skip_serializing_if = "Option::is_none")]
185    pub trace_id: Option<String>,
186
187    pub actor_user_id: Uuid,
188    pub chat_id: Uuid,
189    pub turn_id: Uuid,
190    pub original_request_id: Uuid,
191    pub new_request_id: Uuid,
192}
193
194impl TurnMutationAuditEvent {
195    /// Build a `turn_retry` audit event.
196    #[must_use]
197    #[allow(clippy::too_many_arguments)]
198    pub fn new_retry(
199        timestamp: OffsetDateTime,
200        tenant_id: Uuid,
201        requester_type: RequesterType,
202        trace_id: Option<String>,
203        actor_user_id: Uuid,
204        chat_id: Uuid,
205        turn_id: Uuid,
206        original_request_id: Uuid,
207        new_request_id: Uuid,
208    ) -> Self {
209        Self {
210            event_type: TurnMutationAuditEventType::TurnRetry,
211            timestamp,
212            tenant_id,
213            requester_type,
214            trace_id,
215            actor_user_id,
216            chat_id,
217            turn_id,
218            original_request_id,
219            new_request_id,
220        }
221    }
222
223    /// Build a `turn_edit` audit event.
224    #[must_use]
225    #[allow(clippy::too_many_arguments)]
226    pub fn new_edit(
227        timestamp: OffsetDateTime,
228        tenant_id: Uuid,
229        requester_type: RequesterType,
230        trace_id: Option<String>,
231        actor_user_id: Uuid,
232        chat_id: Uuid,
233        turn_id: Uuid,
234        original_request_id: Uuid,
235        new_request_id: Uuid,
236    ) -> Self {
237        Self {
238            event_type: TurnMutationAuditEventType::TurnEdit,
239            timestamp,
240            tenant_id,
241            requester_type,
242            trace_id,
243            actor_user_id,
244            chat_id,
245            turn_id,
246            original_request_id,
247            new_request_id,
248        }
249    }
250}
251
252/// Audit event emitted when a user retries a turn.
253pub type TurnRetryAuditEvent = TurnMutationAuditEvent;
254
255/// Audit event emitted when a user edits a turn.
256pub type TurnEditAuditEvent = TurnMutationAuditEvent;
257
258/// Discriminator for [`TurnDeleteAuditEvent`].
259///
260/// Single-variant: the event type is always `"turn_delete"`.
261#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
262pub enum TurnDeleteAuditEventType {
263    #[default]
264    #[serde(rename = "turn_delete")]
265    TurnDelete,
266}
267
268impl std::fmt::Display for TurnDeleteAuditEventType {
269    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
270        f.write_str("turn_delete")
271    }
272}
273
274/// Audit event emitted when a user deletes a turn.
275#[derive(Debug, Clone, Serialize, Deserialize)]
276pub struct TurnDeleteAuditEvent {
277    pub event_type: TurnDeleteAuditEventType,
278    #[serde(with = "time::serde::rfc3339")]
279    pub timestamp: OffsetDateTime,
280    pub tenant_id: Uuid,
281    pub requester_type: RequesterType,
282    #[serde(default, skip_serializing_if = "Option::is_none")]
283    pub trace_id: Option<String>,
284
285    pub actor_user_id: Uuid,
286    pub chat_id: Uuid,
287    pub turn_id: Uuid,
288    pub request_id: Uuid,
289}