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}