systemprompt_api/services/gateway/stream_tap/
mod.rs1mod accumulator;
8mod finalize;
9
10#[cfg(feature = "test-api")]
11pub mod test_api {
12 pub use super::accumulator::{Summary, TapState, accumulate_event, extract_summary, snapshot};
13}
14
15use std::pin::Pin;
16use std::sync::{Arc, Mutex};
17use std::task::{Context, Poll};
18
19use axum::body::Body;
20use bytes::Bytes;
21use futures_util::stream::{BoxStream, Stream};
22use systemprompt_database::DbPool;
23use systemprompt_identifiers::AiRequestId;
24
25use self::accumulator::{Summary, TapState, accumulate_event, extract_summary, snapshot};
26use self::finalize::finalize;
27use super::audit::GatewayAudit;
28use super::policy::GatewayPolicySpec;
29use super::protocol::canonical_response::CanonicalEvent;
30use super::protocol::inbound::InboundAdapter;
31use super::protocol::outbound::anthropic::streaming::SseDecoder;
32
33pub use self::finalize::{FinalizeDecision, classify};
34
35#[derive(Debug)]
38pub struct TapFinalizeCtx {
39 pub db: DbPool,
40 pub repos: crate::services::gateway::GatewayRepositories,
41 pub policy: GatewayPolicySpec,
42 pub ai_request_id: AiRequestId,
43}
44
45pub fn tap(
46 upstream: BoxStream<'static, Result<CanonicalEvent, String>>,
47 inbound: Arc<dyn InboundAdapter>,
48 request_model: String,
49 audit: Arc<GatewayAudit>,
50 finalize_ctx: TapFinalizeCtx,
51) -> Body {
52 let state = Arc::new(Mutex::new(TapState::default()));
53 let tapped = TappedStream {
54 inner: upstream,
55 state: Arc::clone(&state),
56 inbound,
57 request_model,
58 audit,
59 finalize_ctx: Some(finalize_ctx),
60 message_stop_rendered: false,
61 };
62 Body::from_stream(tapped)
63}
64
65pub fn tap_raw(
66 upstream: BoxStream<'static, Result<Bytes, String>>,
67 audit: Arc<GatewayAudit>,
68 finalize_ctx: TapFinalizeCtx,
69) -> Body {
70 Body::from_stream(RawTappedStream {
71 inner: upstream,
72 state: Arc::new(Mutex::new(TapState::default())),
73 decoder: SseDecoder::default(),
74 audit,
75 finalize_ctx: Some(finalize_ctx),
76 })
77}
78
79struct RawTappedStream {
80 inner: BoxStream<'static, Result<Bytes, String>>,
81 state: Arc<Mutex<TapState>>,
82 decoder: SseDecoder,
83 audit: Arc<GatewayAudit>,
84 finalize_ctx: Option<TapFinalizeCtx>,
85}
86
87impl RawTappedStream {
88 fn take_summary(&mut self) -> Option<(Summary, TapFinalizeCtx)> {
89 let ctx = self.finalize_ctx.take()?;
90 self.state.lock().ok().and_then(|mut s| {
91 if s.finalized {
92 return None;
93 }
94 s.finalized = true;
95 Some((extract_summary(&mut s), ctx))
96 })
97 }
98}
99
100impl Stream for RawTappedStream {
101 type Item = Result<Bytes, std::io::Error>;
102
103 fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
104 match self.inner.as_mut().poll_next(cx) {
105 Poll::Pending => Poll::Pending,
106 Poll::Ready(None) => {
107 if let Some((summary, ctx)) = self.take_summary() {
108 finalize(Arc::clone(&self.audit), summary, ctx, "eof");
109 }
110 Poll::Ready(None)
111 },
112 Poll::Ready(Some(Err(e))) => {
113 if let Ok(mut s) = self.state.lock() {
114 s.error = Some(e.clone());
115 }
116 Poll::Ready(Some(Err(std::io::Error::new(
117 std::io::ErrorKind::BrokenPipe,
118 e,
119 ))))
120 },
121 Poll::Ready(Some(Ok(bytes))) => {
122 let events = self.decoder.push(&bytes);
123 if let Ok(mut s) = self.state.lock() {
124 for event in &events {
125 accumulate_event(&mut s, event);
126 }
127 s.final_bytes.extend_from_slice(&bytes);
128 }
129 Poll::Ready(Some(Ok(bytes)))
130 },
131 }
132 }
133}
134
135impl Drop for RawTappedStream {
136 fn drop(&mut self) {
137 let Some((summary, ctx)) = self.take_summary() else {
138 return;
139 };
140 finalize(Arc::clone(&self.audit), summary, ctx, "drop");
141 }
142}
143
144struct TappedStream {
145 inner: BoxStream<'static, Result<CanonicalEvent, String>>,
146 state: Arc<Mutex<TapState>>,
147 inbound: Arc<dyn InboundAdapter>,
148 request_model: String,
149 audit: Arc<GatewayAudit>,
150 finalize_ctx: Option<TapFinalizeCtx>,
151 message_stop_rendered: bool,
157}
158
159impl Stream for TappedStream {
160 type Item = Result<Bytes, std::io::Error>;
161
162 fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
163 loop {
164 match self.inner.as_mut().poll_next(cx) {
165 Poll::Pending => return Poll::Pending,
166 Poll::Ready(None) => {
167 return self.finalize_on_eof();
168 },
169 Poll::Ready(Some(Err(e))) => {
170 if let Ok(mut s) = self.state.lock() {
171 s.error = Some(e.clone());
172 }
173 let err = std::io::Error::new(std::io::ErrorKind::BrokenPipe, e);
174 return Poll::Ready(Some(Err(err)));
175 },
176 Poll::Ready(Some(Ok(event))) => {
177 let is_message_stop = matches!(event, CanonicalEvent::MessageStop { .. });
178 let terminal = matches!(event, CanonicalEvent::ContentBlockStop { .. })
179 || (is_message_stop && !self.message_stop_rendered);
180 let snap = self.state.lock().map_or(None, |mut s| {
181 accumulate_event(&mut s, &event);
182 terminal.then(|| snapshot(&s))
183 });
184 if is_message_stop {
185 self.message_stop_rendered = true;
186 }
187 let rendered = snap
188 .as_ref()
189 .and_then(|snapshot| {
190 self.inbound.render_terminal_event(
191 &event,
192 snapshot,
193 &self.request_model,
194 )
195 })
196 .or_else(|| self.inbound.render_event(&event, &self.request_model));
197 if let Some(bytes) = rendered {
198 if let Ok(mut s) = self.state.lock() {
199 s.final_bytes.extend_from_slice(&bytes);
200 }
201 return Poll::Ready(Some(Ok(bytes)));
202 }
203 },
204 }
205 }
206 }
207}
208
209impl TappedStream {
210 fn take_summary(&mut self) -> Option<(Summary, TapFinalizeCtx)> {
211 let ctx = self.finalize_ctx.take()?;
212 self.state.lock().ok().and_then(|mut s| {
213 if s.finalized {
214 return None;
215 }
216 s.finalized = true;
217 Some((extract_summary(&mut s), ctx))
218 })
219 }
220
221 fn finalize_on_eof(&mut self) -> Poll<Option<Result<Bytes, std::io::Error>>> {
222 let Some((summary, ctx)) = self.take_summary() else {
223 return Poll::Ready(None);
224 };
225 finalize(Arc::clone(&self.audit), summary, ctx, "eof");
226 Poll::Ready(None)
227 }
228}
229
230impl Drop for TappedStream {
231 fn drop(&mut self) {
232 let Some((summary, ctx)) = self.take_summary() else {
233 return;
234 };
235 finalize(Arc::clone(&self.audit), summary, ctx, "drop");
236 }
237}