Skip to main content

async_openai/
client.rs

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)]
87/// Client is a container for config and HTTP execution
88/// used to make API calls.
89pub 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    /// Client with default [OpenAIConfig]
123    pub fn new() -> Self {
124        Self::default()
125    }
126}
127
128impl<C: Config> Client<C> {
129    /// Create client with a custom HTTP client and config.
130    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    /// Create client with [OpenAIConfig] or [crate::config::AzureConfig]
139    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    /// Provide your own [client] to make HTTP requests with.
149    ///
150    /// [client]: reqwest::Client
151    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    /// Provide your own tower-compatible service to execute HTTP requests.
158    #[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        // This is the public middleware escape hatch. We erase the concrete
166        // tower stack here so the rest of the client does not become generic
167        // over the service type, which would otherwise leak through every API
168        // group and make the crate much harder to use.
169        self.executor = Arc::new(TowerExecutor::new(service));
170        self
171    }
172
173    /// Provide your own tower-compatible service to execute HTTP requests.
174    #[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        // wasm futures produced by reqwest are not `Send`, so the wasm version
182        // intentionally avoids native thread-safety bounds. Users are still
183        // responsible for choosing tower layers that work in their wasm
184        // runtime.
185        self.executor = Arc::new(TowerExecutor::new(service));
186        self
187    }
188
189    // API groups
190
191    /// To call [Models] group related APIs using this client.
192    #[cfg(feature = "model")]
193    pub fn models(&self) -> Models<'_, C> {
194        Models::new(self)
195    }
196
197    /// To call [Completions] group related APIs using this client.
198    #[cfg(feature = "completions")]
199    pub fn completions(&self) -> Completions<'_, C> {
200        Completions::new(self)
201    }
202
203    /// To call [Chat] group related APIs using this client.
204    #[cfg(feature = "chat-completion")]
205    pub fn chat(&self) -> Chat<'_, C> {
206        Chat::new(self)
207    }
208
209    /// To call [Images] group related APIs using this client.
210    #[cfg(feature = "image")]
211    pub fn images(&self) -> Images<'_, C> {
212        Images::new(self)
213    }
214
215    /// To call [Moderations] group related APIs using this client.
216    #[cfg(feature = "moderation")]
217    pub fn moderations(&self) -> Moderations<'_, C> {
218        Moderations::new(self)
219    }
220
221    /// To call [Files] group related APIs using this client.
222    #[cfg(feature = "file")]
223    pub fn files(&self) -> Files<'_, C> {
224        Files::new(self)
225    }
226
227    /// To call [Uploads] group related APIs using this client.
228    #[cfg(feature = "upload")]
229    pub fn uploads(&self) -> Uploads<'_, C> {
230        Uploads::new(self)
231    }
232
233    /// To call [FineTuning] group related APIs using this client.
234    #[cfg(feature = "finetuning")]
235    pub fn fine_tuning(&self) -> FineTuning<'_, C> {
236        FineTuning::new(self)
237    }
238
239    /// To call [Embeddings] group related APIs using this client.
240    #[cfg(feature = "embedding")]
241    pub fn embeddings(&self) -> Embeddings<'_, C> {
242        Embeddings::new(self)
243    }
244
245    /// To call [Audio] group related APIs using this client.
246    #[cfg(feature = "audio")]
247    pub fn audio(&self) -> Audio<'_, C> {
248        Audio::new(self)
249    }
250
251    /// To call [Videos] group related APIs using this client.
252    #[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    /// To call [VectorStores] group related APIs using this client.
260    #[cfg(feature = "vectorstore")]
261    pub fn vector_stores(&self) -> VectorStores<'_, C> {
262        VectorStores::new(self)
263    }
264
265    /// To call [Batches] group related APIs using this client.
266    #[cfg(feature = "batch")]
267    pub fn batches(&self) -> Batches<'_, C> {
268        Batches::new(self)
269    }
270
271    /// To call [Admin] group related APIs using this client.
272    /// This groups together admin API keys, invites, users, projects, audit logs, and certificates.
273    #[cfg(feature = "administration")]
274    pub fn admin(&self) -> Admin<'_, C> {
275        Admin::new(self)
276    }
277
278    /// To call [Responses] group related APIs using this client.
279    #[cfg(feature = "responses")]
280    pub fn responses(&self) -> Responses<'_, C> {
281        Responses::new(self)
282    }
283
284    /// To call [Conversations] group related APIs using this client.
285    #[cfg(feature = "responses")]
286    pub fn conversations(&self) -> Conversations<'_, C> {
287        Conversations::new(self)
288    }
289
290    /// To call [Containers] group related APIs using this client.
291    #[cfg(feature = "container")]
292    pub fn containers(&self) -> Containers<'_, C> {
293        Containers::new(self)
294    }
295
296    /// To call [Skills] group related APIs using this client.
297    #[cfg(feature = "skill")]
298    pub fn skills(&self) -> Skills<'_, C> {
299        Skills::new(self)
300    }
301
302    /// To call [Evals] group related APIs using this client.
303    #[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    /// To call [Realtime] group related APIs using this client.
314    #[cfg(feature = "realtime")]
315    pub fn realtime(&self) -> Realtime<'_, C> {
316        Realtime::new(self)
317    }
318
319    /// To call [Safety] group related APIs.
320    #[cfg(feature = "safety")]
321    pub fn safety(&self) -> crate::Safety<'_, C> {
322        Safety::new(self)
323    }
324
325    /// To call [ContentProvenanceChecks] group related APIs
326    #[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        // JSON bodies are materialized once so the base BYOT path can keep
397        // accepting borrowed inputs.
398        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        // Multipart is the reason the factory exists.
430        //
431        // `Mutex` is only here to make the captured state `Sync` on native targets.
432        #[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    /// Make a GET request to {path} and deserialize the response body
457    #[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    /// Make a DELETE request to {path} and deserialize the response body
472    #[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    /// Make a GET request to {path} and return the response body
487    #[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    /// Make a POST request to {path} and return the response body
499    #[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    /// Make a POST request to {path} and deserialize the response body
519    #[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    /// POST a form at {path} and return the response body
540    #[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    /// POST a form at {path} and deserialize the response body
561    #[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    /// Make HTTP POST request to receive SSE
661    #[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        // Stream setup is still request/response first. We only create the SSE
679        // stream after the HTTP layer has returned a response object.
680        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    /// Make HTTP GET request to receive SSE
708    #[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        // OpenAI does not guarantee server errors are returned as JSON so we cannot deserialize them.
738        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    // Deserialize response body from the error object
753    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
762/// Request which responds with SSE.
763/// [server-sent events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events#event_stream_format)
764pub(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        // Also observe the consumer dropping the stream: relying on
837        // `tx.send(..).is_err()` alone would keep this task - and the upstream
838        // response it holds - alive until the next event arrives, which never
839        // happens when upstream is open but idle.
840        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                    // One event, then an upstream which stays open but never sends
1126                    // again - no further events, no `[DONE]`, no error.
1127                    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        // The reader task should observe the dropped consumer and release the
1173        // response without waiting for another event from upstream.
1174        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}