systemprompt_api/services/gateway/stream_tap/
mod.rs1mod abort;
8mod accumulator;
9mod finalize;
10
11#[cfg(feature = "test-api")]
12pub mod test_api {
13 pub use super::accumulator::{Summary, TapState, accumulate_event, extract_summary, snapshot};
14}
15
16use std::pin::Pin;
17use std::sync::{Arc, Mutex};
18use std::task::{Context, Poll};
19
20use axum::body::Body;
21use bytes::Bytes;
22use futures_util::stream::{BoxStream, Stream};
23use systemprompt_database::DbPool;
24use systemprompt_identifiers::AiRequestId;
25
26use self::accumulator::{Summary, TapState, accumulate_event, extract_summary, snapshot};
27use self::finalize::finalize;
28use super::audit::GatewayAudit;
29use super::policy::GatewayPolicySpec;
30use super::protocol::canonical_response::CanonicalEvent;
31use super::protocol::inbound::InboundAdapter;
32use super::protocol::outbound::anthropic::streaming::SseDecoder;
33
34pub use self::finalize::{FailCause, FinalizeDecision, classify};
35
36pub use self::abort::STREAM_ABORT_MESSAGE;
37
38#[derive(Debug)]
41pub struct TapFinalizeCtx {
42 pub db: DbPool,
43 pub repos: crate::services::gateway::GatewayRepositories,
44 pub policy: GatewayPolicySpec,
45 pub ai_request_id: AiRequestId,
46}
47
48#[derive(Debug)]
53pub struct TapRender {
54 pub inbound: Arc<dyn InboundAdapter>,
55 pub request_model: String,
56 pub stream_usage: bool,
57}
58
59pub fn tap(
60 upstream: BoxStream<'static, Result<CanonicalEvent, String>>,
61 render: TapRender,
62 audit: Arc<GatewayAudit>,
63 finalize_ctx: TapFinalizeCtx,
64) -> Body {
65 let TapRender {
66 inbound,
67 request_model,
68 stream_usage,
69 } = render;
70 let state = Arc::new(Mutex::new(TapState::default()));
71 let tapped = TappedStream {
72 inner: upstream,
73 state: Arc::clone(&state),
74 inbound,
75 request_model,
76 stream_usage,
77 audit,
78 finalize_ctx: Some(finalize_ctx),
79 message_stop_rendered: false,
80 ended: false,
81 };
82 Body::from_stream(tapped)
83}
84
85pub fn tap_raw(
90 upstream: BoxStream<'static, Result<Bytes, String>>,
91 inbound: Arc<dyn InboundAdapter>,
92 audit: Arc<GatewayAudit>,
93 finalize_ctx: TapFinalizeCtx,
94) -> Body {
95 Body::from_stream(RawTappedStream {
96 inner: upstream,
97 state: Arc::new(Mutex::new(TapState::default())),
98 decoder: SseDecoder::default(),
99 inbound,
100 audit,
101 finalize_ctx: Some(finalize_ctx),
102 ended: false,
103 })
104}
105
106struct RawTappedStream {
107 inner: BoxStream<'static, Result<Bytes, String>>,
108 state: Arc<Mutex<TapState>>,
109 decoder: SseDecoder,
110 inbound: Arc<dyn InboundAdapter>,
111 audit: Arc<GatewayAudit>,
112 finalize_ctx: Option<TapFinalizeCtx>,
113 ended: bool,
114}
115
116impl RawTappedStream {
117 fn take_summary(&mut self) -> Option<(Summary, TapFinalizeCtx)> {
118 let ctx = self.finalize_ctx.take()?;
119 self.state.lock().ok().and_then(|mut s| {
120 if s.finalized {
121 return None;
122 }
123 s.finalized = true;
124 Some((extract_summary(&mut s), ctx))
125 })
126 }
127}
128
129impl Stream for RawTappedStream {
130 type Item = Result<Bytes, std::io::Error>;
131
132 fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
133 if self.ended {
134 return Poll::Ready(None);
135 }
136 match self.inner.as_mut().poll_next(cx) {
137 Poll::Pending => Poll::Pending,
138 Poll::Ready(None) => {
139 self.ended = true;
140 let Some((summary, ctx)) = self.take_summary() else {
141 return Poll::Ready(None);
142 };
143 let aborted = abort::is_abort(&summary);
144 finalize(Arc::clone(&self.audit), summary, ctx, "eof");
145 if !aborted {
146 return Poll::Ready(None);
147 }
148 Poll::Ready(abort::abort_frame(&self.inbound, "").map(Ok))
149 },
150 Poll::Ready(Some(Err(e))) => {
151 if let Ok(mut s) = self.state.lock() {
152 s.error = Some(e.clone());
153 }
154 Poll::Ready(Some(Err(std::io::Error::new(
155 std::io::ErrorKind::BrokenPipe,
156 e,
157 ))))
158 },
159 Poll::Ready(Some(Ok(bytes))) => {
160 let events = self.decoder.push(&bytes);
161 if let Ok(mut s) = self.state.lock() {
162 for event in &events {
163 accumulate_event(&mut s, event);
164 }
165 s.final_bytes.extend_from_slice(&bytes);
166 }
167 Poll::Ready(Some(Ok(bytes)))
168 },
169 }
170 }
171}
172
173impl Drop for RawTappedStream {
174 fn drop(&mut self) {
175 let Some((summary, ctx)) = self.take_summary() else {
176 return;
177 };
178 finalize(Arc::clone(&self.audit), summary, ctx, "drop");
179 }
180}
181
182struct TappedStream {
183 inner: BoxStream<'static, Result<CanonicalEvent, String>>,
184 state: Arc<Mutex<TapState>>,
185 inbound: Arc<dyn InboundAdapter>,
186 request_model: String,
187 stream_usage: bool,
190 audit: Arc<GatewayAudit>,
191 finalize_ctx: Option<TapFinalizeCtx>,
192 message_stop_rendered: bool,
199 ended: bool,
202}
203
204impl Stream for TappedStream {
205 type Item = Result<Bytes, std::io::Error>;
206
207 fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
208 if self.ended {
209 return Poll::Ready(None);
210 }
211 loop {
212 match self.inner.as_mut().poll_next(cx) {
213 Poll::Pending => return Poll::Pending,
214 Poll::Ready(None) => {
215 self.ended = true;
216 return self.finalize_on_eof();
217 },
218 Poll::Ready(Some(Err(e))) => {
219 if let Ok(mut s) = self.state.lock() {
220 s.error = Some(e.clone());
221 }
222 let err = std::io::Error::new(std::io::ErrorKind::BrokenPipe, e);
223 return Poll::Ready(Some(Err(err)));
224 },
225 Poll::Ready(Some(Ok(event))) => {
226 let is_message_stop = matches!(event, CanonicalEvent::MessageStop { .. });
227 let terminal = matches!(event, CanonicalEvent::ContentBlockStop { .. })
228 || (is_message_stop && !self.message_stop_rendered);
229 let snap = self.state.lock().map_or(None, |mut s| {
230 accumulate_event(&mut s, &event);
231 terminal.then(|| snapshot(&s))
232 });
233 let terminal_suppressed = is_message_stop && self.message_stop_rendered;
234 if is_message_stop {
235 self.message_stop_rendered = true;
236 }
237 let rendered = snap
238 .as_ref()
239 .and_then(|snapshot| {
240 self.inbound.render_terminal_event(
241 &event,
242 snapshot,
243 &self.request_model,
244 )
245 })
246 .or_else(|| {
247 (!terminal_suppressed)
254 .then(|| self.inbound.render_event(&event, &self.request_model))
255 .flatten()
256 });
257 if let Some(bytes) = rendered {
258 if let Ok(mut s) = self.state.lock() {
259 s.final_bytes.extend_from_slice(&bytes);
260 }
261 return Poll::Ready(Some(Ok(bytes)));
262 }
263 },
264 }
265 }
266 }
267}
268
269impl TappedStream {
270 fn take_summary(&mut self) -> Option<(Summary, TapFinalizeCtx)> {
271 let ctx = self.finalize_ctx.take()?;
272 self.state.lock().ok().and_then(|mut s| {
273 if s.finalized {
274 return None;
275 }
276 s.finalized = true;
277 Some((extract_summary(&mut s), ctx))
278 })
279 }
280
281 fn finalize_on_eof(&mut self) -> Poll<Option<Result<Bytes, std::io::Error>>> {
282 let Some((summary, ctx)) = self.take_summary() else {
283 return Poll::Ready(None);
284 };
285 let aborted = abort::is_abort(&summary);
286 let tail = (!aborted)
287 .then(|| abort::tail_frames(&self.inbound, &summary.response, self.stream_usage))
288 .flatten();
289 finalize(Arc::clone(&self.audit), summary, ctx, "eof");
290 if !aborted {
291 return Poll::Ready(tail.map(Ok));
292 }
293 Poll::Ready(abort::abort_frame(&self.inbound, &self.request_model).map(Ok))
294 }
295}
296
297impl Drop for TappedStream {
298 fn drop(&mut self) {
299 let Some((summary, ctx)) = self.take_summary() else {
300 return;
301 };
302 finalize(Arc::clone(&self.audit), summary, ctx, "drop");
303 }
304}