1use crate::error;
4use eventsource_stream::Event as MessageEvent;
5use futures::{SinkExt, Stream, StreamExt};
6use reqwest_eventsource::{Event, RequestBuilderExt};
7use std::sync::Arc;
8use tokio_tungstenite::tungstenite;
9
10#[derive(Debug, Clone)]
33pub struct HttpClient {
34 pub http_client: reqwest::Client,
36 pub address: String,
38 pub authorization: Option<Arc<String>>,
40 pub user_agent: Option<String>,
42 pub x_title: Option<String>,
44 pub http_referer: Option<String>,
46 pub x_github_authorization: Option<Arc<String>>,
48 pub x_openrouter_authorization: Option<Arc<String>>,
50 pub x_mcp_authorization:
52 Option<Arc<std::collections::HashMap<String, String>>>,
53 pub agent_instance_hierarchy: Option<Arc<String>>,
55 pub mcp_call_timeout_ms: Option<u64>,
61}
62
63impl HttpClient {
64 pub fn new(
81 http_client: reqwest::Client,
82 address: Option<impl Into<String>>,
83 authorization: Option<impl Into<String>>,
84 user_agent: Option<impl Into<String>>,
85 x_title: Option<impl Into<String>>,
86 http_referer: Option<impl Into<String>>,
87 x_github_authorization: Option<impl Into<String>>,
88 x_openrouter_authorization: Option<impl Into<String>>,
89 x_mcp_authorization: Option<std::collections::HashMap<String, String>>,
90 agent_instance_hierarchy: Option<impl Into<String>>,
91 mcp_call_timeout_ms: Option<u64>,
92 ) -> Self {
93 #[cfg(feature = "env")]
94 let env = |name: &str| -> Option<String> { std::env::var(name).ok() };
95
96 Self {
97 http_client,
98 address: match address {
99 Some(base) => base.into(),
100 #[cfg(feature = "env")]
101 None => env("OBJECTIVEAI_ADDRESS").unwrap_or_else(|| {
102 "https://api.objectiveai.dev".to_string()
103 }),
104 #[cfg(not(feature = "env"))]
105 None => "https://api.objectiveai.dev".to_string(),
106 },
107 authorization: authorization.map(|k| Arc::new(k.into())).or_else(
108 || {
109 #[cfg(feature = "env")]
110 {
111 env("OBJECTIVEAI_AUTHORIZATION").map(Arc::new)
112 }
113 #[cfg(not(feature = "env"))]
114 {
115 None
116 }
117 },
118 ),
119 user_agent: user_agent.map(Into::into).or_else(|| {
120 #[cfg(feature = "env")]
121 {
122 env("USER_AGENT")
123 }
124 #[cfg(not(feature = "env"))]
125 {
126 None
127 }
128 }),
129 x_title: x_title.map(Into::into).or_else(|| {
130 #[cfg(feature = "env")]
131 {
132 env("X_TITLE")
133 }
134 #[cfg(not(feature = "env"))]
135 {
136 None
137 }
138 }),
139 http_referer: http_referer.map(Into::into).or_else(|| {
140 #[cfg(feature = "env")]
141 {
142 env("HTTP_REFERER")
143 }
144 #[cfg(not(feature = "env"))]
145 {
146 None
147 }
148 }),
149 x_github_authorization: x_github_authorization
150 .map(|v| Arc::new(v.into()))
151 .or_else(|| {
152 #[cfg(feature = "env")]
153 {
154 env("GITHUB_AUTHORIZATION").map(Arc::new)
155 }
156 #[cfg(not(feature = "env"))]
157 {
158 None
159 }
160 }),
161 x_openrouter_authorization: x_openrouter_authorization
162 .map(|v| Arc::new(v.into()))
163 .or_else(|| {
164 #[cfg(feature = "env")]
165 {
166 env("OPENROUTER_AUTHORIZATION").map(Arc::new)
167 }
168 #[cfg(not(feature = "env"))]
169 {
170 None
171 }
172 }),
173 x_mcp_authorization: x_mcp_authorization.map(Arc::new).or_else(
174 || {
175 #[cfg(feature = "env")]
176 {
177 env("MCP_AUTHORIZATION")
178 .and_then(|v| serde_json::from_str(&v).ok())
179 .map(Arc::new)
180 }
181 #[cfg(not(feature = "env"))]
182 {
183 None
184 }
185 },
186 ),
187 agent_instance_hierarchy: agent_instance_hierarchy.map(|v| Arc::new(v.into())).or_else(|| {
188 #[cfg(feature = "env")]
189 {
190 env("OBJECTIVEAI_AGENT_INSTANCE_HIERARCHY").map(Arc::new)
191 }
192 #[cfg(not(feature = "env"))]
193 {
194 None
195 }
196 }),
197 mcp_call_timeout_ms,
200 }
201 }
202
203 fn request(
205 &self,
206 method: reqwest::Method,
207 path: &str,
208 body: Option<impl serde::Serialize>,
209 ) -> reqwest::RequestBuilder {
210 let url = format!(
211 "{}/{}",
212 self.address.trim_end_matches('/'),
213 path.trim_start_matches('/')
214 );
215 let mut request = self.http_client.request(method, &url);
216 if let Some(authorization) = &self.authorization {
217 let key = authorization
218 .strip_prefix("Bearer ")
219 .unwrap_or(authorization);
220 request =
221 request.header("authorization", format!("Bearer {}", key));
222 }
223 if let Some(user_agent) = &self.user_agent {
224 request = request.header("user-agent", user_agent);
225 }
226 if let Some(x_title) = &self.x_title {
227 request = request.header("x-title", x_title);
228 }
229 if let Some(http_referer) = &self.http_referer {
230 request = request.header("referer", http_referer);
231 request = request.header("http-referer", http_referer);
232 }
233 if let Some(token) = &self.x_github_authorization {
234 request = request.header("X-GITHUB-AUTHORIZATION", token.as_str());
235 }
236 if let Some(token) = &self.x_openrouter_authorization {
237 request =
238 request.header("X-OPENROUTER-AUTHORIZATION", token.as_str());
239 }
240 if let Some(headers) = &self.x_mcp_authorization {
241 if let Ok(json) = serde_json::to_string(headers.as_ref()) {
242 request = request.header("X-MCP-AUTHORIZATION", json);
243 }
244 }
245 if let Some(id) = &self.agent_instance_hierarchy {
246 request = request.header("X-OBJECTIVEAI-AGENT-INSTANCE-HIERARCHY", id.as_str());
247 }
248 if let Some(ms) = self.mcp_call_timeout_ms {
249 request = request.header("X-MCP-CALL-TIMEOUT", ms.to_string());
250 }
251 if let Some(body) = body {
252 request = request.json(&body);
253 }
254 request
255 }
256
257 pub async fn send_unary<T: serde::de::DeserializeOwned + Send + 'static>(
268 &self,
269 method: reqwest::Method,
270 path: impl AsRef<str>,
271 body: Option<impl serde::Serialize>,
272 ) -> Result<T, super::HttpError> {
273 let response = self
274 .http_client
275 .execute(
276 self.request(method, path.as_ref(), body)
277 .build()
278 .map_err(super::HttpError::RequestError)?,
279 )
280 .await
281 .map_err(super::HttpError::HttpError)?;
282 let code = response.status();
283 if code.is_success() {
284 let text =
285 response.text().await.map_err(super::HttpError::HttpError)?;
286 let mut de = serde_json::Deserializer::from_str(&text);
287 match serde_path_to_error::deserialize::<_, T>(&mut de) {
288 Ok(value) => Ok(value),
289 Err(e) => Err(super::HttpError::DeserializationError(e)),
290 }
291 } else {
292 match response.text().await {
293 Ok(text) => Err(super::HttpError::BadStatus {
294 code,
295 body: match serde_json::from_str::<serde_json::Value>(&text)
296 {
297 Ok(body) => body,
298 Err(_) => serde_json::Value::String(text),
299 },
300 }),
301 Err(_) => Err(super::HttpError::BadStatus {
302 code,
303 body: serde_json::Value::Null,
304 }),
305 }
306 }
307 }
308
309 pub async fn send_unary_no_response(
317 &self,
318 method: reqwest::Method,
319 path: impl AsRef<str>,
320 body: Option<impl serde::Serialize>,
321 ) -> Result<(), super::HttpError> {
322 let response = self
323 .http_client
324 .execute(
325 self.request(method, path.as_ref(), body)
326 .build()
327 .map_err(super::HttpError::RequestError)?,
328 )
329 .await
330 .map_err(super::HttpError::HttpError)?;
331 let code = response.status();
332 if code.is_success() {
333 Ok(())
334 } else {
335 match response.text().await {
336 Ok(text) => Err(super::HttpError::BadStatus {
337 code,
338 body: match serde_json::from_str::<serde_json::Value>(&text)
339 {
340 Ok(body) => body,
341 Err(_) => serde_json::Value::String(text),
342 },
343 }),
344 Err(_) => Err(super::HttpError::BadStatus {
345 code,
346 body: serde_json::Value::Null,
347 }),
348 }
349 }
350 }
351
352 pub async fn send_streaming<
368 T: serde::de::DeserializeOwned + Send + 'static,
369 P: AsRef<str> + Send,
370 B: serde::Serialize + Send,
371 >(
372 &self,
373 method: reqwest::Method,
374 path: P,
375 body: Option<B>,
376 ) -> Result<
377 impl Stream<Item = Result<T, super::HttpError>>
378 + Send
379 + 'static
380 + use<T, P, B>,
381 super::HttpError,
382 > {
383 Ok(
389 self.request(method, path.as_ref(), body)
390 .header("X-Transport", "sse")
391 .eventsource()?
392 .take_while(|result| {
393 let dominated = matches!(
394 result,
395 Ok(Event::Message(MessageEvent { data, .. })) if data == "[DONE]"
396 );
397 async move { !dominated }
398 })
399 .then(|result| async {
400 match result {
401 Ok(Event::Open) => None,
402 Ok(Event::Message(MessageEvent { data, .. }))
403 if data.starts_with(":")
404 || data.is_empty() =>
405 {
406 None
407 }
408 Ok(Event::Message(MessageEvent { data, .. })) => {
409 let mut de =
410 serde_json::Deserializer::from_str(&data);
411 Some(
412 match serde_path_to_error::deserialize::<_, T>(
413 &mut de,
414 ) {
415 Ok(value) => Ok(value),
416 Err(e) => match serde_json::from_str::<error::ResponseError>(&data) {
417 Ok(err) => Err(super::HttpError::ApiError(err)),
418 Err(_) => Err(super::HttpError::DeserializationError(e)),
419 },
420 }
421 )
422 }
423 Err(reqwest_eventsource::Error::InvalidStatusCode(
424 code,
425 response,
426 )) => match response.text().await {
427 Ok(body) => {
428 Some(Err(super::HttpError::BadStatus {
429 code,
430 body: match serde_json::from_str::<
431 serde_json::Value,
432 >(
433 &body
434 ) {
435 Ok(body) => body,
436 Err(_) => {
437 serde_json::Value::String(body)
438 }
439 },
440 }))
441 }
442 Err(_) => Some(Err(super::HttpError::BadStatus {
443 code,
444 body: serde_json::Value::Null,
445 })),
446 },
447 Err(e) => Some(Err(super::HttpError::StreamError(e))),
448 }
449 })
450 .filter_map(|x| async { x }),
451 )
452 }
453
454 #[cfg(feature = "mcp")]
471 pub async fn send_streaming_ws<Chunk, B, H, P>(
472 &self,
473 method: reqwest::Method,
474 path: P,
475 body: B,
476 handler: H,
477 ) -> Result<
478 (
479 impl Stream<Item = Result<Chunk, super::HttpError>>
480 + Send
481 + Unpin
482 + 'static
483 + use<Chunk, B, H, P>,
484 super::Notifier,
485 ),
486 super::HttpError,
487 >
488 where
489 Chunk: serde::de::DeserializeOwned + Send + 'static,
490 B: serde::Serialize + Send + 'static,
491 H: super::McpHandler,
492 P: AsRef<str>,
493 {
494 use crate::client_objectiveai_mcp::{
495 client_response::Response as ClientResponse,
496 server_request::Request as ServerRequest,
497 };
498 use futures::stream::SplitStream;
499 use tokio::net::TcpStream;
500 use tokio_tungstenite::{MaybeTlsStream, WebSocketStream};
501
502 let url = format!(
505 "{}/{}",
506 self.address.trim_end_matches('/'),
507 path.as_ref().trim_start_matches('/')
508 );
509 let ws_url = if let Some(rest) = url.strip_prefix("https://") {
510 format!("wss://{rest}")
511 } else if let Some(rest) = url.strip_prefix("http://") {
512 format!("ws://{rest}")
513 } else {
514 url.clone()
515 };
516 let _ = method; let mut req = tungstenite::handshake::client::Request::builder()
521 .method("GET")
522 .uri(&ws_url)
523 .header(
524 "Host",
525 reqwest::Url::parse(&url)
526 .ok()
527 .and_then(|u| u.host_str().map(str::to_owned))
528 .unwrap_or_default(),
529 )
530 .header("Upgrade", "websocket")
531 .header("Connection", "Upgrade")
532 .header(
533 "Sec-WebSocket-Key",
534 tungstenite::handshake::client::generate_key(),
535 )
536 .header("Sec-WebSocket-Version", "13")
537 .header("X-Transport", "ws");
538 if let Some(authorization) = &self.authorization {
539 let key = authorization
540 .strip_prefix("Bearer ")
541 .unwrap_or(authorization.as_str());
542 req = req.header("authorization", format!("Bearer {}", key));
543 }
544 if let Some(ua) = &self.user_agent {
545 req = req.header("user-agent", ua);
546 }
547 if let Some(x_title) = &self.x_title {
548 req = req.header("x-title", x_title);
549 }
550 if let Some(http_referer) = &self.http_referer {
551 req = req.header("referer", http_referer);
552 req = req.header("http-referer", http_referer);
553 }
554 if let Some(token) = &self.x_github_authorization {
555 req = req.header("X-GITHUB-AUTHORIZATION", token.as_str());
556 }
557 if let Some(token) = &self.x_openrouter_authorization {
558 req = req.header("X-OPENROUTER-AUTHORIZATION", token.as_str());
559 }
560 if let Some(headers) = &self.x_mcp_authorization {
561 if let Ok(json) = serde_json::to_string(headers.as_ref()) {
562 req = req.header("X-MCP-AUTHORIZATION", json);
563 }
564 }
565 if let Some(id) = &self.agent_instance_hierarchy {
566 req = req.header("X-OBJECTIVEAI-AGENT-INSTANCE-HIERARCHY", id.as_str());
567 }
568 if let Some(ms) = self.mcp_call_timeout_ms {
569 req = req.header("X-MCP-CALL-TIMEOUT", ms.to_string());
570 }
571 let req = req.body(()).map_err(|e| {
572 super::HttpError::WsConnect(tungstenite::Error::Http(
573 tungstenite::http::Response::builder()
574 .status(400)
575 .body(Some(e.to_string().into_bytes()))
576 .unwrap(),
577 ))
578 })?;
579
580 let (ws_stream, _resp) = tokio_tungstenite::connect_async(req).await?;
581 let (mut sink, rx_stream): (
582 _,
583 SplitStream<WebSocketStream<MaybeTlsStream<TcpStream>>>,
584 ) = ws_stream.split();
585
586 let body_frame = serde_json::to_string(&body)
588 .map_err(super::HttpError::NotifySerialize)?;
589 sink.send(tungstenite::Message::Text(body_frame.into()))
590 .await
591 .map_err(super::HttpError::NotifySend)?;
592
593 let sink: super::notifier::SharedSink =
595 Arc::new(tokio::sync::Mutex::new(sink));
596 let pending: super::notifier::PendingNotifies =
597 Arc::new(dashmap::DashMap::new());
598
599 let (chunk_tx, chunk_rx) = futures::channel::mpsc::unbounded::<
603 Result<Chunk, super::HttpError>,
604 >();
605
606 let demux_sink = sink.clone();
607 let demux_pending = pending.clone();
608 let handler = Arc::new(handler);
609 tokio::spawn(async move {
610 let mut rx_stream = rx_stream;
611 let mut chunk_tx = chunk_tx;
612 loop {
613 let msg = match rx_stream.next().await {
614 Some(m) => m,
615 None => break,
616 };
617 let text = match msg {
618 Ok(tungstenite::Message::Text(t)) => {
619 let s = t.to_string();
620 s
621 }
622 Ok(tungstenite::Message::Binary(_)) => {
623 continue;
624 }
625 Ok(
626 tungstenite::Message::Ping(_)
627 | tungstenite::Message::Pong(_),
628 ) => continue,
629 Ok(tungstenite::Message::Close(_)) => {
630 break;
631 }
632 Ok(tungstenite::Message::Frame(_)) => continue,
633 Err(_) => {
634 break;
635 }
636 };
637
638 if let Ok(response) =
643 serde_json::from_str::<ClientResponse>(&text)
644 {
645 let id = response.id().to_string();
646 if let Some((_, tx)) = demux_pending.remove(&id) {
647 let _ = tx.send(response);
648 }
649 continue;
650 }
651 if let Ok(request) =
652 serde_json::from_str::<ServerRequest>(&text)
653 {
654 let id = request.id.clone();
655 let handler = handler.clone();
656 let demux_sink = demux_sink.clone();
657 tokio::spawn(async move {
658 let id = id;
659 let response = handler.handle(request).await;
662 let frame = match serde_json::to_string(&response) {
663 Ok(s) => s,
664 Err(_) => {
665 return;
666 }
667 };
668 let mut guard = demux_sink.lock().await;
669 let send_result = guard
670 .send(tungstenite::Message::Text(frame.into()))
671 .await;
672 });
673 continue;
674 }
675
676 let mut de = serde_json::Deserializer::from_str(&text);
678 match serde_path_to_error::deserialize::<_, Chunk>(&mut de) {
679 Ok(chunk) => {
680 if chunk_tx.unbounded_send(Ok(chunk)).is_err() {
681 break;
682 }
683 }
684 Err(e) => {
685 let err = match serde_json::from_str::<
688 error::ResponseError,
689 >(&text)
690 {
691 Ok(api_err) => super::HttpError::ApiError(api_err),
692 Err(_) => super::HttpError::DeserializationError(e),
693 };
694 let _ = chunk_tx.unbounded_send(Err(err));
695 break;
696 }
697 }
698 }
699 drop(demux_pending);
703 drop(chunk_tx);
704 });
705
706 let notifier = super::Notifier::new(sink, pending);
707 Ok((chunk_rx, notifier))
708 }
709}