#![cfg(feature = "http-backend")]
use std::io::{Read, Write};
use std::net::TcpListener;
use std::sync::atomic::AtomicBool;
use std::sync::Arc;
use std::thread;
use orion_core::backends::{OpenAiConfig, OpenAiEndpoint, OpenAiHttpBackend};
use orion_core::{CoreError, InferenceParams, LlmBackend};
fn spawn_server(
status_line: &'static str,
content_type: &'static str,
body: String,
) -> (String, thread::JoinHandle<String>) {
let listener = TcpListener::bind("127.0.0.1:0").expect("bind");
let addr = listener.local_addr().expect("addr");
let handle = thread::spawn(move || -> String {
let mut captured = String::new();
if let Ok((mut stream, _)) = listener.accept() {
let mut buf = [0u8; 4096];
let n = stream.read(&mut buf).unwrap_or(0);
captured = String::from_utf8_lossy(&buf[..n]).into_owned();
let response = format!(
"{status_line}\r\nContent-Type: {content_type}\r\nContent-Length: {}\r\n\
Connection: close\r\n\r\n{body}",
body.len()
);
let _ = stream.write_all(response.as_bytes());
let _ = stream.flush();
}
captured
});
(format!("http://{addr}/v1"), handle)
}
fn backend(base_url: String) -> OpenAiHttpBackend {
OpenAiHttpBackend::new(OpenAiConfig::new(base_url, "test-model")).expect("build backend")
}
fn backend_with(base_url: String, endpoint: OpenAiEndpoint) -> OpenAiHttpBackend {
OpenAiHttpBackend::new(OpenAiConfig::new(base_url, "test-model").with_endpoint(endpoint))
.expect("build backend")
}
fn no_abort() -> Arc<AtomicBool> {
Arc::new(AtomicBool::new(false))
}
const SSE_STREAM: &str = "\
data: {\"choices\":[{\"delta\":{\"content\":\"Hello\"}}]}\n\
\n\
data: {\"choices\":[{\"delta\":{\"content\":\", \"}}]}\n\
\n\
data: {\"choices\":[{\"delta\":{\"content\":\"world!\"}}]}\n\
\n\
data: {\"choices\":[],\"usage\":{\"prompt_tokens\":11,\"completion_tokens\":3,\"total_tokens\":14}}\n\
\n\
data: [DONE]\n\
\n";
#[test]
fn streams_tokens_and_maps_usage() {
let (url, srv) = spawn_server(
"HTTP/1.1 200 OK",
"text/event-stream",
SSE_STREAM.to_string(),
);
let backend = backend(url);
let collected = Arc::new(std::sync::Mutex::new(String::new()));
let sink = collected.clone();
let on_token = Box::new(move |tok: &str, _n: u32, _tps: f64| {
sink.lock().unwrap().push_str(tok);
});
let result = backend
.generate("hi", &InferenceParams::default(), no_abort(), on_token)
.expect("generate");
assert_eq!(*collected.lock().unwrap(), "Hello, world!");
assert_eq!(result.text, "Hello, world!");
assert_eq!(result.prompt_tokens, 11);
assert_eq!(result.tokens_generated, 3);
let request = srv.join().expect("server thread");
assert!(request.contains("POST /v1/chat/completions"), "{request}");
assert!(request.contains("\"messages\""), "{request}");
assert!(!request.contains("\"prompt\""), "{request}");
}
#[test]
fn completions_endpoint_sends_raw_prompt() {
let stream = "\
data: {\"choices\":[{\"text\":\"raw \"}]}\n\
\n\
data: {\"choices\":[{\"text\":\"answer\"}]}\n\
\n\
data: {\"choices\":[],\"usage\":{\"prompt_tokens\":5,\"completion_tokens\":2,\"total_tokens\":7}}\n\
\n\
data: [DONE]\n\
\n";
let (url, srv) = spawn_server("HTTP/1.1 200 OK", "text/event-stream", stream.to_string());
let backend = backend_with(url, OpenAiEndpoint::Completions);
let result = backend
.generate(
"<|im_start|>user\nhi<|im_end|>",
&InferenceParams::default(),
no_abort(),
Box::new(|_, _, _| {}),
)
.expect("generate");
assert_eq!(result.text, "raw answer");
assert_eq!(result.tokens_generated, 2);
assert_eq!(result.prompt_tokens, 5);
let request = srv.join().expect("server thread");
assert!(request.contains("POST /v1/completions"), "{request}");
assert!(request.contains("\"prompt\""), "{request}");
assert!(!request.contains("\"messages\""), "{request}");
assert!(request.contains("im_start"), "{request}");
}
#[test]
fn falls_back_to_streamed_count_without_usage() {
let no_usage = "\
data: {\"choices\":[{\"delta\":{\"content\":\"a\"}}]}\n\
\n\
data: {\"choices\":[{\"delta\":{\"content\":\"b\"}}]}\n\
\n\
data: [DONE]\n\
\n";
let (url, _srv) = spawn_server("HTTP/1.1 200 OK", "text/event-stream", no_usage.to_string());
let backend = backend(url);
let result = backend
.generate(
"hi",
&InferenceParams::default(),
no_abort(),
Box::new(|_, _, _| {}),
)
.expect("generate");
assert_eq!(result.text, "ab");
assert_eq!(result.tokens_generated, 2);
assert_eq!(result.prompt_tokens, 0);
}
#[test]
fn aborts_mid_stream() {
let (url, _srv) = spawn_server(
"HTTP/1.1 200 OK",
"text/event-stream",
SSE_STREAM.to_string(),
);
let backend = backend(url);
let abort = Arc::new(AtomicBool::new(true));
let err = backend
.generate(
"hi",
&InferenceParams::default(),
abort,
Box::new(|_, _, _| {}),
)
.expect_err("should abort");
assert!(matches!(err, CoreError::Aborted), "got {err:?}");
}
#[test]
fn endpoint_error_maps_to_backend() {
let (url, _srv) = spawn_server(
"HTTP/1.1 500 Internal Server Error",
"application/json",
"{\"error\":\"boom\"}".to_string(),
);
let backend = backend(url);
let err = backend
.generate(
"hi",
&InferenceParams::default(),
no_abort(),
Box::new(|_, _, _| {}),
)
.expect_err("should error");
match err {
CoreError::Backend(msg) => {
assert!(
msg.contains("500"),
"message should carry the status: {msg}"
);
}
other => panic!("expected Backend, got {other:?}"),
}
}
#[test]
fn unreachable_endpoint_maps_to_backend_unreachable() {
let listener = TcpListener::bind("127.0.0.1:0").expect("bind");
let addr = listener.local_addr().expect("addr");
drop(listener);
let url = format!("http://{addr}/v1");
let backend = backend(url);
let err = backend
.generate(
"hi",
&InferenceParams::default(),
no_abort(),
Box::new(|_, _, _| {}),
)
.expect_err("should fail to connect");
assert!(
matches!(err, CoreError::BackendUnreachable(_)),
"got {err:?}"
);
}