#![allow(dead_code)]
use std::collections::HashMap;
use std::fmt::Write as _;
use std::net::SocketAddr;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{Arc, Mutex};
use async_trait::async_trait;
use tokio::io::{AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader};
use tokio::net::{TcpListener, TcpStream};
use hey::llm::ir::{ChatRequest, Completion, ToolCall, Usage};
use hey::llm::{Delta, LlmError, Provider};
#[derive(Debug, Clone)]
pub struct MockRequest {
pub method: String,
pub path: String,
pub headers: HashMap<String, String>,
pub body: String,
}
impl MockRequest {
pub fn header(&self, name: &str) -> Option<&str> {
self.headers
.iter()
.find(|(k, _)| k.eq_ignore_ascii_case(name))
.map(|(_, v)| v.as_str())
}
}
#[derive(Debug, Clone)]
pub struct MockResponse {
pub status: u16,
pub headers: Vec<(String, String)>,
pub body: String,
}
impl MockResponse {
pub fn sse(body: &str) -> Self {
Self {
status: 200,
headers: vec![
("Content-Type".into(), "text/event-stream".into()),
("Cache-Control".into(), "no-cache".into()),
],
body: body.to_string(),
}
}
pub fn status(status: u16, body: &str) -> Self {
Self {
status,
headers: vec![("Content-Type".into(), "application/json".into())],
body: body.to_string(),
}
}
}
pub struct MockServer {
pub addr: SocketAddr,
pub requests: Arc<Mutex<Vec<MockRequest>>>,
responses: Arc<Mutex<Vec<MockResponse>>>,
counter: Arc<AtomicUsize>,
shutdown: Option<tokio::sync::oneshot::Sender<()>>,
}
impl MockServer {
pub async fn start(responses: Vec<MockResponse>) -> Self {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
let requests: Arc<Mutex<Vec<MockRequest>>> = Arc::new(Mutex::new(Vec::new()));
let responses = Arc::new(Mutex::new(responses));
let counter = Arc::new(AtomicUsize::new(0));
let (tx, mut rx) = tokio::sync::oneshot::channel::<()>();
let reqs = requests.clone();
let resps = responses.clone();
let cnt = counter.clone();
tokio::spawn(async move {
loop {
tokio::select! {
_ = &mut rx => break,
accepted = listener.accept() => {
let Ok((stream, _)) = accepted else { continue };
let reqs = reqs.clone();
let resps = resps.clone();
let cnt = cnt.clone();
tokio::spawn(async move {
let _ = handle_conn(stream, reqs, resps, cnt).await;
});
}
}
}
});
Self {
addr,
requests,
responses,
counter,
shutdown: Some(tx),
}
}
pub fn count(&self) -> usize {
self.requests.lock().unwrap().len()
}
pub fn request(&self, i: usize) -> Option<MockRequest> {
self.requests.lock().unwrap().get(i).cloned()
}
}
impl Drop for MockServer {
fn drop(&mut self) {
if let Some(tx) = self.shutdown.take() {
let _ = tx.send(());
}
}
}
async fn handle_conn(
mut stream: TcpStream,
reqs: Arc<Mutex<Vec<MockRequest>>>,
resps: Arc<Mutex<Vec<MockResponse>>>,
counter: Arc<AtomicUsize>,
) -> std::io::Result<()> {
let mut reader = BufReader::new(&mut stream);
let mut head = String::new();
loop {
let mut line = String::new();
let n = reader.read_line(&mut line).await?;
if n == 0 {
return Ok(());
}
head.push_str(&line);
if line == "\r\n" || line == "\n" {
break;
}
}
let mut lines = head.lines();
let request_line = lines.next().unwrap_or_default().to_string();
let mut parts = request_line.split_whitespace();
let method = parts.next().unwrap_or("").to_string();
let path = parts.next().unwrap_or("").to_string();
let mut headers = HashMap::new();
let mut content_length = 0usize;
for l in lines {
if let Some((k, v)) = l.split_once(':') {
headers.insert(k.trim().to_string(), v.trim().to_string());
if k.trim().eq_ignore_ascii_case("content-length") {
content_length = v.trim().parse().unwrap_or(0);
}
}
}
let mut body = String::new();
if content_length > 0 {
let mut buf = vec![0u8; content_length];
reader.read_exact(&mut buf).await?;
body = String::from_utf8_lossy(&buf).to_string();
}
reqs.lock().unwrap().push(MockRequest {
method,
path,
headers,
body,
});
let idx = counter.fetch_add(1, Ordering::SeqCst);
let resp = {
let r = resps.lock().unwrap();
r.get(idx).or_else(|| r.last()).cloned()
};
let Some(resp) = resp else {
return Ok(());
};
let reason = match resp.status {
200 => "OK",
400 => "Bad Request",
401 => "Unauthorized",
402 => "Payment Required",
429 => "Too Many Requests",
500 => "Internal Server Error",
503 => "Service Unavailable",
_ => "OK",
};
let mut out = format!("HTTP/1.1 {} {}\r\n", resp.status, reason);
for (k, v) in &resp.headers {
let _ = write!(out, "{k}: {v}\r\n");
}
let _ = write!(out, "Content-Length: {}\r\n", resp.body.len());
out.push_str("\r\n");
out.push_str(&resp.body);
stream.write_all(out.as_bytes()).await?;
stream.flush().await?;
Ok(())
}
#[derive(Debug, Clone)]
pub struct FakeTurn {
pub text: &'static str,
pub tool_calls: Vec<ToolCall>,
}
pub struct FakeProvider {
pub model: String,
pub script: Vec<FakeTurn>,
pub calls: Arc<AtomicUsize>,
}
impl FakeProvider {
pub fn new(model: &str, script: Vec<FakeTurn>) -> Self {
Self {
model: model.to_string(),
script,
calls: Arc::new(AtomicUsize::new(0)),
}
}
pub fn call_count(&self) -> usize {
self.calls.load(Ordering::SeqCst)
}
}
#[async_trait]
impl Provider for FakeProvider {
fn model(&self) -> &str {
&self.model
}
async fn stream(
&self,
_req: &ChatRequest,
on_delta: &mut (dyn FnMut(Delta) + Send),
) -> Result<Completion, LlmError> {
let i = self.calls.fetch_add(1, Ordering::SeqCst);
let turn = &self.script[i.min(self.script.len() - 1)];
if !turn.text.is_empty() {
on_delta(Delta::Text(turn.text.to_string()));
}
Ok(Completion {
text: turn.text.to_string(),
thinking: String::new(),
tool_calls: turn.tool_calls.clone(),
usage: Usage {
input: 10,
output: turn.text.len() as u64,
},
})
}
}
pub struct FlakyProvider {
pub model: String,
pub script: Vec<Result<FakeTurn, LlmError>>,
pub calls: Arc<AtomicUsize>,
pub partial_before_err: bool,
}
impl FlakyProvider {
pub fn new(model: &str, script: Vec<Result<FakeTurn, LlmError>>) -> Self {
Self {
model: model.to_string(),
script,
calls: Arc::new(AtomicUsize::new(0)),
partial_before_err: false,
}
}
pub fn call_count(&self) -> usize {
self.calls.load(Ordering::SeqCst)
}
}
#[async_trait]
impl Provider for FlakyProvider {
fn model(&self) -> &str {
&self.model
}
async fn stream(
&self,
_req: &ChatRequest,
on_delta: &mut (dyn FnMut(Delta) + Send),
) -> Result<Completion, LlmError> {
let i = self.calls.fetch_add(1, Ordering::SeqCst);
let step = &self.script[i.min(self.script.len() - 1)];
match step {
Ok(turn) => {
if !turn.text.is_empty() {
on_delta(Delta::Text(turn.text.to_string()));
}
Ok(Completion {
text: turn.text.to_string(),
thinking: String::new(),
tool_calls: turn.tool_calls.clone(),
usage: Usage::default(),
})
}
Err(e) => {
if self.partial_before_err {
on_delta(Delta::Text("partial".to_string()));
}
Err(e.clone())
}
}
}
}
pub fn tool_call(id: &str, name: &str, args: &str) -> ToolCall {
ToolCall {
id: id.to_string(),
name: name.to_string(),
arguments: args.to_string(),
}
}