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}