1use crate::error::{Error, ErrorEnvelope, Result};
4use crate::stream::{ndjson_lines, sse_chunks};
5use crate::types::*;
6use crate::wire::{speech_content_type, RawEmbeddingResponse};
7use bytes::Bytes;
8use futures::Stream;
9use reqwest::header::{HeaderValue, AUTHORIZATION, CONTENT_TYPE};
10use reqwest::{Method, RequestBuilder, Response};
11use std::time::Duration;
12
13const ADMIN_TOKEN_HEADER: &str = "x-admin-token";
14
15#[derive(Debug, Clone)]
17pub struct ClientBuilder {
18 base_url: String,
19 api_key: String,
20 admin_token: Option<String>,
21 timeout: Option<Duration>,
22 http: Option<reqwest::Client>,
23}
24
25impl ClientBuilder {
26 fn new(base_url: impl Into<String>, api_key: impl Into<String>) -> Self {
27 ClientBuilder {
28 base_url: base_url.into(),
29 api_key: api_key.into(),
30 admin_token: None,
31 timeout: None,
32 http: None,
33 }
34 }
35
36 pub fn timeout(mut self, timeout: Duration) -> Self {
39 self.timeout = Some(timeout);
40 self
41 }
42
43 pub fn admin_token(mut self, token: impl Into<String>) -> Self {
46 self.admin_token = Some(token.into());
47 self
48 }
49
50 pub fn http_client(mut self, client: reqwest::Client) -> Self {
54 self.http = Some(client);
55 self
56 }
57
58 pub fn build(self) -> Result<Client> {
60 let base_url = self.base_url.trim_end_matches('/').to_string();
62 if base_url.is_empty() {
63 return Err(Error::InvalidBaseUrl("base url is empty".to_string()));
64 }
65
66 let http = match self.http {
67 Some(c) => c,
68 None => {
69 let mut b = reqwest::Client::builder();
70 if let Some(t) = self.timeout {
71 b = b.timeout(t);
72 }
73 b.build()?
74 }
75 };
76
77 let bearer = HeaderValue::from_str(&format!("Bearer {}", self.api_key))
78 .map_err(|_| Error::InvalidBaseUrl("api key contains invalid header bytes".into()))?;
79 let admin = self
80 .admin_token
81 .map(|t| HeaderValue::from_str(&t))
82 .transpose()
83 .map_err(|_| {
84 Error::InvalidBaseUrl("admin token contains invalid header bytes".into())
85 })?;
86
87 Ok(Client {
88 http,
89 base_url,
90 bearer,
91 admin_token: admin,
92 })
93 }
94}
95
96#[derive(Debug, Clone)]
102pub struct Client {
103 http: reqwest::Client,
104 base_url: String,
105 bearer: HeaderValue,
106 admin_token: Option<HeaderValue>,
107}
108
109impl Client {
110 pub fn new(base_url: impl Into<String>, api_key: impl Into<String>) -> Result<Self> {
112 ClientBuilder::new(base_url, api_key).build()
113 }
114
115 pub fn builder(base_url: impl Into<String>, api_key: impl Into<String>) -> ClientBuilder {
117 ClientBuilder::new(base_url, api_key)
118 }
119
120 fn url(&self, path: &str) -> String {
123 format!("{}{}", self.base_url, path)
124 }
125
126 fn request(&self, method: Method, path: &str) -> RequestBuilder {
128 let mut req = self
129 .http
130 .request(method, self.url(path))
131 .header(AUTHORIZATION, self.bearer.clone());
132 if let Some(admin) = &self.admin_token {
133 req = req.header(ADMIN_TOKEN_HEADER, admin.clone());
134 }
135 req
136 }
137
138 async fn send(&self, req: RequestBuilder) -> Result<Response> {
141 let resp = req.send().await?;
142 check_status(resp).await
143 }
144
145 pub async fn chat(&self, mut request: ChatRequest) -> Result<ChatResponse> {
151 request.stream = None;
152 let resp = self
153 .send(
154 self.request(Method::POST, "/v1/chat/completions")
155 .json(&request),
156 )
157 .await?;
158 Ok(resp.json::<ChatResponse>().await?)
159 }
160
161 pub async fn chat_stream(
167 &self,
168 mut request: ChatRequest,
169 ) -> Result<impl Stream<Item = Result<ChatCompletionChunk>>> {
170 request.stream = Some(true);
171 let resp = self
172 .send(
173 self.request(Method::POST, "/v1/chat/completions")
174 .json(&request),
175 )
176 .await?;
177 Ok(sse_chunks(resp.bytes_stream()))
178 }
179
180 pub async fn embeddings(&self, request: EmbeddingRequest) -> Result<EmbeddingResponse> {
185 let resp = self
186 .send(self.request(Method::POST, "/v1/embeddings").json(&request))
187 .await?;
188 let raw = resp.json::<RawEmbeddingResponse>().await?;
189 raw.into_public()
190 }
191
192 pub async fn list_models(
198 &self,
199 model_type: Option<ModelType>,
200 search: Option<&str>,
201 ) -> Result<ListModelsResponse> {
202 let mut req = self.request(Method::GET, "/v1/models");
203 let mut query: Vec<(&str, String)> = Vec::new();
204 if let Some(t) = model_type {
205 query.push(("type", t.as_str().to_string()));
206 }
207 if let Some(s) = search {
208 query.push(("search", s.to_string()));
209 }
210 if !query.is_empty() {
211 req = req.query(&query);
212 }
213 let resp = self.send(req).await?;
214 Ok(resp.json::<ListModelsResponse>().await?)
215 }
216
217 pub async fn speech(&self, request: SpeechRequest) -> Result<(Bytes, String)> {
223 let fallback = speech_content_type(request.response_format.as_deref());
224 let resp = self
225 .send(
226 self.request(Method::POST, "/v1/audio/speech")
227 .json(&request),
228 )
229 .await?;
230 let content_type = resp
231 .headers()
232 .get(CONTENT_TYPE)
233 .and_then(|v| v.to_str().ok())
234 .map(|s| s.to_string())
235 .unwrap_or_else(|| fallback.to_string());
236 let bytes = resp.bytes().await?;
237 Ok((bytes, content_type))
238 }
239
240 pub async fn voices(&self, model: &str) -> Result<VoicesResponse> {
242 let resp = self
243 .send(
244 self.request(Method::GET, "/v1/audio/voices")
245 .query(&[("model", model)]),
246 )
247 .await?;
248 Ok(resp.json::<VoicesResponse>().await?)
249 }
250
251 pub async fn transcribe(
257 &self,
258 request: TranscriptionRequest,
259 file_name: impl Into<String>,
260 audio: impl Into<Bytes>,
261 ) -> Result<Transcription> {
262 let audio: Bytes = audio.into();
263 let part = reqwest::multipart::Part::bytes(audio.to_vec()).file_name(file_name.into());
264
265 let mut form = reqwest::multipart::Form::new()
266 .text("model", request.model.clone())
267 .part("file", part);
268 if let Some(language) = &request.language {
269 form = form.text("language", language.clone());
270 }
271 if let Some(prompt) = &request.prompt {
272 form = form.text("prompt", prompt.clone());
273 }
274 if let Some(rf) = &request.response_format {
275 form = form.text("response_format", rf.clone());
276 }
277 if let Some(t) = request.temperature {
278 form = form.text("temperature", t.to_string());
279 }
280
281 let resp = self
282 .send(
283 self.request(Method::POST, "/v1/audio/transcriptions")
284 .multipart(form),
285 )
286 .await?;
287
288 let structured = matches!(
290 request.response_format.as_deref(),
291 None | Some("json") | Some("verbose_json")
292 );
293 if structured {
294 Ok(Transcription::Json(
295 resp.json::<TranscriptionResponse>().await?,
296 ))
297 } else {
298 Ok(Transcription::Text(resp.text().await?))
299 }
300 }
301
302 pub async fn create_batch(&self, request: BatchCreateRequest) -> Result<BatchHandle> {
306 let resp = self
307 .send(self.request(Method::POST, "/v1/batches").json(&request))
308 .await?;
309 Ok(resp.json::<BatchHandle>().await?)
310 }
311
312 pub async fn get_batch(&self, id: &str) -> Result<BatchHandle> {
314 let resp = self
315 .send(self.request(Method::GET, &format!("/v1/batches/{id}")))
316 .await?;
317 Ok(resp.json::<BatchHandle>().await?)
318 }
319
320 pub async fn cancel_batch(&self, id: &str) -> Result<BatchHandle> {
322 let resp = self
323 .send(self.request(Method::POST, &format!("/v1/batches/{id}/cancel")))
324 .await?;
325 Ok(resp.json::<BatchHandle>().await?)
326 }
327
328 pub async fn batch_results(
331 &self,
332 id: &str,
333 ) -> Result<impl Stream<Item = Result<BatchResultLine>>> {
334 let resp = self
335 .send(self.request(Method::GET, &format!("/v1/batches/{id}/results")))
336 .await?;
337 Ok(ndjson_lines(resp.bytes_stream()))
338 }
339}
340
341async fn check_status(resp: Response) -> Result<Response> {
344 let status = resp.status();
345 if status.is_success() {
346 return Ok(resp);
347 }
348 let code = status.as_u16();
349 let body = resp.bytes().await.unwrap_or_default();
350 let message = parse_error_message(&body).unwrap_or_else(|| {
351 status
352 .canonical_reason()
353 .unwrap_or("request failed")
354 .to_string()
355 });
356 Err(Error::Api {
357 status: code,
358 message,
359 })
360}
361
362fn parse_error_message(body: &[u8]) -> Option<String> {
364 serde_json::from_slice::<ErrorEnvelope>(body)
365 .ok()
366 .map(|e| e.error.message)
367 .filter(|m| !m.is_empty())
368}