| use anyhow::Context; |
| use anyhow::Result; |
| use std::io; |
| use std::io::Read; |
| use std::io::Write; |
| use std::net::TcpListener; |
| use std::net::TcpStream; |
| use std::sync::Arc; |
| use std::sync::atomic::AtomicBool; |
| use std::sync::atomic::Ordering; |
| use std::thread; |
| use std::thread::JoinHandle; |
| use std::time::Duration; |
|
|
| pub(super) struct LoopbackResponsesServer { |
| base_url: String, |
| shutdown: Arc<AtomicBool>, |
| thread: Option<JoinHandle<()>>, |
| } |
|
|
| impl LoopbackResponsesServer { |
| pub(super) fn start() -> Result<Self> { |
| let listener = |
| TcpListener::bind("127.0.0.1:0").context("bind loopback Responses API server")?; |
| listener |
| .set_nonblocking(true) |
| .context("set loopback Responses API server nonblocking")?; |
| let address = listener.local_addr()?; |
| let shutdown = Arc::new(AtomicBool::new(false)); |
| let thread_shutdown = Arc::clone(&shutdown); |
| let thread = thread::spawn(move || { |
| while !thread_shutdown.load(Ordering::Relaxed) { |
| match listener.accept() { |
| Ok((stream, _)) => { |
| if let Err(err) = handle_model_connection(stream) { |
| eprintln!("loopback Responses API server error: {err}"); |
| } |
| } |
| Err(err) if err.kind() == io::ErrorKind::WouldBlock => { |
| thread::sleep(Duration::from_millis(10)); |
| } |
| Err(err) => { |
| eprintln!("loopback Responses API accept error: {err}"); |
| break; |
| } |
| } |
| } |
| }); |
| Ok(Self { |
| base_url: format!("http://{address}"), |
| shutdown, |
| thread: Some(thread), |
| }) |
| } |
|
|
| pub(super) fn base_url(&self) -> &str { |
| &self.base_url |
| } |
| } |
|
|
| impl Drop for LoopbackResponsesServer { |
| fn drop(&mut self) { |
| self.shutdown.store(true, Ordering::Relaxed); |
| if let Some(thread) = self.thread.take() { |
| let _ = thread.join(); |
| } |
| } |
| } |
|
|
| fn handle_model_connection(mut stream: TcpStream) -> io::Result<()> { |
| stream.set_nonblocking(false)?; |
| stream.set_read_timeout(Some(Duration::from_secs(2)))?; |
| let request = read_http_request(&mut stream)?; |
| let request_line = request |
| .split(|byte| *byte == b'\n') |
| .next() |
| .and_then(|line| std::str::from_utf8(line).ok()) |
| .unwrap_or_default(); |
| if request_line.starts_with("POST ") && request_line.contains("/responses ") { |
| let body = concat!( |
| "event: response.created\n", |
| "data: {\"type\":\"response.created\",\"response\":{\"id\":\"resp-plugin-analytics\"}}\n\n", |
| "event: response.completed\n", |
| "data: {\"type\":\"response.completed\",\"response\":{\"id\":\"resp-plugin-analytics\",\"usage\":{\"input_tokens\":0,\"input_tokens_details\":null,\"output_tokens\":0,\"output_tokens_details\":null,\"total_tokens\":0}}}\n\n" |
| ); |
| write_http_response(&mut stream, "200 OK", "text/event-stream", body) |
| } else { |
| write_http_response( |
| &mut stream, |
| "404 Not Found", |
| "application/json", |
| r#"{"error":"not found"}"#, |
| ) |
| } |
| } |
|
|
| fn read_http_request(stream: &mut TcpStream) -> io::Result<Vec<u8>> { |
| let mut request = Vec::new(); |
| let mut buffer = [0_u8; 4096]; |
| let header_end = loop { |
| let read = stream.read(&mut buffer)?; |
| if read == 0 { |
| return Ok(request); |
| } |
| request.extend_from_slice(&buffer[..read]); |
| if let Some(position) = request.windows(4).position(|window| window == b"\r\n\r\n") { |
| break position + 4; |
| } |
| }; |
| let content_length = parse_content_length(&request[..header_end]); |
| while request.len() < header_end + content_length { |
| let read = stream.read(&mut buffer)?; |
| if read == 0 { |
| break; |
| } |
| request.extend_from_slice(&buffer[..read]); |
| } |
| Ok(request) |
| } |
|
|
| fn parse_content_length(headers: &[u8]) -> usize { |
| String::from_utf8_lossy(headers) |
| .lines() |
| .find_map(|line| { |
| let (name, value) = line.split_once(':')?; |
| name.eq_ignore_ascii_case("content-length") |
| .then(|| value.trim().parse().ok()) |
| .flatten() |
| }) |
| .unwrap_or(0) |
| } |
|
|
| fn write_http_response( |
| stream: &mut TcpStream, |
| status: &str, |
| content_type: &str, |
| body: &str, |
| ) -> io::Result<()> { |
| write!( |
| stream, |
| "HTTP/1.1 {status}\r\nContent-Type: {content_type}\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}", |
| body.len() |
| )?; |
| stream.flush() |
| } |
|
|