use std::sync::atomic::{AtomicBool, Ordering};
use reqwest::Client;
use serde::{Deserialize, Serialize};
use tracing::{debug, info};
pub struct HttpEngine {
client: Client,
base_url: String,
model_id: String,
ready: AtomicBool,
}
#[derive(Debug, Deserialize)]
struct ModelsResponse {
data: Vec<ModelInfo>,
}
#[derive(Debug, Deserialize)]
struct ModelInfo {
id: String,
}
#[derive(Debug, Serialize)]
struct ChatRequest {
model: String,
messages: Vec<ChatMessage>,
max_tokens: u32,
temperature: f32,
stream: bool,
}
#[derive(Debug, Serialize)]
struct ChatMessage {
role: String,
content: String,
}
#[derive(Debug, Deserialize)]
struct ChatResponse {
choices: Vec<ChatChoice>,
}
#[derive(Debug, Deserialize)]
struct ChatChoice {
message: ChatChoiceMessage,
}
#[derive(Debug, Deserialize)]
struct ChatChoiceMessage {
content: String,
}
#[derive(Debug, Clone)]
pub struct SimpleMessage {
pub role: String,
pub content: String,
}
impl SimpleMessage {
pub fn system(content: impl Into<String>) -> Self {
Self {
role: "system".to_string(),
content: content.into(),
}
}
pub fn user(content: impl Into<String>) -> Self {
Self {
role: "user".to_string(),
content: content.into(),
}
}
pub fn assistant(content: impl Into<String>) -> Self {
Self {
role: "assistant".to_string(),
content: content.into(),
}
}
}
pub type Result<T> = std::result::Result<T, HttpEngineError>;
#[derive(Debug, thiserror::Error)]
pub enum HttpEngineError {
#[error("Connection failed: {0}")]
Connection(String),
#[error("Request failed: {0}")]
Request(String),
#[error("Server error {status}: {message}")]
Server {
status: u16,
message: String,
},
#[error("No model loaded on server")]
NoModel,
}
impl HttpEngine {
pub fn new(base_url: impl Into<String>) -> Self {
Self {
client: Client::new(),
base_url: base_url.into(),
model_id: String::new(),
ready: AtomicBool::new(false),
}
}
pub async fn connect(&mut self) -> Result<()> {
let health_url = format!("{}/health", self.base_url);
let resp = self
.client
.get(&health_url)
.send()
.await
.map_err(|e| HttpEngineError::Connection(e.to_string()))?;
if !resp.status().is_success() {
return Err(HttpEngineError::Connection(
"Server not healthy".to_string(),
));
}
info!("Connected to Infernum at {}", self.base_url);
let models_url = format!("{}/v1/models", self.base_url);
let resp: ModelsResponse = self
.client
.get(&models_url)
.send()
.await
.map_err(|e| HttpEngineError::Request(e.to_string()))?
.json()
.await
.map_err(|e| HttpEngineError::Request(format!("Failed to parse: {}", e)))?;
if let Some(model) = resp.data.first() {
self.model_id = model.id.clone();
self.ready.store(true, Ordering::SeqCst);
info!("Model available: {}", model.id);
Ok(())
} else {
Err(HttpEngineError::NoModel)
}
}
pub fn model_id(&self) -> &str {
&self.model_id
}
pub fn is_ready(&self) -> bool {
self.ready.load(Ordering::SeqCst)
}
pub async fn chat(
&self,
messages: Vec<SimpleMessage>,
max_tokens: u32,
temperature: f32,
) -> Result<String> {
if !self.is_ready() {
return Err(HttpEngineError::NoModel);
}
let url = format!("{}/v1/chat/completions", self.base_url);
let request = ChatRequest {
model: self.model_id.clone(),
messages: messages
.into_iter()
.map(|m| ChatMessage {
role: m.role,
content: m.content,
})
.collect(),
max_tokens,
temperature,
stream: false,
};
debug!("Sending chat request to {}", url);
let resp = self
.client
.post(&url)
.json(&request)
.send()
.await
.map_err(|e| HttpEngineError::Request(e.to_string()))?;
if !resp.status().is_success() {
let status = resp.status().as_u16();
let message = resp.text().await.unwrap_or_default();
return Err(HttpEngineError::Server { status, message });
}
let response: ChatResponse = resp
.json()
.await
.map_err(|e| HttpEngineError::Request(format!("Failed to parse: {}", e)))?;
let content = response
.choices
.first()
.map(|c| c.message.content.clone())
.unwrap_or_default();
debug!("Received response: {} chars", content.len());
Ok(content)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_http_engine_creation() {
let engine = HttpEngine::new("http://localhost:8081");
assert!(!engine.is_ready());
assert!(engine.model_id().is_empty());
}
#[test]
fn test_simple_message() {
let sys = SimpleMessage::system("You are helpful");
assert_eq!(sys.role, "system");
let user = SimpleMessage::user("Hello");
assert_eq!(user.role, "user");
let asst = SimpleMessage::assistant("Hi there");
assert_eq!(asst.role, "assistant");
}
}