1use std::sync::Arc;
2#[cfg(not(target_family = "wasm"))]
3use std::sync::Mutex;
4
5use bytes::Bytes;
6use futures::stream::StreamExt;
7use reqwest::{header::HeaderMap, multipart::Form, Response};
8use serde::{de::DeserializeOwned, Serialize};
9
10use crate::error::StreamError;
11#[cfg(feature = "middleware")]
12use crate::executor::TowerExecutor;
13use crate::{
14 config::{Config, OpenAIConfig},
15 error::{map_deserialization_error, ApiError, ApiErrorResponse, OpenAIError, WrappedError},
16 executor::{HttpRequestFactory, ReqwestExecutor, SharedExecutor},
17 traits::AsyncTryFrom,
18 RequestOptions,
19};
20
21struct RequestParts {
22 request_client: reqwest::Client,
23 method: reqwest::Method,
24 url: String,
25 headers: HeaderMap,
26 query: Vec<(String, String)>,
27}
28
29impl RequestParts {
30 fn build_request_builder(&self) -> reqwest::RequestBuilder {
31 self.request_client
32 .request(self.method.clone(), self.url.clone())
33 .query(&self.query)
34 .headers(self.headers.clone())
35 }
36}
37
38#[cfg(feature = "administration")]
39use crate::admin::Admin;
40#[cfg(feature = "chatkit")]
41use crate::chatkit::Chatkit;
42#[cfg(feature = "file")]
43use crate::file::Files;
44#[cfg(feature = "image")]
45use crate::image::Images;
46#[cfg(feature = "moderation")]
47use crate::moderation::Moderations;
48#[cfg(feature = "audio")]
49use crate::Audio;
50#[cfg(feature = "batch")]
51use crate::Batches;
52#[cfg(feature = "chat-completion")]
53use crate::Chat;
54#[cfg(feature = "completions")]
55use crate::Completions;
56#[cfg(feature = "container")]
57use crate::Containers;
58#[cfg(feature = "content-provenance-checks")]
59use crate::ContentProvenanceChecks;
60#[cfg(feature = "responses")]
61use crate::Conversations;
62#[cfg(feature = "embedding")]
63use crate::Embeddings;
64#[cfg(feature = "evals")]
65use crate::Evals;
66#[cfg(feature = "finetuning")]
67use crate::FineTuning;
68#[cfg(feature = "model")]
69use crate::Models;
70#[cfg(feature = "realtime")]
71use crate::Realtime;
72#[cfg(feature = "responses")]
73use crate::Responses;
74#[cfg(feature = "safety")]
75use crate::Safety;
76#[cfg(feature = "skill")]
77use crate::Skills;
78#[cfg(feature = "upload")]
79use crate::Uploads;
80#[cfg(feature = "vectorstore")]
81use crate::VectorStores;
82#[cfg(feature = "video")]
83#[allow(deprecated)]
84use crate::Videos;
85
86#[derive(Clone)]
87pub struct Client<C: Config> {
90 request_client: reqwest::Client,
91 executor: SharedExecutor,
92 config: C,
93}
94
95impl<C> std::fmt::Debug for Client<C>
96where
97 C: Config + std::fmt::Debug,
98{
99 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
100 f.debug_struct("Client")
101 .field("request_client", &self.request_client)
102 .field("config", &self.config)
103 .finish()
104 }
105}
106
107impl<C: Config> Default for Client<C>
108where
109 C: Default,
110{
111 fn default() -> Self {
112 let request_client = reqwest::Client::new();
113 Self {
114 executor: Arc::new(ReqwestExecutor::new(request_client.clone())),
115 request_client,
116 config: C::default(),
117 }
118 }
119}
120
121impl Client<OpenAIConfig> {
122 pub fn new() -> Self {
124 Self::default()
125 }
126}
127
128impl<C: Config> Client<C> {
129 pub fn build(http_client: reqwest::Client, config: C) -> Self {
131 Self {
132 executor: Arc::new(ReqwestExecutor::new(http_client.clone())),
133 request_client: http_client,
134 config,
135 }
136 }
137
138 pub fn with_config(config: C) -> Self {
140 let request_client = reqwest::Client::new();
141 Self {
142 executor: Arc::new(ReqwestExecutor::new(request_client.clone())),
143 request_client,
144 config,
145 }
146 }
147
148 pub fn with_http_client(mut self, http_client: reqwest::Client) -> Self {
152 self.executor = Arc::new(ReqwestExecutor::new(http_client.clone()));
153 self.request_client = http_client;
154 self
155 }
156
157 #[cfg(all(feature = "middleware", not(target_family = "wasm")))]
159 pub fn with_http_service<S>(mut self, service: S) -> Self
160 where
161 S: tower::Service<HttpRequestFactory, Response = Response> + Clone + Send + Sync + 'static,
162 S::Future: Send + 'static,
163 S::Error: Into<OpenAIError> + Send + Sync + 'static,
164 {
165 self.executor = Arc::new(TowerExecutor::new(service));
170 self
171 }
172
173 #[cfg(all(feature = "middleware", target_family = "wasm"))]
175 pub fn with_http_service<S>(mut self, service: S) -> Self
176 where
177 S: tower::Service<HttpRequestFactory, Response = Response> + Clone + 'static,
178 S::Future: 'static,
179 S::Error: Into<OpenAIError> + 'static,
180 {
181 self.executor = Arc::new(TowerExecutor::new(service));
186 self
187 }
188
189 #[cfg(feature = "model")]
193 pub fn models(&self) -> Models<'_, C> {
194 Models::new(self)
195 }
196
197 #[cfg(feature = "completions")]
199 pub fn completions(&self) -> Completions<'_, C> {
200 Completions::new(self)
201 }
202
203 #[cfg(feature = "chat-completion")]
205 pub fn chat(&self) -> Chat<'_, C> {
206 Chat::new(self)
207 }
208
209 #[cfg(feature = "image")]
211 pub fn images(&self) -> Images<'_, C> {
212 Images::new(self)
213 }
214
215 #[cfg(feature = "moderation")]
217 pub fn moderations(&self) -> Moderations<'_, C> {
218 Moderations::new(self)
219 }
220
221 #[cfg(feature = "file")]
223 pub fn files(&self) -> Files<'_, C> {
224 Files::new(self)
225 }
226
227 #[cfg(feature = "upload")]
229 pub fn uploads(&self) -> Uploads<'_, C> {
230 Uploads::new(self)
231 }
232
233 #[cfg(feature = "finetuning")]
235 pub fn fine_tuning(&self) -> FineTuning<'_, C> {
236 FineTuning::new(self)
237 }
238
239 #[cfg(feature = "embedding")]
241 pub fn embeddings(&self) -> Embeddings<'_, C> {
242 Embeddings::new(self)
243 }
244
245 #[cfg(feature = "audio")]
247 pub fn audio(&self) -> Audio<'_, C> {
248 Audio::new(self)
249 }
250
251 #[cfg(feature = "video")]
253 #[deprecated(note = "The Videos API is deprecated.")]
254 #[allow(deprecated)]
255 pub fn videos(&self) -> Videos<'_, C> {
256 Videos::new(self)
257 }
258
259 #[cfg(feature = "vectorstore")]
261 pub fn vector_stores(&self) -> VectorStores<'_, C> {
262 VectorStores::new(self)
263 }
264
265 #[cfg(feature = "batch")]
267 pub fn batches(&self) -> Batches<'_, C> {
268 Batches::new(self)
269 }
270
271 #[cfg(feature = "administration")]
274 pub fn admin(&self) -> Admin<'_, C> {
275 Admin::new(self)
276 }
277
278 #[cfg(feature = "responses")]
280 pub fn responses(&self) -> Responses<'_, C> {
281 Responses::new(self)
282 }
283
284 #[cfg(feature = "responses")]
286 pub fn conversations(&self) -> Conversations<'_, C> {
287 Conversations::new(self)
288 }
289
290 #[cfg(feature = "container")]
292 pub fn containers(&self) -> Containers<'_, C> {
293 Containers::new(self)
294 }
295
296 #[cfg(feature = "skill")]
298 pub fn skills(&self) -> Skills<'_, C> {
299 Skills::new(self)
300 }
301
302 #[cfg(feature = "evals")]
304 pub fn evals(&self) -> Evals<'_, C> {
305 Evals::new(self)
306 }
307
308 #[cfg(feature = "chatkit")]
309 pub fn chatkit(&self) -> Chatkit<'_, C> {
310 Chatkit::new(self)
311 }
312
313 #[cfg(feature = "realtime")]
315 pub fn realtime(&self) -> Realtime<'_, C> {
316 Realtime::new(self)
317 }
318
319 #[cfg(feature = "safety")]
321 pub fn safety(&self) -> crate::Safety<'_, C> {
322 Safety::new(self)
323 }
324
325 #[cfg(feature = "content-provenance-checks")]
327 pub fn content_provenance_checks(&self) -> crate::ContentProvenanceChecks<'_, C> {
328 ContentProvenanceChecks::new(self)
329 }
330
331 pub fn config(&self) -> &C {
332 &self.config
333 }
334
335 fn build_request_parts(
336 &self,
337 method: reqwest::Method,
338 path: &str,
339 request_options: &RequestOptions,
340 ) -> Arc<RequestParts> {
341 let url = if let Some(path) = request_options.path() {
342 self.config.url(path.as_str())
343 } else {
344 self.config.url(path)
345 };
346 let mut headers = self.config.headers();
347 if let Some(request_headers) = request_options.headers() {
348 headers.extend(request_headers.clone());
349 }
350
351 let mut query = self
352 .config
353 .query()
354 .into_iter()
355 .map(|(key, value)| (key.to_string(), value.to_string()))
356 .collect::<Vec<_>>();
357 query.extend_from_slice(request_options.query());
358
359 Arc::new(RequestParts {
360 request_client: self.request_client.clone(),
361 method,
362 url,
363 headers,
364 query,
365 })
366 }
367
368 fn build_request_factory(
369 &self,
370 method: reqwest::Method,
371 path: &str,
372 request_options: &RequestOptions,
373 ) -> HttpRequestFactory {
374 let request_parts = self.build_request_parts(method, path, request_options);
375
376 HttpRequestFactory::new(move || {
377 let request_parts = request_parts.clone();
378
379 async move {
380 let request = request_parts.build_request_builder().build()?;
381 Ok(request)
382 }
383 })
384 }
385
386 fn build_request_factory_with_json<I>(
387 &self,
388 method: reqwest::Method,
389 path: &str,
390 request: I,
391 request_options: &RequestOptions,
392 ) -> Result<HttpRequestFactory, OpenAIError>
393 where
394 I: Serialize,
395 {
396 let request = Bytes::from(serde_json::to_vec(&request).map_err(|error| {
399 OpenAIError::InvalidArgument(format!("failed to serialize request: {error}"))
400 })?);
401 let request_parts = self.build_request_parts(method, path, request_options);
402
403 Ok(HttpRequestFactory::new(move || {
404 let request_parts = request_parts.clone();
405 let request = request.clone();
406
407 async move {
408 let request_builder = request_parts
409 .build_request_builder()
410 .header(reqwest::header::CONTENT_TYPE, "application/json")
411 .body(request.clone());
412
413 Ok(request_builder.build()?)
414 }
415 }))
416 }
417
418 fn build_request_factory_with_form<F>(
419 &self,
420 method: reqwest::Method,
421 path: &str,
422 form: F,
423 request_options: &RequestOptions,
424 ) -> Result<HttpRequestFactory, OpenAIError>
425 where
426 F: Clone + crate::traits::MaybeSend + 'static,
427 Form: AsyncTryFrom<F, Error = OpenAIError>,
428 {
429 #[cfg(not(target_family = "wasm"))]
433 let form = Arc::new(Mutex::new(form));
434 let request_parts = self.build_request_parts(method, path, request_options);
435
436 Ok(HttpRequestFactory::new(move || {
437 let request_parts = request_parts.clone();
438 let form = form.clone();
439
440 async move {
441 #[cfg(not(target_family = "wasm"))]
442 let form = form
443 .lock()
444 .expect("multipart request factory mutex poisoned")
445 .clone();
446 #[cfg(target_family = "wasm")]
447 let form = form.clone();
448 let form = <Form as AsyncTryFrom<F>>::try_from(form).await?;
449 let request_builder = request_parts.build_request_builder().multipart(form);
450
451 Ok(request_builder.build()?)
452 }
453 }))
454 }
455
456 #[allow(unused)]
458 pub(crate) async fn get<O>(
459 &self,
460 path: &str,
461 request_options: &RequestOptions,
462 ) -> Result<O, OpenAIError>
463 where
464 O: DeserializeOwned,
465 {
466 let request_factory =
467 self.build_request_factory(reqwest::Method::GET, path, request_options);
468 self.execute(request_factory).await
469 }
470
471 #[allow(unused)]
473 pub(crate) async fn delete<O>(
474 &self,
475 path: &str,
476 request_options: &RequestOptions,
477 ) -> Result<O, OpenAIError>
478 where
479 O: DeserializeOwned,
480 {
481 let request_factory =
482 self.build_request_factory(reqwest::Method::DELETE, path, request_options);
483 self.execute(request_factory).await
484 }
485
486 #[allow(unused)]
488 pub(crate) async fn get_raw(
489 &self,
490 path: &str,
491 request_options: &RequestOptions,
492 ) -> Result<(Bytes, HeaderMap), OpenAIError> {
493 let request_factory =
494 self.build_request_factory(reqwest::Method::GET, path, request_options);
495 self.execute_raw(request_factory).await
496 }
497
498 #[allow(unused)]
500 pub(crate) async fn post_raw<I>(
501 &self,
502 path: &str,
503 request: I,
504 request_options: &RequestOptions,
505 ) -> Result<(Bytes, HeaderMap), OpenAIError>
506 where
507 I: Serialize,
508 {
509 let request_factory = self.build_request_factory_with_json(
510 reqwest::Method::POST,
511 path,
512 request,
513 request_options,
514 )?;
515 self.execute_raw(request_factory).await
516 }
517
518 #[allow(unused)]
520 pub(crate) async fn post<I, O>(
521 &self,
522 path: &str,
523 request: I,
524 request_options: &RequestOptions,
525 ) -> Result<O, OpenAIError>
526 where
527 I: Serialize,
528 O: DeserializeOwned,
529 {
530 let request_factory = self.build_request_factory_with_json(
531 reqwest::Method::POST,
532 path,
533 request,
534 request_options,
535 )?;
536 self.execute(request_factory).await
537 }
538
539 #[allow(unused)]
541 pub(crate) async fn post_form_raw<F>(
542 &self,
543 path: &str,
544 form: F,
545 request_options: &RequestOptions,
546 ) -> Result<(Bytes, HeaderMap), OpenAIError>
547 where
548 F: Clone + crate::traits::MaybeSend + 'static,
549 Form: AsyncTryFrom<F, Error = OpenAIError>,
550 {
551 let request_factory = self.build_request_factory_with_form(
552 reqwest::Method::POST,
553 path,
554 form,
555 request_options,
556 )?;
557 self.execute_raw(request_factory).await
558 }
559
560 #[allow(unused)]
562 pub(crate) async fn post_form<O, F>(
563 &self,
564 path: &str,
565 form: F,
566 request_options: &RequestOptions,
567 ) -> Result<O, OpenAIError>
568 where
569 O: DeserializeOwned,
570 F: Clone + crate::traits::MaybeSend + 'static,
571 Form: AsyncTryFrom<F, Error = OpenAIError>,
572 {
573 let request_factory = self.build_request_factory_with_form(
574 reqwest::Method::POST,
575 path,
576 form,
577 request_options,
578 )?;
579 self.execute(request_factory).await
580 }
581
582 #[allow(unused)]
583 pub(crate) async fn post_form_stream<O, F>(
584 &self,
585 path: &str,
586 form: F,
587 request_options: &RequestOptions,
588 ) -> Result<crate::types::stream::StreamResponse<O>, OpenAIError>
589 where
590 F: Clone + crate::traits::MaybeSend + 'static,
591 Form: AsyncTryFrom<F, Error = OpenAIError>,
592 O: DeserializeOwned + crate::traits::MaybeSend + 'static,
593 {
594 let request_factory = self.build_request_factory_with_form(
595 reqwest::Method::POST,
596 path,
597 form,
598 request_options,
599 )?;
600
601 self.execute_stream(request_factory).await
602 }
603
604 async fn execute_raw(
605 &self,
606 request_factory: HttpRequestFactory,
607 ) -> Result<(Bytes, HeaderMap), OpenAIError> {
608 let response = self.execute_response(request_factory).await?;
609 read_response(response).await
610 }
611
612 async fn execute<O>(&self, request_factory: HttpRequestFactory) -> Result<O, OpenAIError>
613 where
614 O: DeserializeOwned,
615 {
616 let (bytes, _headers) = self.execute_raw(request_factory).await?;
617
618 let response: O = serde_json::from_slice(bytes.as_ref())
619 .map_err(|e| map_deserialization_error(e, bytes.as_ref()))?;
620
621 Ok(response)
622 }
623
624 async fn execute_response(
625 &self,
626 request_factory: HttpRequestFactory,
627 ) -> Result<Response, OpenAIError> {
628 let response = self.executor.execute(request_factory).await?;
629 if !response.status().is_success() {
630 return Err(read_error_response(response).await);
631 }
632 Ok(response)
633 }
634
635 async fn execute_stream<O>(
636 &self,
637 request_factory: HttpRequestFactory,
638 ) -> Result<crate::types::stream::StreamResponse<O>, OpenAIError>
639 where
640 O: DeserializeOwned + crate::traits::MaybeSend + 'static,
641 {
642 let response = self.execute_response(request_factory).await?;
643 Ok(stream(response).await)
644 }
645
646 async fn execute_stream_mapped_raw_events<O>(
647 &self,
648 request_factory: HttpRequestFactory,
649 event_mapper: impl Fn(eventsource_stream::Event) -> Result<O, OpenAIError>
650 + crate::traits::MaybeSend
651 + 'static,
652 ) -> Result<crate::types::stream::StreamResponse<O>, OpenAIError>
653 where
654 O: DeserializeOwned + crate::traits::MaybeSend + 'static,
655 {
656 let response = self.execute_response(request_factory).await?;
657 Ok(stream_mapped_raw_events(response, event_mapper).await)
658 }
659
660 #[allow(unused)]
662 pub(crate) async fn post_stream<I, O>(
663 &self,
664 path: &str,
665 request: I,
666 request_options: &RequestOptions,
667 ) -> Result<crate::types::stream::StreamResponse<O>, OpenAIError>
668 where
669 I: Serialize,
670 O: DeserializeOwned + crate::traits::MaybeSend + 'static,
671 {
672 let request_factory = self.build_request_factory_with_json(
673 reqwest::Method::POST,
674 path,
675 request,
676 request_options,
677 )?;
678 self.execute_stream(request_factory).await
681 }
682
683 #[allow(unused)]
684 pub(crate) async fn post_stream_mapped_raw_events<I, O>(
685 &self,
686 path: &str,
687 request: I,
688 request_options: &RequestOptions,
689 event_mapper: impl Fn(eventsource_stream::Event) -> Result<O, OpenAIError>
690 + crate::traits::MaybeSend
691 + 'static,
692 ) -> Result<crate::types::stream::StreamResponse<O>, OpenAIError>
693 where
694 I: Serialize,
695 O: DeserializeOwned + crate::traits::MaybeSend + 'static,
696 {
697 let request_factory = self.build_request_factory_with_json(
698 reqwest::Method::POST,
699 path,
700 request,
701 request_options,
702 )?;
703 self.execute_stream_mapped_raw_events(request_factory, event_mapper)
704 .await
705 }
706
707 #[allow(unused)]
709 pub(crate) async fn get_stream<O>(
710 &self,
711 path: &str,
712 request_options: &RequestOptions,
713 ) -> Result<crate::types::stream::StreamResponse<O>, OpenAIError>
714 where
715 O: DeserializeOwned + crate::traits::MaybeSend + 'static,
716 {
717 let request_factory =
718 self.build_request_factory(reqwest::Method::GET, path, request_options);
719 self.execute_stream(request_factory).await
720 }
721}
722
723async fn read_response(response: Response) -> Result<(Bytes, HeaderMap), OpenAIError> {
724 let headers = response.headers().clone();
725 let bytes = response.bytes().await.map_err(OpenAIError::Reqwest)?;
726 Ok((bytes, headers))
727}
728
729async fn read_error_response(response: Response) -> OpenAIError {
730 let status = response.status();
731 let bytes = match response.bytes().await {
732 Ok(b) => b,
733 Err(e) => return OpenAIError::Reqwest(e),
734 };
735
736 if status.is_server_error() {
737 let message: String = String::from_utf8_lossy(&bytes).into_owned();
739 tracing::warn!("Server error: {status} - {message}");
740 return OpenAIError::ApiError(ApiErrorResponse {
741 status_code: status,
742 api_error: ApiError {
743 misalignment: None,
744 message,
745 r#type: None,
746 param: None,
747 code: None,
748 },
749 });
750 }
751
752 match serde_json::from_slice::<WrappedError>(bytes.as_ref()) {
754 Ok(wrapped) => OpenAIError::ApiError(ApiErrorResponse {
755 status_code: status,
756 api_error: wrapped.error,
757 }),
758 Err(e) => map_deserialization_error(e, bytes.as_ref()),
759 }
760}
761
762pub(crate) async fn stream<O>(response: Response) -> crate::types::stream::StreamResponse<O>
765where
766 O: DeserializeOwned + crate::traits::MaybeSend + 'static,
767{
768 stream_mapped_raw_events(response, |event| {
769 serde_json::from_str::<O>(&event.data)
770 .map_err(|error| map_deserialization_error(error, event.data.as_bytes()))
771 })
772 .await
773}
774
775#[cfg(target_family = "wasm")]
776pub(crate) async fn stream_mapped_raw_events<O>(
777 response: Response,
778 event_mapper: impl Fn(eventsource_stream::Event) -> Result<O, OpenAIError> + 'static,
779) -> crate::types::stream::StreamResponse<O>
780where
781 O: DeserializeOwned + 'static,
782{
783 let byte_stream = response
784 .bytes_stream()
785 .map(|result| result.map_err(std::io::Error::other));
786 let event_stream = Box::pin(eventsource_stream::EventStream::new(byte_stream));
787
788 Box::pin(futures::stream::unfold(
789 (event_stream, event_mapper),
790 |(mut event_stream, event_mapper)| async move {
791 loop {
792 let event = match event_stream.next().await {
793 Some(Ok(event)) => event,
794 Some(Err(error)) => {
795 return Some((
796 Err(OpenAIError::StreamError(Box::new(
797 StreamError::EventStream(error.to_string()),
798 ))),
799 (event_stream, event_mapper),
800 ));
801 }
802 None => return None,
803 };
804
805 if event.data == "[DONE]" {
806 return None;
807 }
808
809 if event.event == "keepalive" {
810 continue;
811 }
812
813 let response = event_mapper(event);
814 return Some((response, (event_stream, event_mapper)));
815 }
816 },
817 ))
818}
819
820#[cfg(not(target_family = "wasm"))]
821pub(crate) async fn stream_mapped_raw_events<O>(
822 response: Response,
823 event_mapper: impl Fn(eventsource_stream::Event) -> Result<O, OpenAIError> + Send + 'static,
824) -> crate::types::stream::StreamResponse<O>
825where
826 O: DeserializeOwned + std::marker::Send + 'static,
827{
828 let (tx, rx) = tokio::sync::mpsc::unbounded_channel();
829
830 tokio::spawn(async move {
831 let byte_stream = response
832 .bytes_stream()
833 .map(|r| r.map_err(std::io::Error::other));
834 let mut event_stream = std::pin::pin!(eventsource_stream::EventStream::new(byte_stream));
835
836 while let Some(ev) = tokio::select! {
841 biased;
842 _ = tx.closed() => None,
843 ev = event_stream.next() => ev,
844 } {
845 let event = match ev {
846 Ok(e) => e,
847 Err(e) => {
848 let _ = tx.send(Err(OpenAIError::StreamError(Box::new(
849 StreamError::EventStream(e.to_string()),
850 ))));
851 break;
852 }
853 };
854 if event.data == "[DONE]" {
855 break;
856 }
857
858 if event.event == "keepalive" {
859 continue;
860 }
861
862 let response = event_mapper(event);
863
864 if tx.send(response).is_err() {
865 break;
866 }
867 }
868 });
869
870 Box::pin(tokio_stream::wrappers::UnboundedReceiverStream::new(rx))
871}
872
873#[cfg(all(test, feature = "middleware", not(target_family = "wasm")))]
874mod tests {
875 use std::sync::{
876 atomic::{AtomicBool, AtomicUsize, Ordering},
877 Arc,
878 };
879
880 use futures::StreamExt;
881 use http::Response as HttpResponse;
882 use serde_json::json;
883 use tower::{service_fn, ServiceBuilder};
884
885 use super::Client;
886 use crate::{
887 config::OpenAIConfig, error::OpenAIError, executor::HttpRequestFactory,
888 retry::SimpleRetryPolicy, traits::AsyncTryFrom, RequestOptions,
889 };
890
891 #[tokio::test]
892 async fn unary_requests_dispatch_through_middleware_service() {
893 let request_count = Arc::new(AtomicUsize::new(0));
894 let service = {
895 let request_count = request_count.clone();
896 ServiceBuilder::new()
897 .concurrency_limit(1)
898 .service(service_fn(move |factory: HttpRequestFactory| {
899 let request_count = request_count.clone();
900 async move {
901 let request = factory.build().await?;
902 assert_eq!(request.url().path(), "/models");
903 request_count.fetch_add(1, Ordering::SeqCst);
904 Ok::<reqwest::Response, OpenAIError>(
905 HttpResponse::builder()
906 .status(200)
907 .header("content-type", "application/json")
908 .body(reqwest::Body::from(
909 "{\"object\":\"list\",\"data\":[{\"id\":\"model\"}]}",
910 ))
911 .unwrap()
912 .into(),
913 )
914 }
915 }))
916 };
917
918 let client = Client::with_config(
919 OpenAIConfig::new()
920 .with_api_base("http://example.test")
921 .with_api_key("test-key"),
922 )
923 .with_http_service(service);
924
925 let value: serde_json::Value = client.get("/models", &RequestOptions::new()).await.unwrap();
926
927 assert_eq!(value["object"], "list");
928 assert_eq!(request_count.load(Ordering::SeqCst), 1);
929 }
930
931 #[tokio::test]
932 async fn stream_requests_open_through_middleware_service() {
933 let request_count = Arc::new(AtomicUsize::new(0));
934 let service = {
935 let request_count = request_count.clone();
936 ServiceBuilder::new()
937 .concurrency_limit(1)
938 .service(service_fn(move |factory: HttpRequestFactory| {
939 let request_count = request_count.clone();
940 async move {
941 let request = factory.build().await?;
942 assert_eq!(request.url().path(), "/responses");
943 request_count.fetch_add(1, Ordering::SeqCst);
944 Ok::<reqwest::Response, OpenAIError>(
945 HttpResponse::builder()
946 .status(200)
947 .header("content-type", "text/event-stream")
948 .body(reqwest::Body::from(
949 "data: {\"ok\":true}\n\ndata: [DONE]\n\n",
950 ))
951 .unwrap()
952 .into(),
953 )
954 }
955 }))
956 };
957
958 let client = Client::with_config(
959 OpenAIConfig::new()
960 .with_api_base("http://example.test")
961 .with_api_key("test-key"),
962 )
963 .with_http_service(service);
964
965 let mut stream = client
966 .post_stream::<_, serde_json::Value>(
967 "/responses",
968 json!({ "stream": true }),
969 &RequestOptions::new(),
970 )
971 .await
972 .unwrap();
973
974 let first = stream.next().await.unwrap().unwrap();
975
976 assert_eq!(first, json!({ "ok": true }));
977 assert_eq!(request_count.load(Ordering::SeqCst), 1);
978 }
979
980 #[tokio::test]
981 async fn middleware_retry_policy_retries_429_responses() {
982 let request_count = Arc::new(AtomicUsize::new(0));
983 let service = {
984 let request_count = request_count.clone();
985 ServiceBuilder::new()
986 .retry(SimpleRetryPolicy::default())
987 .service(service_fn(move |factory: HttpRequestFactory| {
988 let request_count = request_count.clone();
989 async move {
990 let request = factory.build().await?;
991 assert_eq!(request.url().path(), "/models");
992 let attempt = request_count.fetch_add(1, Ordering::SeqCst);
993
994 let response = if attempt == 0 {
995 HttpResponse::builder()
996 .status(429)
997 .header("content-type", "application/json")
998 .body(reqwest::Body::from(
999 r#"{"error":{"message":"retry me","type":"rate_limit_error","param":null,"code":null}}"#,
1000 ))
1001 .unwrap()
1002 } else {
1003 HttpResponse::builder()
1004 .status(200)
1005 .header("content-type", "application/json")
1006 .body(reqwest::Body::from(
1007 r#"{"object":"list","data":[{"id":"retry-model"}]}"#,
1008 ))
1009 .unwrap()
1010 };
1011
1012 Ok::<reqwest::Response, OpenAIError>(response.into())
1013 }
1014 }))
1015 };
1016
1017 let client = Client::with_config(
1018 OpenAIConfig::new()
1019 .with_api_base("http://example.test")
1020 .with_api_key("test-key"),
1021 )
1022 .with_http_service(service);
1023
1024 let value: serde_json::Value = client.get("/models", &RequestOptions::new()).await.unwrap();
1025
1026 assert_eq!(value["data"][0]["id"], "retry-model");
1027 assert_eq!(request_count.load(Ordering::SeqCst), 2);
1028 }
1029
1030 #[derive(Clone)]
1031 struct RetryableMultipartInput {
1032 conversions: Arc<AtomicUsize>,
1033 }
1034
1035 impl AsyncTryFrom<RetryableMultipartInput> for reqwest::multipart::Form {
1036 type Error = OpenAIError;
1037
1038 async fn try_from(value: RetryableMultipartInput) -> Result<Self, Self::Error> {
1039 value.conversions.fetch_add(1, Ordering::SeqCst);
1040 Ok(reqwest::multipart::Form::new().text("field", "value"))
1041 }
1042 }
1043
1044 #[tokio::test]
1045 async fn middleware_retry_policy_rebuilds_multipart_form_per_attempt() {
1046 let request_count = Arc::new(AtomicUsize::new(0));
1047 let conversion_count = Arc::new(AtomicUsize::new(0));
1048
1049 let service = {
1050 let request_count = request_count.clone();
1051 ServiceBuilder::new()
1052 .retry(SimpleRetryPolicy::default())
1053 .service(service_fn(move |factory: HttpRequestFactory| {
1054 let request_count = request_count.clone();
1055 async move {
1056 let request = factory.build().await?;
1057 assert_eq!(request.method(), reqwest::Method::POST);
1058 assert_eq!(request.url().path(), "/files");
1059 let attempt = request_count.fetch_add(1, Ordering::SeqCst);
1060
1061 let response = if attempt == 0 {
1062 HttpResponse::builder()
1063 .status(429)
1064 .header("content-type", "application/json")
1065 .body(reqwest::Body::from(
1066 r#"{"error":{"message":"retry me","type":"rate_limit_error","param":null,"code":null}}"#,
1067 ))
1068 .unwrap()
1069 } else {
1070 HttpResponse::builder()
1071 .status(200)
1072 .header("content-type", "application/json")
1073 .body(reqwest::Body::from(r#"{"ok":true}"#))
1074 .unwrap()
1075 };
1076
1077 Ok::<reqwest::Response, OpenAIError>(response.into())
1078 }
1079 }))
1080 };
1081
1082 let client = Client::with_config(
1083 OpenAIConfig::new()
1084 .with_api_base("http://example.test")
1085 .with_api_key("test-key"),
1086 )
1087 .with_http_service(service);
1088
1089 let value: serde_json::Value = client
1090 .post_form(
1091 "/files",
1092 RetryableMultipartInput {
1093 conversions: conversion_count.clone(),
1094 },
1095 &RequestOptions::new(),
1096 )
1097 .await
1098 .unwrap();
1099
1100 assert_eq!(value, json!({ "ok": true }));
1101 assert_eq!(request_count.load(Ordering::SeqCst), 2);
1102 assert_eq!(conversion_count.load(Ordering::SeqCst), 2);
1103 }
1104
1105 #[tokio::test]
1106 async fn dropping_stream_releases_idle_upstream_response() {
1107 struct DropGuard(Arc<AtomicBool>);
1108
1109 impl Drop for DropGuard {
1110 fn drop(&mut self) {
1111 self.0.store(true, Ordering::SeqCst);
1112 }
1113 }
1114
1115 let upstream_dropped = Arc::new(AtomicBool::new(false));
1116
1117 let dropped = upstream_dropped.clone();
1118 let service = ServiceBuilder::new()
1119 .concurrency_limit(1)
1120 .service(service_fn(move |factory: HttpRequestFactory| {
1121 let guard = DropGuard(dropped.clone());
1122 async move {
1123 factory.build().await?;
1124
1125 let body = futures::stream::once(async {
1128 Ok::<_, std::io::Error>(bytes::Bytes::from_static(
1129 b"data: {\"ok\":true}\n\n",
1130 ))
1131 })
1132 .chain(futures::stream::unfold(
1133 guard,
1134 |guard| async move {
1135 futures::future::pending::<()>().await;
1136 Some((Ok(bytes::Bytes::new()), guard))
1137 },
1138 ));
1139
1140 Ok::<reqwest::Response, OpenAIError>(
1141 HttpResponse::builder()
1142 .status(200)
1143 .header("content-type", "text/event-stream")
1144 .body(reqwest::Body::wrap_stream(body))
1145 .unwrap()
1146 .into(),
1147 )
1148 }
1149 }));
1150
1151 let client = Client::with_config(
1152 OpenAIConfig::new()
1153 .with_api_base("http://example.test")
1154 .with_api_key("test-key"),
1155 )
1156 .with_http_service(service);
1157
1158 let mut stream = client
1159 .post_stream::<_, serde_json::Value>(
1160 "/responses",
1161 json!({ "stream": true }),
1162 &RequestOptions::new(),
1163 )
1164 .await
1165 .unwrap();
1166
1167 assert_eq!(stream.next().await.unwrap().unwrap(), json!({ "ok": true }));
1168 assert!(!upstream_dropped.load(Ordering::SeqCst));
1169
1170 drop(stream);
1171
1172 for _ in 0..100 {
1175 if upstream_dropped.load(Ordering::SeqCst) {
1176 break;
1177 }
1178 tokio::task::yield_now().await;
1179 }
1180
1181 assert!(
1182 upstream_dropped.load(Ordering::SeqCst),
1183 "reader task leaked the upstream response after the stream was dropped"
1184 );
1185 }
1186}