use super::{summarize_one_model, SummarizerOpts};
use std::time::Duration;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::{TcpListener, TcpStream};
const PIECE_GAP: Duration = Duration::from_secs(40);
struct Reply {
frames: &'static [&'static str],
whole: &'static str,
stall: bool,
}
async fn read_json_request(sock: &mut TcpStream) -> Option<serde_json::Value> {
let mut bytes = Vec::new();
let mut chunk = [0u8; 4096];
let header_end = loop {
if let Some(end) = bytes.windows(4).position(|w| w == b"\r\n\r\n") {
break end + 4;
}
match sock.read(&mut chunk).await {
Ok(0) | Err(_) => return None,
Ok(n) => bytes.extend_from_slice(&chunk[..n]),
}
};
let headers = String::from_utf8_lossy(&bytes[..header_end]).to_ascii_lowercase();
let len: usize = headers
.lines()
.find_map(|line| line.strip_prefix("content-length:"))
.and_then(|v| v.trim().parse().ok())
.unwrap_or(0);
while bytes.len() < header_end + len {
match sock.read(&mut chunk).await {
Ok(0) | Err(_) => return None,
Ok(n) => bytes.extend_from_slice(&chunk[..n]),
}
}
serde_json::from_slice(&bytes[header_end..header_end + len]).ok()
}
async fn serve(mut sock: TcpStream, reply: &'static Reply) {
let Some(body) = read_json_request(&mut sock).await else {
return;
};
if body["stream"] == true {
let head = b"HTTP/1.1 200 OK\r\nContent-Type: application/json\r\n\
Transfer-Encoding: chunked\r\nConnection: close\r\n\r\n";
if sock.write_all(head).await.is_err() {
return;
}
for (i, frame) in reply.frames.iter().enumerate() {
if i > 0 {
tokio::time::sleep(PIECE_GAP).await;
}
let chunk = format!("{:x}\r\n{frame}\r\n", frame.len());
if sock.write_all(chunk.as_bytes()).await.is_err() {
return;
}
if i == 0 && reply.stall {
std::future::pending::<()>().await;
}
}
let _ = sock.write_all(b"0\r\n\r\n").await;
} else {
tokio::time::sleep(PIECE_GAP * reply.frames.len() as u32).await;
let head = format!(
"HTTP/1.1 200 OK\r\nContent-Type: application/json\r\n\
Content-Length: {}\r\nConnection: close\r\n\r\n",
reply.whole.len()
);
let _ = sock.write_all(head.as_bytes()).await;
let _ = sock.write_all(reply.whole.as_bytes()).await;
}
}
async fn spawn_stub(reply: &'static Reply) -> String {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
tokio::spawn(async move {
loop {
let Ok((sock, _)) = listener.accept().await else {
return;
};
tokio::spawn(serve(sock, reply));
}
});
format!("http://{addr}")
}
static OLLAMA_STREAM: Reply = Reply {
frames: &[
"{\"message\":{\"role\":\"assistant\",\"content\":\"ok \"},\"done\":false}\n",
"{\"message\":{\"role\":\"assistant\",\"content\":\"summary\"},\"done\":false}\n",
"{\"message\":{\"role\":\"assistant\",\"content\":\"\"},\"done\":true,\"prompt_eval_count\":1,\"eval_count\":1}\n",
],
whole: "{\"message\":{\"role\":\"assistant\",\"content\":\"ok summary\"},\"done\":true}",
stall: false,
};
static OLLAMA_STALL: Reply = Reply {
frames: &[
"{\"message\":{\"role\":\"assistant\",\"content\":\"ok \"},\"done\":false}\n",
"{\"message\":{\"role\":\"assistant\",\"content\":\"summary\"},\"done\":false}\n",
],
whole: "{\"message\":{\"role\":\"assistant\",\"content\":\"ok summary\"},\"done\":true}",
stall: true,
};
static OPENAI_STREAM: Reply = Reply {
frames: &[
"data: {\"choices\":[{\"index\":0,\"delta\":{\"content\":\"ok \"}}]}\n\n",
"data: {\"choices\":[{\"index\":0,\"delta\":{\"content\":\"summary\"}}]}\n\n",
"data: {\"choices\":[{\"index\":0,\"delta\":{},\"finish_reason\":\"stop\"}],\"usage\":{\"prompt_tokens\":1,\"completion_tokens\":1}}\n\n",
"data: [DONE]\n\n",
],
whole: "{\"choices\":[{\"message\":{\"content\":\"ok summary\"}}]}",
stall: false,
};
static OPENAI_STALL: Reply = Reply {
frames: &[
"data: {\"choices\":[{\"index\":0,\"delta\":{\"content\":\"ok \"}}]}\n\n",
"data: {\"choices\":[{\"index\":0,\"delta\":{\"content\":\"summary\"}}]}\n\n",
],
whole: "{\"choices\":[{\"message\":{\"content\":\"ok summary\"}}]}",
stall: true,
};
const SETTLE_YIELDS: usize = 500;
async fn drive<F: std::future::Future>(fut: F) -> F::Output {
let mut fut = std::pin::pin!(fut);
loop {
tokio::select! {
biased;
out = &mut fut => return out,
() = async {
for _ in 0..SETTLE_YIELDS {
tokio::task::yield_now().await;
}
tokio::time::advance(PIECE_GAP).await;
} => {}
}
}
}
static OLLAMA_DRIP: Reply = Reply {
frames: &["{\"message\":{\"role\":\"assistant\",\"content\":\"x\"},\"done\":false}\n"; 11],
whole: "",
stall: false,
};
static OPENAI_DRIP: Reply = Reply {
frames: &["data: {\"choices\":[{\"index\":0,\"delta\":{\"content\":\"x\"}}]}\n\n"; 11],
whole: "",
stall: false,
};
fn opts() -> SummarizerOpts {
SummarizerOpts {
timeout_secs: 60,
retries: 0,
..Default::default()
}
}
#[tokio::test(start_paused = true)]
async fn ollama_stream_running_past_timeout_secs_succeeds() {
let url = spawn_stub(&OLLAMA_STREAM).await;
let result = drive(summarize_one_model(
&url,
"m",
false,
"prompt",
&opts(),
&None,
))
.await;
let (summary, _) = result.unwrap_or_else(|e| {
panic!("an Ollama stream that keeps producing frames was cut off: {e:#}")
});
assert_eq!(summary, "ok summary");
}
#[tokio::test(start_paused = true)]
async fn openai_stream_running_past_timeout_secs_succeeds() {
let url = spawn_stub(&OPENAI_STREAM).await;
let result = drive(summarize_one_model(
&url,
"m",
true,
"prompt",
&opts(),
&None,
))
.await;
let (summary, _) = result.unwrap_or_else(|e| {
panic!("an OpenAI stream that keeps producing frames was cut off: {e:#}")
});
assert_eq!(summary, "ok summary");
}
#[tokio::test(start_paused = true)]
async fn ollama_stream_stalled_mid_stream_fails_on_idle_bound() {
let url = spawn_stub(&OLLAMA_STALL).await;
let err = drive(summarize_one_model(
&url,
"m",
false,
"prompt",
&opts(),
&None,
))
.await
.expect_err("a stream silent past the idle bound must fail");
assert!(
err.downcast_ref::<reqwest::Error>()
.is_some_and(reqwest::Error::is_timeout),
"expected an idle-bound timeout, got: {err:#}"
);
}
#[tokio::test(start_paused = true)]
async fn openai_stream_stalled_mid_stream_fails_on_idle_bound() {
let url = spawn_stub(&OPENAI_STALL).await;
let err = drive(summarize_one_model(
&url,
"m",
true,
"prompt",
&opts(),
&None,
))
.await
.expect_err("a stream silent past the idle bound must fail");
assert!(
err.downcast_ref::<reqwest::Error>()
.is_some_and(reqwest::Error::is_timeout),
"expected an idle-bound timeout, got: {err:#}"
);
}
#[tokio::test(start_paused = true)]
async fn steady_stream_past_five_times_the_bound_hits_the_total_backstop() {
let url = spawn_stub(&OLLAMA_DRIP).await;
let err = drive(summarize_one_model(
&url,
"m",
false,
"prompt",
&opts(),
&None,
))
.await
.expect_err("a stream that never finishes must hit the total bound");
assert!(
err.to_string().contains("total bound"),
"expected the total backstop, got: {err:#}"
);
}
#[tokio::test(start_paused = true)]
async fn openai_steady_stream_past_five_times_the_bound_hits_the_total_backstop() {
let url = spawn_stub(&OPENAI_DRIP).await;
let err = drive(summarize_one_model(
&url,
"m",
true,
"prompt",
&opts(),
&None,
))
.await
.expect_err("a stream that never finishes must hit the total bound");
assert!(
err.to_string().contains("total bound"),
"expected the total backstop, got: {err:#}"
);
}