use std::sync::Arc;
use std::time::Duration;
use async_stream::stream;
use futures::{Stream, StreamExt};
use crate::executor::error::{ExecutorError, ExecutorResult};
use crate::proxy::processed_response_headers;
pub type BoxStream = std::pin::Pin<Box<dyn Stream<Item = String> + Send>>;
pub(super) const DONE_MARKER: &str = "data: [DONE]\n\n";
pub(super) async fn next_chunk<S>(stream: &mut S, timeout: Duration) -> ExecutorResult<Option<bytes::Bytes>>
where
S: futures::Stream<Item = Result<bytes::Bytes, reqwest::Error>> + Unpin,
{
let item = if timeout.is_zero() {
stream.next().await
} else {
tokio::time::timeout(timeout, stream.next()).await.map_err(|_| {
ExecutorError::StreamError("chunk timeout: no data received within the configured window".into())
})?
};
item.transpose().map_err(ExecutorError::NetworkError)
}
fn drain_complete_utf8_lines(buffer: &mut Vec<u8>) -> Vec<String> {
let mut lines = Vec::new();
while let Some(pos) = buffer.iter().position(|byte| *byte == b'\n') {
let line = buffer.drain(..=pos).collect::<Vec<_>>();
let line_end = if pos > 0 && line.get(pos - 1) == Some(&b'\r') {
pos - 1
} else {
pos
};
if let Ok(line) = std::str::from_utf8(&line[..line_end]) {
lines.push(line.to_string());
}
}
lines
}
pub(super) async fn send_request(
client: &reqwest::Client,
url: &str,
body: String,
auth: Option<&str>,
forwarded_headers: Option<&reqwest::header::HeaderMap>,
) -> ExecutorResult<reqwest::Response> {
let mut headers = forwarded_headers.cloned().unwrap_or_default();
headers
.entry(reqwest::header::CONTENT_TYPE)
.or_insert(reqwest::header::HeaderValue::from_static("application/json"));
let mut req = client.post(url).headers(headers).body(body);
if let Some(key) = auth {
req = req.bearer_auth(key);
}
let resp = req.send().await.map_err(|e| ExecutorError::LLMTransport {
status: if e.is_timeout() {
http::StatusCode::GATEWAY_TIMEOUT
} else {
http::StatusCode::BAD_GATEWAY
},
message: if e.is_timeout() {
"LLM timeout"
} else {
"LLM unavailable"
},
})?;
if !resp.status().is_success() {
let status = resp.status().as_u16();
let headers = processed_response_headers(resp.headers());
let body = resp
.text()
.await
.inspect_err(|e| tracing::debug!("failed to read error response body: {e}"))
.unwrap_or_default();
return Err(ExecutorError::LLMRequest {
status: http::StatusCode::from_u16(status).unwrap_or(http::StatusCode::INTERNAL_SERVER_ERROR),
body,
headers,
});
}
Ok(resp)
}
pub(super) async fn fetch_response_json(
upstream_json: String,
url: &str,
client: &reqwest::Client,
auth: Option<&str>,
) -> ExecutorResult<String> {
let resp = send_request(client, url, upstream_json, auth, None).await?;
resp.text().await.map_err(ExecutorError::NetworkError)
}
pub(super) async fn fetch_response_json_with_headers(
upstream_json: String,
url: &str,
client: &reqwest::Client,
headers: &reqwest::header::HeaderMap,
) -> ExecutorResult<(String, http::HeaderMap)> {
let resp = send_request(client, url, upstream_json, None, Some(headers)).await?;
let response_headers = processed_response_headers(resp.headers());
let body = resp.text().await.map_err(ExecutorError::NetworkError)?;
Ok((body, response_headers))
}
pub fn call_inference(
upstream_json: String,
url: String,
client: Arc<reqwest::Client>,
auth: Option<String>,
chunk_timeout: Duration,
) -> impl Stream<Item = Result<String, ExecutorError>> + Send + 'static {
stream! {
let resp = match send_request(&client, &url, upstream_json, auth.as_deref(), None).await {
Ok(r) => r,
Err(e) => { yield Err(e); return; }
};
let mut lines = Box::pin(response_lines(resp, chunk_timeout));
while let Some(line) = lines.next().await {
yield line;
}
}
}
pub(super) fn response_lines(
resp: reqwest::Response,
chunk_timeout: Duration,
) -> impl Stream<Item = Result<String, ExecutorError>> + Send + 'static {
stream! {
let mut bytes = resp.bytes_stream();
let mut buf = Vec::with_capacity(8192);
loop {
let chunk = match next_chunk(&mut bytes, chunk_timeout).await {
Ok(Some(c)) => c,
Ok(None) => break,
Err(e) => { yield Err(e); return; }
};
buf.extend_from_slice(&chunk);
for line in drain_complete_utf8_lines(&mut buf) {
match line.as_str() {
"data: [DONE]" => return,
l if l.starts_with("data: ") => yield Ok(line),
_ => {}
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn utf8_line_reader_preserves_split_multibyte_characters() {
let snowman = "\u{2603}";
let line = format!(r#"data: {{"delta":"snow {snowman}"}}"#);
let bytes = format!("{line}\n").into_bytes();
let split_at = bytes
.windows(snowman.len())
.position(|window| window == snowman.as_bytes())
.expect("snowman bytes present")
+ 1;
let mut buffer = bytes[..split_at].to_vec();
assert!(drain_complete_utf8_lines(&mut buffer).is_empty());
buffer.extend_from_slice(&bytes[split_at..]);
let lines = drain_complete_utf8_lines(&mut buffer);
assert!(buffer.is_empty());
assert_eq!(lines, vec![line]);
assert!(!lines[0].contains('\u{FFFD}'));
}
}