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 original_request_id: Uuid,
190    pub new_request_id: Uuid,
191}
192
193impl TurnMutationAuditEvent {
194    /// Build a `turn_retry` audit event.
195    #[must_use]
196    #[allow(clippy::too_many_arguments)]
197    pub fn new_retry(
198        timestamp: OffsetDateTime,
199        tenant_id: Uuid,
200        requester_type: RequesterType,
201        trace_id: Option<String>,
202        actor_user_id: Uuid,
203        chat_id: Uuid,
204        original_request_id: Uuid,
205        new_request_id: Uuid,
206    ) -> Self {
207        Self {
208            event_type: TurnMutationAuditEventType::TurnRetry,
209            timestamp,
210            tenant_id,
211            requester_type,
212            trace_id,
213            actor_user_id,
214            chat_id,
215            original_request_id,
216            new_request_id,
217        }
218    }
219
220    /// Build a `turn_edit` audit event.
221    #[must_use]
222    #[allow(clippy::too_many_arguments)]
223    pub fn new_edit(
224        timestamp: OffsetDateTime,
225        tenant_id: Uuid,
226        requester_type: RequesterType,
227        trace_id: Option<String>,
228        actor_user_id: Uuid,
229        chat_id: Uuid,
230        original_request_id: Uuid,
231        new_request_id: Uuid,
232    ) -> Self {
233        Self {
234            event_type: TurnMutationAuditEventType::TurnEdit,
235            timestamp,
236            tenant_id,
237            requester_type,
238            trace_id,
239            actor_user_id,
240            chat_id,
241            original_request_id,
242            new_request_id,
243        }
244    }
245}
246
247/// Audit event emitted when a user retries a turn.
248pub type TurnRetryAuditEvent = TurnMutationAuditEvent;
249
250/// Audit event emitted when a user edits a turn.
251pub type TurnEditAuditEvent = TurnMutationAuditEvent;
252
253/// Discriminator for [`TurnDeleteAuditEvent`].
254///
255/// Single-variant: the event type is always `"turn_delete"`.
256#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
257pub enum TurnDeleteAuditEventType {
258    #[default]
259    #[serde(rename = "turn_delete")]
260    TurnDelete,
261}
262
263impl std::fmt::Display for TurnDeleteAuditEventType {
264    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
265        f.write_str("turn_delete")
266    }
267}
268
269/// Audit event emitted when a user deletes a turn.
270#[derive(Debug, Clone, Serialize, Deserialize)]
271pub struct TurnDeleteAuditEvent {
272    pub event_type: TurnDeleteAuditEventType,
273    #[serde(with = "time::serde::rfc3339")]
274    pub timestamp: OffsetDateTime,
275    pub tenant_id: Uuid,
276    pub requester_type: RequesterType,
277    #[serde(default, skip_serializing_if = "Option::is_none")]
278    pub trace_id: Option<String>,
279
280    pub actor_user_id: Uuid,
281    pub chat_id: Uuid,
282    pub request_id: Uuid,
283}