1use crate::error;
4use eventsource_stream::Event as MessageEvent;
5use futures::{Stream, StreamExt};
6use reqwest_eventsource::{Event, RequestBuilderExt};
7use std::sync::Arc;
8
9#[derive(Debug, Clone)]
34pub struct HttpClient {
35 pub http_client: reqwest::Client,
37 pub address: String,
39 pub authorization: Option<Arc<String>>,
41 pub user_agent: Option<String>,
43 pub x_title: Option<String>,
45 pub http_referer: Option<String>,
47 pub x_github_authorization: Option<Arc<String>>,
49 pub x_openrouter_authorization: Option<Arc<String>>,
51 pub x_mcp_authorization: Option<Arc<std::collections::HashMap<String, String>>>,
53 pub x_viewer_signature: Option<Arc<String>>,
55 pub x_viewer_address: Option<Arc<String>>,
57 pub x_commit_author_name: Option<Arc<String>>,
59 pub x_commit_author_email: Option<Arc<String>>,
61}
62
63impl HttpClient {
64 pub fn new(
82 http_client: reqwest::Client,
83 address: Option<impl Into<String>>,
84 authorization: Option<impl Into<String>>,
85 user_agent: Option<impl Into<String>>,
86 x_title: Option<impl Into<String>>,
87 http_referer: Option<impl Into<String>>,
88 x_github_authorization: Option<impl Into<String>>,
89 x_openrouter_authorization: Option<impl Into<String>>,
90 x_mcp_authorization: Option<std::collections::HashMap<String, String>>,
91 x_viewer_signature: Option<impl Into<String>>,
92 x_viewer_address: Option<impl Into<String>>,
93 x_commit_author_name: Option<impl Into<String>>,
94 x_commit_author_email: Option<impl Into<String>>,
95 ) -> Self {
96 #[cfg(feature = "env")]
97 let env = |name: &str| -> Option<String> { std::env::var(name).ok() };
98
99 Self {
100 http_client,
101 address: match address {
102 Some(base) => base.into(),
103 #[cfg(feature = "env")]
104 None => env("OBJECTIVEAI_ADDRESS")
105 .unwrap_or_else(|| "https://api.objectiveai.dev".to_string()),
106 #[cfg(not(feature = "env"))]
107 None => "https://api.objectiveai.dev".to_string(),
108 },
109 authorization: authorization.map(|k| Arc::new(k.into()))
110 .or_else(|| { #[cfg(feature = "env")] { env("OBJECTIVEAI_AUTHORIZATION").map(Arc::new) } #[cfg(not(feature = "env"))] { None } }),
111 user_agent: user_agent.map(Into::into)
112 .or_else(|| { #[cfg(feature = "env")] { env("USER_AGENT") } #[cfg(not(feature = "env"))] { None } }),
113 x_title: x_title.map(Into::into)
114 .or_else(|| { #[cfg(feature = "env")] { env("X_TITLE") } #[cfg(not(feature = "env"))] { None } }),
115 http_referer: http_referer.map(Into::into)
116 .or_else(|| { #[cfg(feature = "env")] { env("HTTP_REFERER") } #[cfg(not(feature = "env"))] { None } }),
117 x_github_authorization: x_github_authorization.map(|v| Arc::new(v.into()))
118 .or_else(|| { #[cfg(feature = "env")] { env("GITHUB_AUTHORIZATION").map(Arc::new) } #[cfg(not(feature = "env"))] { None } }),
119 x_openrouter_authorization: x_openrouter_authorization.map(|v| Arc::new(v.into()))
120 .or_else(|| { #[cfg(feature = "env")] { env("OPENROUTER_AUTHORIZATION").map(Arc::new) } #[cfg(not(feature = "env"))] { None } }),
121 x_mcp_authorization: x_mcp_authorization.map(Arc::new)
122 .or_else(|| { #[cfg(feature = "env")] { env("MCP_AUTHORIZATION").and_then(|v| serde_json::from_str(&v).ok()).map(Arc::new) } #[cfg(not(feature = "env"))] { None } }),
123 x_viewer_signature: x_viewer_signature.map(|v| Arc::new(v.into()))
124 .or_else(|| { #[cfg(feature = "env")] { env("VIEWER_SIGNATURE").map(Arc::new) } #[cfg(not(feature = "env"))] { None } }),
125 x_viewer_address: x_viewer_address.map(|v| Arc::new(v.into()))
126 .or_else(|| { #[cfg(feature = "env")] { env("VIEWER_ADDRESS").map(Arc::new) } #[cfg(not(feature = "env"))] { None } }),
127 x_commit_author_name: x_commit_author_name.map(|v| Arc::new(v.into()))
128 .or_else(|| { #[cfg(feature = "env")] { env("COMMIT_AUTHOR_NAME").map(Arc::new) } #[cfg(not(feature = "env"))] { None } }),
129 x_commit_author_email: x_commit_author_email.map(|v| Arc::new(v.into()))
130 .or_else(|| { #[cfg(feature = "env")] { env("COMMIT_AUTHOR_EMAIL").map(Arc::new) } #[cfg(not(feature = "env"))] { None } }),
131 }
132 }
133
134 fn request(
136 &self,
137 method: reqwest::Method,
138 path: &str,
139 body: Option<impl serde::Serialize>,
140 ) -> reqwest::RequestBuilder {
141 let url = format!(
142 "{}/{}",
143 self.address.trim_end_matches('/'),
144 path.trim_start_matches('/')
145 );
146 let mut request = self.http_client.request(method, &url);
147 if let Some(authorization) = &self.authorization {
148 let key = authorization.strip_prefix("Bearer ").unwrap_or(authorization);
149 request =
150 request.header("authorization", format!("Bearer {}", key));
151 }
152 if let Some(user_agent) = &self.user_agent {
153 request = request.header("user-agent", user_agent);
154 }
155 if let Some(x_title) = &self.x_title {
156 request = request.header("x-title", x_title);
157 }
158 if let Some(http_referer) = &self.http_referer {
159 request = request.header("referer", http_referer);
160 request = request.header("http-referer", http_referer);
161 }
162 if let Some(token) = &self.x_github_authorization {
163 request = request.header("X-GITHUB-AUTHORIZATION", token.as_str());
164 }
165 if let Some(token) = &self.x_openrouter_authorization {
166 request = request.header("X-OPENROUTER-AUTHORIZATION", token.as_str());
167 }
168 if let Some(headers) = &self.x_mcp_authorization {
169 if let Ok(json) = serde_json::to_string(headers.as_ref()) {
170 request = request.header("X-MCP-AUTHORIZATION", json);
171 }
172 }
173 if let Some(sig) = &self.x_viewer_signature {
174 request = request.header("X-VIEWER-SIGNATURE", sig.as_str());
175 }
176 if let Some(addr) = &self.x_viewer_address {
177 request = request.header("X-VIEWER-ADDRESS", addr.as_str());
178 }
179 if let Some(name) = &self.x_commit_author_name {
180 request = request.header("X-COMMIT-AUTHOR-NAME", name.as_str());
181 }
182 if let Some(email) = &self.x_commit_author_email {
183 request = request.header("X-COMMIT-AUTHOR-EMAIL", email.as_str());
184 }
185 if let Some(body) = body {
186 request = request.json(&body);
187 }
188 request
189 }
190
191 pub async fn send_unary<T: serde::de::DeserializeOwned + Send + 'static>(
202 &self,
203 method: reqwest::Method,
204 path: impl AsRef<str>,
205 body: Option<impl serde::Serialize>,
206 ) -> Result<T, super::HttpError> {
207 let response = self
208 .http_client
209 .execute(
210 self.request(method, path.as_ref(), body)
211 .build()
212 .map_err(super::HttpError::RequestError)?,
213 )
214 .await
215 .map_err(super::HttpError::HttpError)?;
216 let code = response.status();
217 if code.is_success() {
218 let text =
219 response.text().await.map_err(super::HttpError::HttpError)?;
220 let mut de = serde_json::Deserializer::from_str(&text);
221 match serde_path_to_error::deserialize::<_, T>(&mut de) {
222 Ok(value) => Ok(value),
223 Err(e) => Err(super::HttpError::DeserializationError(e)),
224 }
225 } else {
226 match response.text().await {
227 Ok(text) => Err(super::HttpError::BadStatus {
228 code,
229 body: match serde_json::from_str::<serde_json::Value>(&text)
230 {
231 Ok(body) => body,
232 Err(_) => serde_json::Value::String(text),
233 },
234 }),
235 Err(_) => Err(super::HttpError::BadStatus {
236 code,
237 body: serde_json::Value::Null,
238 }),
239 }
240 }
241 }
242
243 pub async fn send_unary_no_response(
251 &self,
252 method: reqwest::Method,
253 path: impl AsRef<str>,
254 body: Option<impl serde::Serialize>,
255 ) -> Result<(), super::HttpError> {
256 let response = self
257 .http_client
258 .execute(
259 self.request(method, path.as_ref(), body)
260 .build()
261 .map_err(super::HttpError::RequestError)?,
262 )
263 .await
264 .map_err(super::HttpError::HttpError)?;
265 let code = response.status();
266 if code.is_success() {
267 Ok(())
268 } else {
269 match response.text().await {
270 Ok(text) => Err(super::HttpError::BadStatus {
271 code,
272 body: match serde_json::from_str::<serde_json::Value>(&text)
273 {
274 Ok(body) => body,
275 Err(_) => serde_json::Value::String(text),
276 },
277 }),
278 Err(_) => Err(super::HttpError::BadStatus {
279 code,
280 body: serde_json::Value::Null,
281 }),
282 }
283 }
284 }
285
286 pub async fn send_streaming<
302 T: serde::de::DeserializeOwned + Send + 'static,
303 P: AsRef<str> + Send,
304 B: serde::Serialize + Send,
305 >(
306 &self,
307 method: reqwest::Method,
308 path: P,
309 body: Option<B>,
310 ) -> Result<
311 impl Stream<Item = Result<T, super::HttpError>>
312 + Send
313 + 'static
314 + use<T, P, B>,
315 super::HttpError,
316 > {
317 Ok(
321 self.request(method, path.as_ref(), body)
322 .eventsource()?
323 .take_while(|result| {
324 let dominated = matches!(
325 result,
326 Ok(Event::Message(MessageEvent { data, .. })) if data == "[DONE]"
327 );
328 async move { !dominated }
329 })
330 .then(|result| async {
331 match result {
332 Ok(Event::Open) => None,
333 Ok(Event::Message(MessageEvent { data, .. }))
334 if data.starts_with(":")
335 || data.is_empty() =>
336 {
337 None
338 }
339 Ok(Event::Message(MessageEvent { data, .. })) => {
340 let mut de =
341 serde_json::Deserializer::from_str(&data);
342 Some(
343 match serde_path_to_error::deserialize::<_, T>(
344 &mut de,
345 ) {
346 Ok(value) => Ok(value),
347 Err(e) => match serde_json::from_str::<error::ResponseError>(&data) {
348 Ok(err) => Err(super::HttpError::ApiError(err)),
349 Err(_) => Err(super::HttpError::DeserializationError(e)),
350 },
351 }
352 )
353 }
354 Err(reqwest_eventsource::Error::InvalidStatusCode(
355 code,
356 response,
357 )) => match response.text().await {
358 Ok(body) => {
359 Some(Err(super::HttpError::BadStatus {
360 code,
361 body: match serde_json::from_str::<
362 serde_json::Value,
363 >(
364 &body
365 ) {
366 Ok(body) => body,
367 Err(_) => {
368 serde_json::Value::String(body)
369 }
370 },
371 }))
372 }
373 Err(_) => Some(Err(super::HttpError::BadStatus {
374 code,
375 body: serde_json::Value::Null,
376 })),
377 },
378 Err(e) => Some(Err(super::HttpError::StreamError(e))),
379 }
380 })
381 .filter_map(|x| async { x }),
382 )
383 }
384}