use super::RequestLog;
#[derive(Debug, Clone)]
pub struct StreamOutcome {
pub streamed: bool,
pub terminated: bool,
pub inspectable: bool,
pub detail: Option<String>,
pub frames: u64,
pub bytes: u64,
pub duration_ms: u128,
}
impl StreamOutcome {
#[must_use]
pub const fn is_complete(&self) -> bool {
if !self.streamed {
return self.detail.is_none();
}
if !self.inspectable {
return false;
}
self.terminated && self.detail.is_none()
}
#[must_use]
pub const fn is_demonstrably_cut(&self) -> bool {
if self.detail.is_some() {
return true;
}
self.streamed && self.inspectable && !self.terminated
}
#[must_use]
pub const fn label(&self) -> &'static str {
if self.detail.is_some() {
"upstream_error"
} else if !self.streamed {
"completed_not_streamed"
} else if self.terminated {
"completed"
} else if self.inspectable {
"ended_without_terminator"
} else {
"encoded_not_verifiable"
}
}
}
pub const STREAM_END_MARKER: &str = "stream-end marker";
pub fn settle_stream(
log: &RequestLog,
correlation_id: &str,
outcome: &std::sync::Mutex<StreamOutcome>,
duration_ms: u128,
logger: &log_lazy::LogLazy,
) {
let outcome = {
let mut outcome = outcome.lock().expect("stream outcome lock");
outcome.duration_ms = duration_ms;
outcome.clone()
};
if stream_warrants_a_warning(&outcome) {
logger.warn(|| {
format!(
"stream {correlation_id} ended without its terminator after {} frames in {}ms{}",
outcome.frames,
outcome.duration_ms,
outcome
.detail
.as_ref()
.map_or_else(String::new, |detail| format!(": {detail}"))
)
});
}
log.record_stream_end(correlation_id, &outcome);
}
#[must_use]
pub const fn stream_warrants_a_warning(outcome: &StreamOutcome) -> bool {
outcome.is_demonstrably_cut()
}
#[must_use]
pub fn body_is_inspectable(headers: &reqwest::header::HeaderMap) -> bool {
headers
.get(reqwest::header::CONTENT_ENCODING)
.and_then(|value| value.to_str().ok())
.is_none_or(|encoding| {
encoding
.split(',')
.all(|part| part.trim().is_empty() || part.trim().eq_ignore_ascii_case("identity"))
})
}
#[must_use]
pub fn response_is_streamed(headers: &reqwest::header::HeaderMap) -> bool {
is_streaming_media_type(
headers
.get(reqwest::header::CONTENT_TYPE)
.and_then(|value| value.to_str().ok()),
)
}
#[must_use]
pub fn is_streaming_media_type(content_type: Option<&str>) -> bool {
content_type.is_none_or(|value| {
value
.split(';')
.next()
.unwrap_or_default()
.trim()
.eq_ignore_ascii_case("text/event-stream")
})
}
#[must_use]
pub fn frame_terminates_stream(frame: &[u8]) -> bool {
let Ok(text) = std::str::from_utf8(frame) else {
return false;
};
text_terminates_stream(text)
}
#[must_use]
pub fn text_terminates_stream(text: &str) -> bool {
text.contains("message_stop")
|| text.contains("[DONE]")
|| text.contains("response.completed")
|| text.contains("finishReason")
}