1use anyhow::{Context, Result};
7use reqwest::Client;
8use std::time::Duration;
9
10use super::types::*;
11
12pub struct OpenCodeClient {
14 base_url: String,
15 client: Client,
16}
17
18impl OpenCodeClient {
19 pub fn new(base_url: &str) -> Self {
21 let client = Client::builder()
22 .timeout(Duration::from_secs(300)) .build()
24 .expect("Failed to create HTTP client");
25
26 Self {
27 base_url: base_url.trim_end_matches('/').to_string(),
28 client,
29 }
30 }
31
32 pub fn localhost(port: u16) -> Self {
34 Self::new(&format!("http://127.0.0.1:{}", port))
35 }
36
37 pub async fn health_check(&self) -> Result<bool> {
39 let response = self
40 .client
41 .get(format!("{}/health", self.base_url))
42 .timeout(Duration::from_secs(2))
43 .send()
44 .await;
45
46 match response {
47 Ok(r) => Ok(r.status().is_success()),
48 Err(_) => Ok(false),
49 }
50 }
51
52 pub async fn server_info(&self) -> Result<ServerInfo> {
54 let response = self
55 .client
56 .get(format!("{}/", self.base_url))
57 .send()
58 .await
59 .context("Failed to get server info")?;
60
61 if !response.status().is_success() {
62 let error: ErrorResponse = response.json().await.unwrap_or(ErrorResponse {
63 error: "Unknown error".to_string(),
64 details: None,
65 });
66 anyhow::bail!("Server error: {}", error.error);
67 }
68
69 response
70 .json()
71 .await
72 .context("Failed to parse server info")
73 }
74
75 pub async fn create_session(&self, title: &str) -> Result<Session> {
77 let request = CreateSessionRequest {
78 title: title.to_string(),
79 system_prompt: None,
80 };
81
82 let response = self
83 .client
84 .post(format!("{}/session", self.base_url))
85 .json(&request)
86 .send()
87 .await
88 .context("Failed to create session")?;
89
90 if !response.status().is_success() {
91 let status = response.status();
92 let error_text = response.text().await.unwrap_or_default();
93 anyhow::bail!("Failed to create session ({}): {}", status, error_text);
94 }
95
96 response
97 .json()
98 .await
99 .context("Failed to parse session response")
100 }
101
102 pub async fn send_message(
104 &self,
105 session_id: &str,
106 text: &str,
107 model: Option<(&str, &str)>, ) -> Result<()> {
109 let request = MessageRequest {
110 parts: vec![MessagePart::Text {
111 text: text.to_string(),
112 }],
113 model: model.map(|(provider, model_id)| ModelSpec {
114 provider_id: provider.to_string(),
115 model_id: model_id.to_string(),
116 }),
117 };
118
119 let response = self
120 .client
121 .post(format!("{}/session/{}/message", self.base_url, session_id))
122 .json(&request)
123 .send()
124 .await
125 .context("Failed to send message")?;
126
127 if !response.status().is_success() {
128 let status = response.status();
129 let error_text = response.text().await.unwrap_or_default();
130 anyhow::bail!("Failed to send message ({}): {}", status, error_text);
131 }
132
133 Ok(())
134 }
135
136 pub async fn get_session_status(&self, session_id: &str) -> Result<SessionStatus> {
138 let response = self
139 .client
140 .get(format!("{}/session/{}", self.base_url, session_id))
141 .send()
142 .await
143 .context("Failed to get session status")?;
144
145 if !response.status().is_success() {
146 let status = response.status();
147 let error_text = response.text().await.unwrap_or_default();
148 anyhow::bail!("Failed to get session ({}): {}", status, error_text);
149 }
150
151 response
152 .json()
153 .await
154 .context("Failed to parse session status")
155 }
156
157 pub async fn abort_session(&self, session_id: &str) -> Result<()> {
159 let response = self
160 .client
161 .post(format!("{}/session/{}/abort", self.base_url, session_id))
162 .send()
163 .await
164 .context("Failed to abort session")?;
165
166 if !response.status().is_success() {
167 let status = response.status();
168 let error_text = response.text().await.unwrap_or_default();
169 anyhow::bail!("Failed to abort session ({}): {}", status, error_text);
170 }
171
172 Ok(())
173 }
174
175 pub async fn delete_session(&self, session_id: &str) -> Result<()> {
177 let response = self
178 .client
179 .delete(format!("{}/session/{}", self.base_url, session_id))
180 .send()
181 .await
182 .context("Failed to delete session")?;
183
184 if !response.status().is_success() {
185 let status = response.status();
186 let error_text = response.text().await.unwrap_or_default();
187 anyhow::bail!("Failed to delete session ({}): {}", status, error_text);
188 }
189
190 Ok(())
191 }
192
193 pub async fn list_sessions(&self) -> Result<Vec<Session>> {
195 let response = self
196 .client
197 .get(format!("{}/session", self.base_url))
198 .send()
199 .await
200 .context("Failed to list sessions")?;
201
202 if !response.status().is_success() {
203 let status = response.status();
204 let error_text = response.text().await.unwrap_or_default();
205 anyhow::bail!("Failed to list sessions ({}): {}", status, error_text);
206 }
207
208 response
209 .json()
210 .await
211 .context("Failed to parse sessions list")
212 }
213
214 pub fn event_stream_url(&self) -> String {
216 format!("{}/event", self.base_url)
217 }
218
219 pub fn base_url(&self) -> &str {
221 &self.base_url
222 }
223}
224
225#[cfg(test)]
226mod tests {
227 use super::*;
228
229 #[test]
230 fn test_client_creation() {
231 let client = OpenCodeClient::localhost(4096);
232 assert_eq!(client.base_url(), "http://127.0.0.1:4096");
233 }
234
235 #[test]
236 fn test_client_creation_with_trailing_slash() {
237 let client = OpenCodeClient::new("http://localhost:4096/");
238 assert_eq!(client.base_url(), "http://localhost:4096");
239 }
240
241 #[test]
242 fn test_event_stream_url() {
243 let client = OpenCodeClient::new("http://localhost:4096");
244 assert_eq!(client.event_stream_url(), "http://localhost:4096/event");
245 }
246
247 #[test]
248 fn test_localhost_constructor() {
249 let client = OpenCodeClient::localhost(8080);
250 assert_eq!(client.base_url(), "http://127.0.0.1:8080");
251 }
252}