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.json().await.context("Failed to parse server info")
70 }
71
72 pub async fn create_session(&self, title: &str) -> Result<Session> {
74 let request = CreateSessionRequest {
75 title: title.to_string(),
76 system_prompt: None,
77 };
78
79 let response = self
80 .client
81 .post(format!("{}/session", self.base_url))
82 .json(&request)
83 .send()
84 .await
85 .context("Failed to create session")?;
86
87 if !response.status().is_success() {
88 let status = response.status();
89 let error_text = response.text().await.unwrap_or_default();
90 anyhow::bail!("Failed to create session ({}): {}", status, error_text);
91 }
92
93 response
94 .json()
95 .await
96 .context("Failed to parse session response")
97 }
98
99 pub async fn send_message(
101 &self,
102 session_id: &str,
103 text: &str,
104 model: Option<(&str, &str)>, ) -> Result<()> {
106 let request = MessageRequest {
107 parts: vec![MessagePart::Text {
108 text: text.to_string(),
109 }],
110 model: model.map(|(provider, model_id)| ModelSpec {
111 provider_id: provider.to_string(),
112 model_id: model_id.to_string(),
113 }),
114 };
115
116 let response = self
117 .client
118 .post(format!("{}/session/{}/message", self.base_url, session_id))
119 .json(&request)
120 .send()
121 .await
122 .context("Failed to send message")?;
123
124 if !response.status().is_success() {
125 let status = response.status();
126 let error_text = response.text().await.unwrap_or_default();
127 anyhow::bail!("Failed to send message ({}): {}", status, error_text);
128 }
129
130 Ok(())
131 }
132
133 pub async fn get_session_status(&self, session_id: &str) -> Result<SessionStatus> {
135 let response = self
136 .client
137 .get(format!("{}/session/{}", self.base_url, session_id))
138 .send()
139 .await
140 .context("Failed to get session status")?;
141
142 if !response.status().is_success() {
143 let status = response.status();
144 let error_text = response.text().await.unwrap_or_default();
145 anyhow::bail!("Failed to get session ({}): {}", status, error_text);
146 }
147
148 response
149 .json()
150 .await
151 .context("Failed to parse session status")
152 }
153
154 pub async fn abort_session(&self, session_id: &str) -> Result<()> {
156 let response = self
157 .client
158 .post(format!("{}/session/{}/abort", self.base_url, session_id))
159 .send()
160 .await
161 .context("Failed to abort session")?;
162
163 if !response.status().is_success() {
164 let status = response.status();
165 let error_text = response.text().await.unwrap_or_default();
166 anyhow::bail!("Failed to abort session ({}): {}", status, error_text);
167 }
168
169 Ok(())
170 }
171
172 pub async fn delete_session(&self, session_id: &str) -> Result<()> {
174 let response = self
175 .client
176 .delete(format!("{}/session/{}", self.base_url, session_id))
177 .send()
178 .await
179 .context("Failed to delete session")?;
180
181 if !response.status().is_success() {
182 let status = response.status();
183 let error_text = response.text().await.unwrap_or_default();
184 anyhow::bail!("Failed to delete session ({}): {}", status, error_text);
185 }
186
187 Ok(())
188 }
189
190 pub async fn list_sessions(&self) -> Result<Vec<Session>> {
192 let response = self
193 .client
194 .get(format!("{}/session", self.base_url))
195 .send()
196 .await
197 .context("Failed to list sessions")?;
198
199 if !response.status().is_success() {
200 let status = response.status();
201 let error_text = response.text().await.unwrap_or_default();
202 anyhow::bail!("Failed to list sessions ({}): {}", status, error_text);
203 }
204
205 response
206 .json()
207 .await
208 .context("Failed to parse sessions list")
209 }
210
211 pub fn event_stream_url(&self) -> String {
213 format!("{}/event", self.base_url)
214 }
215
216 pub fn base_url(&self) -> &str {
218 &self.base_url
219 }
220}
221
222#[cfg(test)]
223mod tests {
224 use super::*;
225
226 #[test]
227 fn test_client_creation() {
228 let client = OpenCodeClient::localhost(4096);
229 assert_eq!(client.base_url(), "http://127.0.0.1:4096");
230 }
231
232 #[test]
233 fn test_client_creation_with_trailing_slash() {
234 let client = OpenCodeClient::new("http://localhost:4096/");
235 assert_eq!(client.base_url(), "http://localhost:4096");
236 }
237
238 #[test]
239 fn test_event_stream_url() {
240 let client = OpenCodeClient::new("http://localhost:4096");
241 assert_eq!(client.event_stream_url(), "http://localhost:4096/event");
242 }
243
244 #[test]
245 fn test_localhost_constructor() {
246 let client = OpenCodeClient::localhost(8080);
247 assert_eq!(client.base_url(), "http://127.0.0.1:8080");
248 }
249}