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    /// let request = RequestBody {
89    ///     messages: vec![
90    ///         Message::System {
91    ///             content: "This is a request of test purpose. Reply briefly".to_string(),
92    ///             name: None,
93    ///         },
94    ///         Message::User {
95    ///             content: "What's your name?".into(),
96    ///             name: None,
97    ///         },
98    ///     ],
99    ///     model: DEEPSEEK_MODEL.to_string(),
100    ///     stream: Some(true),
101    ///     ..Default::default()
102    /// };
103    ///
104    /// let mut response = request
105    ///     .get_stream_response_string(&default_client(), DEEPSEEK_CHAT_URL, DEEPSEEK_API_KEY)
106    ///     .await?;
107    ///
108    /// while let Some(chunk) = response.next().await {
109    ///     println!("{}", chunk?);
110    /// }
111    /// # Ok(())
112    /// # }
113    /// ```
114    fn get_stream_response_string(
115        &self,
116        client: &reqwest::Client,
117        base_url: &str,
118        api_key: &str,
119    ) -> impl Future<
120        Output = Result<
121            impl Stream<Item = Result<String, OapiError>> + Send + Unpin + 'static,
122            OapiError,
123        >,
124    > + Send
125    + Sync {
126        async move {
127            if !self.is_streaming() {
128                return Err(OapiError::StreamingViolation);
129            }
130
131            let response = client
132                .post(self.build_url(base_url)?)
133                .header("Content-Type", "application/json")
134                .header("Accept", "text/event-stream")
135                .bearer_auth(api_key)
136                .json(self)
137                .send()
138                .await?;
139
140            // Parse the body as an SSE stream. Errors end the stream.
141            let stream = crate::rest::check_status(response)
142                .await?
143                .bytes_stream()
144                .eventsource()
145                .map(|event| match event {
146                    Ok(event) => Ok(event.data),
147                    Err(e) => Err(OapiError::SseParseError(format!("SSE parse error: {e}"))),
148                });
149
150            Ok(stream)
151        }
152    }
153
154    /// Sends a streaming POST request and deserializes every SSE data item
155    /// into [`Self::Response`].
156    ///
157    /// The stream ends after the `data: [DONE]` sentinel or on the first
158    /// error, whichever comes first.
159    fn get_stream_response(
160        &self,
161        client: &reqwest::Client,
162        base_url: &str,
163        api_key: &str,
164    ) -> impl Future<
165        Output = Result<
166            impl Stream<Item = Result<Self::Response, OapiError>> + Send + Unpin + 'static,
167            OapiError,
168        >,
169    > + Send
170    + Sync {
171        async move {
172            let stream = self
173                .get_stream_response_string(client, base_url, api_key)
174                .await?;
175
176            let parsed_stream = stream
177                // The `data: [DONE]` sentinel is not JSON; end the stream there.
178                // `future::ready` keeps the combinators `Unpin` (async blocks
179                // are not).
180                .take_while(|result| {
181                    std::future::ready(matches!(result, Ok(data) if data != "[DONE]"))
182                })
183                .and_then(|data| std::future::ready(Self::Response::from_str(&data)));
184
185            Ok(parsed_stream)
186        }
187    }
188}
189
190/// Trait for POST requests whose response body is binary rather than JSON.
191///
192/// Endpoints such as `POST /audio/speech` return the raw audio bytes instead
193/// of a JSON object, so the body is returned as raw bytes.
194pub trait PostBinary: Post + Serialize + Sync + Send {
195    /// Sends a POST request with a JSON body and returns the raw response
196    /// body bytes.
197    ///
198    /// The `client` is supplied by the caller; see [`crate::rest::default_client`]
199    /// for a sensible default.
200    fn get_response_bytes(
201        &self,
202        client: &reqwest::Client,
203        base_url: &str,
204        api_key: &str,
205    ) -> impl Future<Output = Result<Vec<u8>, OapiError>> + Send + Sync {
206        async move {
207            let response = client
208                .post(self.build_url(base_url)?)
209                .header("Content-Type", "application/json")
210                .header("Accept", "application/octet-stream")
211                .bearer_auth(api_key)
212                .json(self)
213                .send()
214                .await?;
215
216            crate::rest::response_bytes_checked(response).await
217        }
218    }
219
220    /// Sends a POST request with a JSON body and streams the raw response
221    /// body as byte chunks.
222    ///
223    /// Unlike [`Self::get_response_bytes`], the body is not buffered: each
224    /// item is one transport-level chunk. This matches the official SDK's
225    /// streaming binary responses (e.g. `POST /audio/speech` with
226    /// `stream_format`, where the chunks carry either raw audio or SSE
227    /// frames). Parsing any framing inside the byte stream is up to the
228    /// caller.
229    ///
230    /// The `client` is supplied by the caller; see [`crate::rest::default_client`]
231    /// for a sensible default.
232    fn get_stream_response_bytes(
233        &self,
234        client: &reqwest::Client,
235        base_url: &str,
236        api_key: &str,
237    ) -> impl Future<
238        Output = Result<
239            impl Stream<Item = Result<Vec<u8>, OapiError>> + Send + Unpin + 'static,
240            OapiError,
241        >,
242    > + Send
243    + Sync {
244        async move {
245            let response = client
246                .post(self.build_url(base_url)?)
247                .header("Content-Type", "application/json")
248                .header("Accept", "application/octet-stream")
249                .bearer_auth(api_key)
250                .json(self)
251                .send()
252                .await?;
253
254            let response = crate::rest::check_status(response).await?;
255
256            Ok(response.bytes_stream().map(|chunk| {
257                chunk
258                    .map(|bytes| bytes.to_vec())
259                    .map_err(OapiError::SendError)
260            }))
261        }
262    }
263}