systemprompt_api/services/gateway/audit/
mod.rs1mod complete;
14mod message_text;
15mod open;
16pub mod payload;
17
18#[cfg(feature = "test-api")]
19pub mod test_api {
20 pub use super::message_text::flatten_message_content;
21}
22
23use std::sync::{Arc, Mutex};
24use std::time::Instant;
25
26use anyhow::Result;
27use systemprompt_ai::models::RequestStatus;
28use systemprompt_ai::repository::{AiRequestPayloadRepository, AiRequestRepository};
29use systemprompt_identifiers::{
30 AiRequestId, ContextId, GatewayConversationId, SessionId, TraceId, UserId,
31};
32
33#[derive(Debug, Clone)]
34pub struct GatewayRequestContext {
35 pub ai_request_id: AiRequestId,
36 pub user_id: UserId,
37 pub session_id: Option<SessionId>,
38 pub context_id: ContextId,
39 pub gateway_conversation_id: Option<GatewayConversationId>,
40 pub trace_id: Option<TraceId>,
41 pub provider: String,
42 pub model: String,
43 pub requested_model: Option<String>,
44 pub max_tokens: Option<u32>,
45 pub is_streaming: bool,
46 pub wire_protocol: String,
47}
48
49#[expect(
50 missing_debug_implementations,
51 reason = "service type holds repository clients that intentionally do not implement Debug"
52)]
53pub struct GatewayAudit {
54 requests: Arc<AiRequestRepository>,
55 payloads: Arc<AiRequestPayloadRepository>,
56 context_materializer: systemprompt_traits::DynContextMaterializer,
57 pub ctx: GatewayRequestContext,
58 served_model: Mutex<Option<String>>,
59 started_at: Instant,
60}
61
62impl GatewayAudit {
63 pub fn new(repos: &super::GatewayRepositories, ctx: GatewayRequestContext) -> Self {
64 Self {
65 requests: Arc::clone(&repos.requests),
66 payloads: Arc::clone(&repos.payloads),
67 context_materializer: Arc::clone(&repos.context_materializer),
68 ctx,
69 served_model: Mutex::new(None),
70 started_at: Instant::now(),
71 }
72 }
73
74 pub async fn set_served_model(&self, model: &str) {
75 if model.is_empty() || model == self.ctx.model {
76 return;
77 }
78 if let Ok(mut slot) = self.served_model.lock() {
79 *slot = Some(model.to_owned());
80 }
81 if let Err(e) = self
82 .requests
83 .update_model(&self.ctx.ai_request_id, model)
84 .await
85 {
86 tracing::warn!(error = %e, "update_model failed");
87 }
88 }
89
90 pub async fn set_prepared_body_digest(&self, body: &[u8]) {
91 let sha256 = payload::digest_hex(body);
92 if let Err(e) = self
93 .payloads
94 .upsert_prepared_sha256(&self.ctx.ai_request_id, &sha256)
95 .await
96 {
97 tracing::warn!(error = %e, ai_request_id = %self.ctx.ai_request_id, "prepared body digest write failed");
98 }
99 }
100
101 pub async fn set_system_prompt_override(&self, descriptor: &str) {
102 if let Err(e) = self
103 .requests
104 .update_system_prompt_override(&self.ctx.ai_request_id, descriptor)
105 .await
106 {
107 tracing::warn!(error = %e, "update_system_prompt_override failed");
108 }
109 }
110
111 pub async fn set_route_match(&self, descriptor: &str) {
112 if let Err(e) = self
113 .requests
114 .update_route_match(&self.ctx.ai_request_id, descriptor)
115 .await
116 {
117 tracing::warn!(error = %e, "update_route_match failed");
118 }
119 }
120
121 pub async fn fail(&self, error: &str) -> Result<()> {
122 if let Err(e) = self
123 .requests
124 .update_error(&self.ctx.ai_request_id, RequestStatus::Failed, error)
125 .await
126 {
127 tracing::warn!(error = %e, "audit fail update failed");
128 }
129 tracing::info!(
130 ai_request_id = %self.ctx.ai_request_id,
131 user_id = %self.ctx.user_id,
132 provider = %self.ctx.provider,
133 model = %self.ctx.model,
134 error,
135 "Gateway audit: request failed"
136 );
137 Ok(())
138 }
139}