1use crate::error::{Error, ErrorEnvelope, Result};
4use crate::stream::{ndjson_lines, sse_chunks, sse_responses};
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 responses(&self, mut request: ResponsesRequest) -> Result<ResponsesResponse> {
188 request.stream = None;
189 let resp = self
190 .send(self.request(Method::POST, "/v1/responses").json(&request))
191 .await?;
192 Ok(resp.json::<ResponsesResponse>().await?)
193 }
194
195 pub async fn responses_stream(
204 &self,
205 mut request: ResponsesRequest,
206 ) -> Result<impl Stream<Item = Result<ResponsesStreamEvent>>> {
207 request.stream = Some(true);
208 let resp = self
209 .send(self.request(Method::POST, "/v1/responses").json(&request))
210 .await?;
211 Ok(sse_responses(resp.bytes_stream()))
212 }
213
214 pub async fn embeddings(&self, request: EmbeddingRequest) -> Result<EmbeddingResponse> {
219 let resp = self
220 .send(self.request(Method::POST, "/v1/embeddings").json(&request))
221 .await?;
222 let raw = resp.json::<RawEmbeddingResponse>().await?;
223 raw.into_public()
224 }
225
226 pub async fn rerank(&self, request: RerankRequest) -> Result<RerankResponse> {
233 let resp = self
234 .send(self.request(Method::POST, "/v1/rerank").json(&request))
235 .await?;
236 Ok(resp.json::<RerankResponse>().await?)
237 }
238
239 pub async fn list_models(
245 &self,
246 model_type: Option<ModelType>,
247 search: Option<&str>,
248 ) -> Result<ListModelsResponse> {
249 let mut req = self.request(Method::GET, "/v1/models");
250 let mut query: Vec<(&str, String)> = Vec::new();
251 if let Some(t) = model_type {
252 query.push(("type", t.as_str().to_string()));
253 }
254 if let Some(s) = search {
255 query.push(("search", s.to_string()));
256 }
257 if !query.is_empty() {
258 req = req.query(&query);
259 }
260 let resp = self.send(req).await?;
261 Ok(resp.json::<ListModelsResponse>().await?)
262 }
263
264 pub async fn speech(&self, request: SpeechRequest) -> Result<(Bytes, String)> {
270 let fallback = speech_content_type(request.response_format.as_deref());
271 let resp = self
272 .send(
273 self.request(Method::POST, "/v1/audio/speech")
274 .json(&request),
275 )
276 .await?;
277 let content_type = resp
278 .headers()
279 .get(CONTENT_TYPE)
280 .and_then(|v| v.to_str().ok())
281 .map(|s| s.to_string())
282 .unwrap_or_else(|| fallback.to_string());
283 let bytes = resp.bytes().await?;
284 Ok((bytes, content_type))
285 }
286
287 pub async fn voices(&self, model: &str) -> Result<VoicesResponse> {
289 let resp = self
290 .send(
291 self.request(Method::GET, "/v1/audio/voices")
292 .query(&[("model", model)]),
293 )
294 .await?;
295 Ok(resp.json::<VoicesResponse>().await?)
296 }
297
298 pub async fn transcribe(
304 &self,
305 request: TranscriptionRequest,
306 file_name: impl Into<String>,
307 audio: impl Into<Bytes>,
308 ) -> Result<Transcription> {
309 let audio: Bytes = audio.into();
310 let part = reqwest::multipart::Part::bytes(audio.to_vec()).file_name(file_name.into());
311
312 let mut form = reqwest::multipart::Form::new()
313 .text("model", request.model.clone())
314 .part("file", part);
315 if let Some(language) = &request.language {
316 form = form.text("language", language.clone());
317 }
318 if let Some(prompt) = &request.prompt {
319 form = form.text("prompt", prompt.clone());
320 }
321 if let Some(rf) = &request.response_format {
322 form = form.text("response_format", rf.clone());
323 }
324 if let Some(t) = request.temperature {
325 form = form.text("temperature", t.to_string());
326 }
327
328 let resp = self
329 .send(
330 self.request(Method::POST, "/v1/audio/transcriptions")
331 .multipart(form),
332 )
333 .await?;
334
335 let structured = matches!(
337 request.response_format.as_deref(),
338 None | Some("json") | Some("verbose_json")
339 );
340 if structured {
341 Ok(Transcription::Json(
342 resp.json::<TranscriptionResponse>().await?,
343 ))
344 } else {
345 Ok(Transcription::Text(resp.text().await?))
346 }
347 }
348
349 pub async fn create_batch(&self, request: BatchCreateRequest) -> Result<BatchHandle> {
353 let resp = self
354 .send(self.request(Method::POST, "/v1/batches").json(&request))
355 .await?;
356 Ok(resp.json::<BatchHandle>().await?)
357 }
358
359 pub async fn get_batch(&self, id: &str) -> Result<BatchHandle> {
361 let resp = self
362 .send(self.request(Method::GET, &format!("/v1/batches/{id}")))
363 .await?;
364 Ok(resp.json::<BatchHandle>().await?)
365 }
366
367 pub async fn cancel_batch(&self, id: &str) -> Result<BatchHandle> {
369 let resp = self
370 .send(self.request(Method::POST, &format!("/v1/batches/{id}/cancel")))
371 .await?;
372 Ok(resp.json::<BatchHandle>().await?)
373 }
374
375 pub async fn batch_results(
378 &self,
379 id: &str,
380 ) -> Result<impl Stream<Item = Result<BatchResultLine>>> {
381 let resp = self
382 .send(self.request(Method::GET, &format!("/v1/batches/{id}/results")))
383 .await?;
384 Ok(ndjson_lines(resp.bytes_stream()))
385 }
386}
387
388async fn check_status(resp: Response) -> Result<Response> {
391 let status = resp.status();
392 if status.is_success() {
393 return Ok(resp);
394 }
395 let code = status.as_u16();
396 let body = resp.bytes().await.unwrap_or_default();
397 let message = parse_error_message(&body).unwrap_or_else(|| {
398 status
399 .canonical_reason()
400 .unwrap_or("request failed")
401 .to_string()
402 });
403 Err(Error::Api {
404 status: code,
405 message,
406 })
407}
408
409fn parse_error_message(body: &[u8]) -> Option<String> {
411 serde_json::from_slice::<ErrorEnvelope>(body)
412 .ok()
413 .map(|e| e.error.message)
414 .filter(|m| !m.is_empty())
415}