use futures_util::StreamExt;
use super::contract::{ApiMessage, ChatChunk, ChatRequest, ChatStream};
pub(crate) fn serve(events: &'static [&'static str]) -> String {
let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
let port = listener.local_addr().unwrap().port();
std::thread::spawn(move || {
use std::io::{Read, Write};
let Ok((mut sock, _)) = listener.accept() else {
return;
};
let mut buf = [0u8; 4096];
let _ = sock.read(&mut buf);
let mut body = String::new();
for e in events {
body.push_str(&format!("data: {e}\n\n"));
}
let _ = sock.write_all(
format!(
"HTTP/1.1 200 OK\r\nContent-Type: text/event-stream\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}",
body.len()
)
.as_bytes(),
);
let _ = sock.flush();
});
format!("http://127.0.0.1:{port}")
}
pub(crate) type Canned = (&'static str, &'static str, &'static str);
pub(crate) fn serve_in_turn(
answers: &'static [Canned],
) -> (String, std::thread::JoinHandle<Vec<String>>) {
let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
let port = listener.local_addr().unwrap().port();
listener.set_nonblocking(true).unwrap();
let handle = std::thread::spawn(move || {
use std::io::Write;
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(10);
let mut bodies = Vec::new();
for (status, kind, answer) in answers {
let mut sock = loop {
match listener.accept() {
Ok((sock, _)) => break sock,
Err(_) if std::time::Instant::now() >= deadline => return bodies,
Err(_) => std::thread::sleep(std::time::Duration::from_millis(5)),
}
};
sock.set_nonblocking(false).unwrap();
bodies.push(request_body(&mut sock));
let _ = sock.write_all(
format!(
"HTTP/1.1 {status}\r\nContent-Type: {kind}\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{answer}",
answer.len()
)
.as_bytes(),
);
let _ = sock.flush();
}
bodies
});
(format!("http://127.0.0.1:{port}"), handle)
}
fn request_body(sock: &mut std::net::TcpStream) -> String {
use std::io::Read;
let mut raw = Vec::new();
let mut chunk = [0u8; 8192];
loop {
let text = String::from_utf8_lossy(&raw).to_string();
if let Some((head, body)) = text.split_once("\r\n\r\n") {
let want = head
.lines()
.filter_map(|l| l.split_once(':'))
.find(|(name, _)| name.eq_ignore_ascii_case("content-length"))
.and_then(|(_, len)| len.trim().parse::<usize>().ok())
.unwrap_or(0);
if body.len() >= want {
return body.to_string();
}
}
match sock.read(&mut chunk) {
Ok(n) if n > 0 => raw.extend_from_slice(&chunk[..n]),
_ => return String::new(),
}
}
}
pub(crate) async fn collect(stream: ChatStream) -> Vec<ChatChunk> {
let mut out = Vec::new();
let mut stream = stream;
while let Some(c) = stream.next().await {
let last = matches!(c, ChatChunk::Finished(_));
out.push(c);
if last {
break;
}
}
out
}
pub(crate) const JSON: &str = "application/json";
pub(crate) const BAD_REQUEST: &str = "400 Bad Request";
pub(crate) fn muted() -> ChatRequest {
let mut req = hello();
req.sampling.thinking = Some(false);
req.sampling.reasoning_effort = Some(crate::entities::sampling::ReasoningEffort::None);
req.sampling.reasoning_budget = Some(0);
req
}
pub(crate) fn hello() -> ChatRequest {
ChatRequest {
continue_final: false,
system: None,
messages: vec![ApiMessage::user("hi")],
sampling: Default::default(),
tools: vec![],
}
}