use anyhow::{Context, Result};
use reqwest::Client;
use std::time::Duration;
use super::types::*;
pub struct OpenCodeClient {
base_url: String,
client: Client,
}
impl OpenCodeClient {
pub fn new(base_url: &str) -> Self {
let client = Client::builder()
.timeout(Duration::from_secs(300)) .build()
.expect("Failed to create HTTP client");
Self {
base_url: base_url.trim_end_matches('/').to_string(),
client,
}
}
pub fn localhost(port: u16) -> Self {
Self::new(&format!("http://127.0.0.1:{}", port))
}
pub async fn health_check(&self) -> Result<bool> {
let response = self
.client
.get(format!("{}/health", self.base_url))
.timeout(Duration::from_secs(2))
.send()
.await;
match response {
Ok(r) => Ok(r.status().is_success()),
Err(_) => Ok(false),
}
}
pub async fn server_info(&self) -> Result<ServerInfo> {
let response = self
.client
.get(format!("{}/", self.base_url))
.send()
.await
.context("Failed to get server info")?;
if !response.status().is_success() {
let error: ErrorResponse = response.json().await.unwrap_or(ErrorResponse {
error: "Unknown error".to_string(),
details: None,
});
anyhow::bail!("Server error: {}", error.error);
}
response.json().await.context("Failed to parse server info")
}
pub async fn create_session(&self, title: &str) -> Result<Session> {
let request = CreateSessionRequest {
title: title.to_string(),
system_prompt: None,
};
let response = self
.client
.post(format!("{}/session", self.base_url))
.json(&request)
.send()
.await
.context("Failed to create session")?;
if !response.status().is_success() {
let status = response.status();
let error_text = response.text().await.unwrap_or_default();
anyhow::bail!("Failed to create session ({}): {}", status, error_text);
}
response
.json()
.await
.context("Failed to parse session response")
}
pub async fn send_message(
&self,
session_id: &str,
text: &str,
model: Option<(&str, &str)>, ) -> Result<()> {
let request = MessageRequest {
parts: vec![MessagePart::Text {
text: text.to_string(),
}],
model: model.map(|(provider, model_id)| ModelSpec {
provider_id: provider.to_string(),
model_id: model_id.to_string(),
}),
};
let response = self
.client
.post(format!("{}/session/{}/message", self.base_url, session_id))
.json(&request)
.send()
.await
.context("Failed to send message")?;
if !response.status().is_success() {
let status = response.status();
let error_text = response.text().await.unwrap_or_default();
anyhow::bail!("Failed to send message ({}): {}", status, error_text);
}
Ok(())
}
pub async fn get_session_status(&self, session_id: &str) -> Result<SessionStatus> {
let response = self
.client
.get(format!("{}/session/{}", self.base_url, session_id))
.send()
.await
.context("Failed to get session status")?;
if !response.status().is_success() {
let status = response.status();
let error_text = response.text().await.unwrap_or_default();
anyhow::bail!("Failed to get session ({}): {}", status, error_text);
}
response
.json()
.await
.context("Failed to parse session status")
}
pub async fn abort_session(&self, session_id: &str) -> Result<()> {
let response = self
.client
.post(format!("{}/session/{}/abort", self.base_url, session_id))
.send()
.await
.context("Failed to abort session")?;
if !response.status().is_success() {
let status = response.status();
let error_text = response.text().await.unwrap_or_default();
anyhow::bail!("Failed to abort session ({}): {}", status, error_text);
}
Ok(())
}
pub async fn delete_session(&self, session_id: &str) -> Result<()> {
let response = self
.client
.delete(format!("{}/session/{}", self.base_url, session_id))
.send()
.await
.context("Failed to delete session")?;
if !response.status().is_success() {
let status = response.status();
let error_text = response.text().await.unwrap_or_default();
anyhow::bail!("Failed to delete session ({}): {}", status, error_text);
}
Ok(())
}
pub async fn list_sessions(&self) -> Result<Vec<Session>> {
let response = self
.client
.get(format!("{}/session", self.base_url))
.send()
.await
.context("Failed to list sessions")?;
if !response.status().is_success() {
let status = response.status();
let error_text = response.text().await.unwrap_or_default();
anyhow::bail!("Failed to list sessions ({}): {}", status, error_text);
}
response
.json()
.await
.context("Failed to parse sessions list")
}
pub fn event_stream_url(&self) -> String {
format!("{}/event", self.base_url)
}
pub fn base_url(&self) -> &str {
&self.base_url
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_client_creation() {
let client = OpenCodeClient::localhost(4096);
assert_eq!(client.base_url(), "http://127.0.0.1:4096");
}
#[test]
fn test_client_creation_with_trailing_slash() {
let client = OpenCodeClient::new("http://localhost:4096/");
assert_eq!(client.base_url(), "http://localhost:4096");
}
#[test]
fn test_event_stream_url() {
let client = OpenCodeClient::new("http://localhost:4096");
assert_eq!(client.event_stream_url(), "http://localhost:4096/event");
}
#[test]
fn test_localhost_constructor() {
let client = OpenCodeClient::localhost(8080);
assert_eq!(client.base_url(), "http://127.0.0.1:8080");
}
}