Skip to main content

openai_interface/rest/
post.rs

1use std::{future::Future, str::FromStr};
2
3use eventsource_stream::Eventsource;
4use futures_util::{Stream, StreamExt, TryStreamExt};
5use serde::{Serialize, de::DeserializeOwned};
6
7use crate::errors::OapiError;
8
9pub trait Post {
10    fn is_streaming(&self) -> bool;
11    /// Builds the URL for the request.
12    ///
13    /// `base_url` should be like <https://api.openai.com/v1>
14    fn build_url(&self, base_url: &str) -> Result<String, OapiError>;
15}
16
17pub trait PostNoStream: Post + Serialize + Sync + Send {
18    type Response: DeserializeOwned + FromStr<Err = OapiError> + Send + Sync;
19
20    /// Sends a POST request and returns the raw response body.
21    ///
22    /// The `client` is supplied by the caller; see [`crate::rest::default_client`]
23    /// for a sensible default.
24    fn get_response_string(
25        &self,
26        client: &reqwest::Client,
27        base_url: &str,
28        key: &str,
29    ) -> impl Future<Output = Result<String, OapiError>> + Send + Sync {
30        async move {
31            if self.is_streaming() {
32                return Err(OapiError::NonStreamingViolation);
33            }
34
35            let response = client
36                .post(self.build_url(base_url)?)
37                .header("Content-Type", "application/json")
38                .header("Accept", "application/json")
39                .bearer_auth(key)
40                .json(self)
41                .send()
42                .await?;
43
44            crate::rest::response_text_checked(response).await
45        }
46    }
47
48    /// Sends a POST request and deserializes the response.
49    fn get_response(
50        &self,
51        client: &reqwest::Client,
52        url: &str,
53        key: &str,
54    ) -> impl Future<Output = Result<Self::Response, OapiError>> + Send + Sync {
55        async move {
56            let text = self.get_response_string(client, url, key).await?;
57            let result = Self::Response::from_str(&text)?;
58            Ok(result)
59        }
60    }
61}
62
63pub trait PostStream: Post + Serialize + Sync + Send {
64    type Response: DeserializeOwned + FromStr<Err = OapiError> + Send + Sync + 'static;
65
66    /// Sends a streaming POST request and returns the raw SSE data items as
67    /// strings.
68    ///
69    /// The returned stream stops after the `data: [DONE]` sentinel. Errors
70    /// terminate the stream.
71    ///
72    /// The `client` is supplied by the caller; see [`crate::rest::default_client`]
73    /// for a sensible default.
74    ///
75    /// # Example
76    ///
77    /// ```rust,no_run
78    /// use std::sync::LazyLock;
79    /// use futures_util::StreamExt;
80    /// use openai_interface::chat::create::request::{Message, RequestBody};
81    /// use openai_interface::rest::{default_client, post::PostStream};
82    ///
83    /// const DEEPSEEK_API_KEY: &str = "YOUR_API_KEY";
84    /// const DEEPSEEK_CHAT_URL: &'static str = "https://api.deepseek.com";
85    /// const DEEPSEEK_MODEL: &'static str = "deepseek-chat";
86    ///
87    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
88    /// // Needs the `ferritls` cargo feature; drop this line if you install
89    /// // your own rustls crypto provider (see `openai_interface::rest`).
90    /// # #[cfg(feature = "ferritls")]
91    /// openai_interface::rest::install_crypto_provider().ok();
92    ///
93    /// let request = RequestBody {
94    ///     messages: vec![
95    ///         Message::System {
96    ///             content: "This is a request of test purpose. Reply briefly".to_string(),
97    ///             name: None,
98    ///         },
99    ///         Message::User {
100    ///             content: "What's your name?".into(),
101    ///             name: None,
102    ///         },
103    ///     ],
104    ///     model: DEEPSEEK_MODEL.to_string(),
105    ///     stream: Some(true),
106    ///     ..Default::default()
107    /// };
108    ///
109    /// let mut response = request
110    ///     .get_stream_response_string(&default_client(), DEEPSEEK_CHAT_URL, DEEPSEEK_API_KEY)
111    ///     .await?;
112    ///
113    /// while let Some(chunk) = response.next().await {
114    ///     println!("{}", chunk?);
115    /// }
116    /// # Ok(())
117    /// # }
118    /// ```
119    fn get_stream_response_string(
120        &self,
121        client: &reqwest::Client,
122        base_url: &str,
123        api_key: &str,
124    ) -> impl Future<
125        Output = Result<
126            impl Stream<Item = Result<String, OapiError>> + Send + Unpin + 'static,
127            OapiError,
128        >,
129    > + Send
130    + Sync {
131        async move {
132            if !self.is_streaming() {
133                return Err(OapiError::StreamingViolation);
134            }
135
136            let response = client
137                .post(self.build_url(base_url)?)
138                .header("Content-Type", "application/json")
139                .header("Accept", "text/event-stream")
140                .bearer_auth(api_key)
141                .json(self)
142                .send()
143                .await?;
144
145            // Parse the body as an SSE stream. Errors end the stream.
146            let stream = crate::rest::check_status(response)
147                .await?
148                .bytes_stream()
149                .eventsource()
150                .map(|event| match event {
151                    Ok(event) => Ok(event.data),
152                    Err(e) => Err(OapiError::SseParseError(format!("SSE parse error: {e}"))),
153                });
154
155            Ok(stream)
156        }
157    }
158
159    /// Sends a streaming POST request and deserializes every SSE data item
160    /// into [`Self::Response`].
161    ///
162    /// The stream ends after the `data: [DONE]` sentinel or on the first
163    /// error, whichever comes first.
164    fn get_stream_response(
165        &self,
166        client: &reqwest::Client,
167        base_url: &str,
168        api_key: &str,
169    ) -> impl Future<
170        Output = Result<
171            impl Stream<Item = Result<Self::Response, OapiError>> + Send + Unpin + 'static,
172            OapiError,
173        >,
174    > + Send
175    + Sync {
176        async move {
177            let stream = self
178                .get_stream_response_string(client, base_url, api_key)
179                .await?;
180
181            let parsed_stream = stream
182                // The `data: [DONE]` sentinel is not JSON; end the stream there.
183                // `future::ready` keeps the combinators `Unpin` (async blocks
184                // are not).
185                .take_while(|result| {
186                    std::future::ready(matches!(result, Ok(data) if data != "[DONE]"))
187                })
188                .and_then(|data| std::future::ready(Self::Response::from_str(&data)));
189
190            Ok(parsed_stream)
191        }
192    }
193}
194
195/// Trait for POST requests whose response body is binary rather than JSON.
196///
197/// Endpoints such as `POST /audio/speech` return the raw audio bytes instead
198/// of a JSON object, so the body is returned as raw bytes.
199pub trait PostBinary: Post + Serialize + Sync + Send {
200    /// Sends a POST request with a JSON body and returns the raw response
201    /// body bytes.
202    ///
203    /// The `client` is supplied by the caller; see [`crate::rest::default_client`]
204    /// for a sensible default.
205    fn get_response_bytes(
206        &self,
207        client: &reqwest::Client,
208        base_url: &str,
209        api_key: &str,
210    ) -> impl Future<Output = Result<Vec<u8>, OapiError>> + Send + Sync {
211        async move {
212            let response = client
213                .post(self.build_url(base_url)?)
214                .header("Content-Type", "application/json")
215                .header("Accept", "application/octet-stream")
216                .bearer_auth(api_key)
217                .json(self)
218                .send()
219                .await?;
220
221            crate::rest::response_bytes_checked(response).await
222        }
223    }
224
225    /// Sends a POST request with a JSON body and streams the raw response
226    /// body as byte chunks.
227    ///
228    /// Unlike [`Self::get_response_bytes`], the body is not buffered: each
229    /// item is one transport-level chunk. This matches the official SDK's
230    /// streaming binary responses (e.g. `POST /audio/speech` with
231    /// `stream_format`, where the chunks carry either raw audio or SSE
232    /// frames). Parsing any framing inside the byte stream is up to the
233    /// caller.
234    ///
235    /// The `client` is supplied by the caller; see [`crate::rest::default_client`]
236    /// for a sensible default.
237    fn get_stream_response_bytes(
238        &self,
239        client: &reqwest::Client,
240        base_url: &str,
241        api_key: &str,
242    ) -> impl Future<
243        Output = Result<
244            impl Stream<Item = Result<Vec<u8>, OapiError>> + Send + Unpin + 'static,
245            OapiError,
246        >,
247    > + Send
248    + Sync {
249        async move {
250            let response = client
251                .post(self.build_url(base_url)?)
252                .header("Content-Type", "application/json")
253                .header("Accept", "application/octet-stream")
254                .bearer_auth(api_key)
255                .json(self)
256                .send()
257                .await?;
258
259            let response = crate::rest::check_status(response).await?;
260
261            Ok(response.bytes_stream().map(|chunk| {
262                chunk
263                    .map(|bytes| bytes.to_vec())
264                    .map_err(OapiError::SendError)
265            }))
266        }
267    }
268}