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