open_ai.rs

  1pub mod batches;
  2pub mod completion;
  3pub mod responses;
  4
  5use anyhow::{Context as _, Result, anyhow};
  6use futures::{AsyncBufReadExt, AsyncReadExt, StreamExt, io::BufReader, stream::BoxStream};
  7use http_client::{
  8    AsyncBody, HttpClient, Method, Request as HttpRequest, StatusCode,
  9    http::{HeaderMap, HeaderValue},
 10};
 11pub use language_model_core::ReasoningEffort;
 12use serde::{Deserialize, Serialize};
 13use serde_json::Value;
 14use std::{convert::TryFrom, future::Future};
 15use strum::EnumIter;
 16use thiserror::Error;
 17
 18pub const OPEN_AI_API_URL: &str = "https://api.openai.com/v1";
 19
 20fn is_none_or_empty<T: AsRef<[U]>, U>(opt: &Option<T>) -> bool {
 21    opt.as_ref().is_none_or(|v| v.as_ref().is_empty())
 22}
 23
 24#[derive(Clone, Copy, Serialize, Deserialize, Debug, Eq, PartialEq)]
 25#[serde(rename_all = "lowercase")]
 26pub enum Role {
 27    User,
 28    Assistant,
 29    System,
 30    Tool,
 31}
 32
 33impl TryFrom<String> for Role {
 34    type Error = anyhow::Error;
 35
 36    fn try_from(value: String) -> Result<Self> {
 37        match value.as_str() {
 38            "user" => Ok(Self::User),
 39            "assistant" => Ok(Self::Assistant),
 40            "system" => Ok(Self::System),
 41            "tool" => Ok(Self::Tool),
 42            _ => anyhow::bail!("invalid role '{value}'"),
 43        }
 44    }
 45}
 46
 47impl From<Role> for String {
 48    fn from(val: Role) -> Self {
 49        match val {
 50            Role::User => "user".to_owned(),
 51            Role::Assistant => "assistant".to_owned(),
 52            Role::System => "system".to_owned(),
 53            Role::Tool => "tool".to_owned(),
 54        }
 55    }
 56}
 57
 58#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
 59#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, EnumIter)]
 60pub enum Model {
 61    #[serde(rename = "gpt-3.5-turbo")]
 62    ThreePointFiveTurbo,
 63    #[serde(rename = "gpt-4")]
 64    Four,
 65    #[serde(rename = "gpt-4-turbo")]
 66    FourTurbo,
 67    #[serde(rename = "gpt-4o-mini")]
 68    FourOmniMini,
 69    #[serde(rename = "gpt-4.1-nano")]
 70    FourPointOneNano,
 71    #[serde(rename = "o1")]
 72    O1,
 73    #[serde(rename = "o3-mini")]
 74    O3Mini,
 75    #[serde(rename = "o3")]
 76    O3,
 77    #[serde(rename = "gpt-5")]
 78    Five,
 79    #[serde(rename = "gpt-5-codex")]
 80    FiveCodex,
 81    #[serde(rename = "gpt-5-mini")]
 82    #[default]
 83    FiveMini,
 84    #[serde(rename = "gpt-5-nano")]
 85    FiveNano,
 86    #[serde(rename = "gpt-5.1")]
 87    FivePointOne,
 88    #[serde(rename = "gpt-5.2")]
 89    FivePointTwo,
 90    #[serde(rename = "gpt-5.2-codex")]
 91    FivePointTwoCodex,
 92    #[serde(rename = "gpt-5.3-codex")]
 93    FivePointThreeCodex,
 94    #[serde(rename = "gpt-5.4")]
 95    FivePointFour,
 96    #[serde(rename = "gpt-5.4-pro")]
 97    FivePointFourPro,
 98    #[serde(rename = "custom")]
 99    Custom {
100        name: String,
101        /// The name displayed in the UI, such as in the assistant panel model dropdown menu.
102        display_name: Option<String>,
103        max_tokens: u64,
104        max_output_tokens: Option<u64>,
105        max_completion_tokens: Option<u64>,
106        reasoning_effort: Option<ReasoningEffort>,
107        #[serde(default = "default_supports_chat_completions")]
108        supports_chat_completions: bool,
109    },
110}
111
112const fn default_supports_chat_completions() -> bool {
113    true
114}
115
116impl Model {
117    pub fn default_fast() -> Self {
118        Self::FiveMini
119    }
120
121    pub fn from_id(id: &str) -> Result<Self> {
122        match id {
123            "gpt-3.5-turbo" => Ok(Self::ThreePointFiveTurbo),
124            "gpt-4" => Ok(Self::Four),
125            "gpt-4-turbo-preview" => Ok(Self::FourTurbo),
126            "gpt-4o-mini" => Ok(Self::FourOmniMini),
127            "gpt-4.1-nano" => Ok(Self::FourPointOneNano),
128            "o1" => Ok(Self::O1),
129            "o3-mini" => Ok(Self::O3Mini),
130            "o3" => Ok(Self::O3),
131            "gpt-5" => Ok(Self::Five),
132            "gpt-5-codex" => Ok(Self::FiveCodex),
133            "gpt-5-mini" => Ok(Self::FiveMini),
134            "gpt-5-nano" => Ok(Self::FiveNano),
135            "gpt-5.1" => Ok(Self::FivePointOne),
136            "gpt-5.2" => Ok(Self::FivePointTwo),
137            "gpt-5.2-codex" => Ok(Self::FivePointTwoCodex),
138            "gpt-5.3-codex" => Ok(Self::FivePointThreeCodex),
139            "gpt-5.4" => Ok(Self::FivePointFour),
140            "gpt-5.4-pro" => Ok(Self::FivePointFourPro),
141            invalid_id => anyhow::bail!("invalid model id '{invalid_id}'"),
142        }
143    }
144
145    pub fn id(&self) -> &str {
146        match self {
147            Self::ThreePointFiveTurbo => "gpt-3.5-turbo",
148            Self::Four => "gpt-4",
149            Self::FourTurbo => "gpt-4-turbo",
150            Self::FourOmniMini => "gpt-4o-mini",
151            Self::FourPointOneNano => "gpt-4.1-nano",
152            Self::O1 => "o1",
153            Self::O3Mini => "o3-mini",
154            Self::O3 => "o3",
155            Self::Five => "gpt-5",
156            Self::FiveCodex => "gpt-5-codex",
157            Self::FiveMini => "gpt-5-mini",
158            Self::FiveNano => "gpt-5-nano",
159            Self::FivePointOne => "gpt-5.1",
160            Self::FivePointTwo => "gpt-5.2",
161            Self::FivePointTwoCodex => "gpt-5.2-codex",
162            Self::FivePointThreeCodex => "gpt-5.3-codex",
163            Self::FivePointFour => "gpt-5.4",
164            Self::FivePointFourPro => "gpt-5.4-pro",
165            Self::Custom { name, .. } => name,
166        }
167    }
168
169    pub fn display_name(&self) -> &str {
170        match self {
171            Self::ThreePointFiveTurbo => "gpt-3.5-turbo",
172            Self::Four => "gpt-4",
173            Self::FourTurbo => "gpt-4-turbo",
174            Self::FourOmniMini => "gpt-4o-mini",
175            Self::FourPointOneNano => "gpt-4.1-nano",
176            Self::O1 => "o1",
177            Self::O3Mini => "o3-mini",
178            Self::O3 => "o3",
179            Self::Five => "gpt-5",
180            Self::FiveCodex => "gpt-5-codex",
181            Self::FiveMini => "gpt-5-mini",
182            Self::FiveNano => "gpt-5-nano",
183            Self::FivePointOne => "gpt-5.1",
184            Self::FivePointTwo => "gpt-5.2",
185            Self::FivePointTwoCodex => "gpt-5.2-codex",
186            Self::FivePointThreeCodex => "gpt-5.3-codex",
187            Self::FivePointFour => "gpt-5.4",
188            Self::FivePointFourPro => "gpt-5.4-pro",
189            Self::Custom { display_name, .. } => display_name.as_deref().unwrap_or(&self.id()),
190        }
191    }
192
193    pub fn max_token_count(&self) -> u64 {
194        match self {
195            Self::ThreePointFiveTurbo => 16_385,
196            Self::Four => 8_192,
197            Self::FourTurbo => 128_000,
198            Self::FourOmniMini => 128_000,
199            Self::FourPointOneNano => 1_047_576,
200            Self::O1 => 200_000,
201            Self::O3Mini => 200_000,
202            Self::O3 => 200_000,
203            Self::Five => 272_000,
204            Self::FiveCodex => 272_000,
205            Self::FiveMini => 400_000,
206            Self::FiveNano => 400_000,
207            Self::FivePointOne => 400_000,
208            Self::FivePointTwo => 400_000,
209            Self::FivePointTwoCodex => 400_000,
210            Self::FivePointThreeCodex => 400_000,
211            Self::FivePointFour => 1_050_000,
212            Self::FivePointFourPro => 1_050_000,
213            Self::Custom { max_tokens, .. } => *max_tokens,
214        }
215    }
216
217    pub fn max_output_tokens(&self) -> Option<u64> {
218        match self {
219            Self::Custom {
220                max_output_tokens, ..
221            } => *max_output_tokens,
222            Self::ThreePointFiveTurbo => Some(4_096),
223            Self::Four => Some(8_192),
224            Self::FourTurbo => Some(4_096),
225            Self::FourOmniMini => Some(16_384),
226            Self::FourPointOneNano => Some(32_768),
227            Self::O1 => Some(100_000),
228            Self::O3Mini => Some(100_000),
229            Self::O3 => Some(100_000),
230            Self::Five => Some(128_000),
231            Self::FiveCodex => Some(128_000),
232            Self::FiveMini => Some(128_000),
233            Self::FiveNano => Some(128_000),
234            Self::FivePointOne => Some(128_000),
235            Self::FivePointTwo => Some(128_000),
236            Self::FivePointTwoCodex => Some(128_000),
237            Self::FivePointThreeCodex => Some(128_000),
238            Self::FivePointFour => Some(128_000),
239            Self::FivePointFourPro => Some(128_000),
240        }
241    }
242
243    pub fn reasoning_effort(&self) -> Option<ReasoningEffort> {
244        match self {
245            Self::Custom {
246                reasoning_effort, ..
247            } => reasoning_effort.to_owned(),
248            Self::FivePointThreeCodex | Self::FivePointFourPro => Some(ReasoningEffort::Medium),
249            _ => None,
250        }
251    }
252
253    pub fn supports_chat_completions(&self) -> bool {
254        match self {
255            Self::Custom {
256                supports_chat_completions,
257                ..
258            } => *supports_chat_completions,
259            Self::FiveCodex
260            | Self::FivePointTwoCodex
261            | Self::FivePointThreeCodex
262            | Self::FivePointFourPro => false,
263            _ => true,
264        }
265    }
266
267    /// Returns whether the given model supports the `parallel_tool_calls` parameter.
268    ///
269    /// If the model does not support the parameter, do not pass it up, or the API will return an error.
270    pub fn supports_parallel_tool_calls(&self) -> bool {
271        match self {
272            Self::ThreePointFiveTurbo
273            | Self::Four
274            | Self::FourTurbo
275            | Self::FourOmniMini
276            | Self::FourPointOneNano
277            | Self::Five
278            | Self::FiveCodex
279            | Self::FiveMini
280            | Self::FivePointOne
281            | Self::FivePointTwo
282            | Self::FivePointTwoCodex
283            | Self::FivePointThreeCodex
284            | Self::FivePointFour
285            | Self::FivePointFourPro
286            | Self::FiveNano => true,
287            Self::O1 | Self::O3 | Self::O3Mini | Model::Custom { .. } => false,
288        }
289    }
290
291    /// Returns whether the given model supports the `prompt_cache_key` parameter.
292    ///
293    /// If the model does not support the parameter, do not pass it up.
294    pub fn supports_prompt_cache_key(&self) -> bool {
295        true
296    }
297}
298
299#[derive(Debug, Serialize, Deserialize)]
300pub struct StreamOptions {
301    pub include_usage: bool,
302}
303
304impl Default for StreamOptions {
305    fn default() -> Self {
306        Self {
307            include_usage: true,
308        }
309    }
310}
311
312#[derive(Debug, Serialize, Deserialize)]
313pub struct Request {
314    pub model: String,
315    pub messages: Vec<RequestMessage>,
316    pub stream: bool,
317    #[serde(default, skip_serializing_if = "Option::is_none")]
318    pub stream_options: Option<StreamOptions>,
319    #[serde(default, skip_serializing_if = "Option::is_none")]
320    pub max_completion_tokens: Option<u64>,
321    #[serde(default, skip_serializing_if = "Vec::is_empty")]
322    pub stop: Vec<String>,
323    #[serde(default, skip_serializing_if = "Option::is_none")]
324    pub temperature: Option<f32>,
325    #[serde(default, skip_serializing_if = "Option::is_none")]
326    pub tool_choice: Option<ToolChoice>,
327    /// Whether to enable parallel function calling during tool use.
328    #[serde(default, skip_serializing_if = "Option::is_none")]
329    pub parallel_tool_calls: Option<bool>,
330    #[serde(default, skip_serializing_if = "Vec::is_empty")]
331    pub tools: Vec<ToolDefinition>,
332    #[serde(default, skip_serializing_if = "Option::is_none")]
333    pub prompt_cache_key: Option<String>,
334    #[serde(default, skip_serializing_if = "Option::is_none")]
335    pub reasoning_effort: Option<ReasoningEffort>,
336}
337
338#[derive(Debug, Serialize, Deserialize)]
339#[serde(rename_all = "lowercase")]
340pub enum ToolChoice {
341    Auto,
342    Required,
343    None,
344    #[serde(untagged)]
345    Other(ToolDefinition),
346}
347
348#[derive(Clone, Deserialize, Serialize, Debug)]
349#[serde(tag = "type", rename_all = "snake_case")]
350pub enum ToolDefinition {
351    #[allow(dead_code)]
352    Function { function: FunctionDefinition },
353}
354
355#[derive(Clone, Debug, Serialize, Deserialize)]
356pub struct FunctionDefinition {
357    pub name: String,
358    pub description: Option<String>,
359    pub parameters: Option<Value>,
360}
361
362#[derive(Clone, Serialize, Deserialize, Debug, Eq, PartialEq)]
363#[serde(tag = "role", rename_all = "lowercase")]
364pub enum RequestMessage {
365    Assistant {
366        content: Option<MessageContent>,
367        #[serde(default, skip_serializing_if = "Vec::is_empty")]
368        tool_calls: Vec<ToolCall>,
369    },
370    User {
371        content: MessageContent,
372    },
373    System {
374        content: MessageContent,
375    },
376    Tool {
377        content: MessageContent,
378        tool_call_id: String,
379    },
380}
381
382#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)]
383#[serde(untagged)]
384pub enum MessageContent {
385    Plain(String),
386    Multipart(Vec<MessagePart>),
387}
388
389impl MessageContent {
390    pub fn empty() -> Self {
391        MessageContent::Multipart(vec![])
392    }
393
394    pub fn push_part(&mut self, part: MessagePart) {
395        match self {
396            MessageContent::Plain(text) => {
397                *self =
398                    MessageContent::Multipart(vec![MessagePart::Text { text: text.clone() }, part]);
399            }
400            MessageContent::Multipart(parts) if parts.is_empty() => match part {
401                MessagePart::Text { text } => *self = MessageContent::Plain(text),
402                MessagePart::Image { .. } => *self = MessageContent::Multipart(vec![part]),
403            },
404            MessageContent::Multipart(parts) => parts.push(part),
405        }
406    }
407}
408
409impl From<Vec<MessagePart>> for MessageContent {
410    fn from(mut parts: Vec<MessagePart>) -> Self {
411        if let [MessagePart::Text { text }] = parts.as_mut_slice() {
412            MessageContent::Plain(std::mem::take(text))
413        } else {
414            MessageContent::Multipart(parts)
415        }
416    }
417}
418
419#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)]
420#[serde(tag = "type")]
421pub enum MessagePart {
422    #[serde(rename = "text")]
423    Text { text: String },
424    #[serde(rename = "image_url")]
425    Image { image_url: ImageUrl },
426}
427
428#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)]
429pub struct ImageUrl {
430    pub url: String,
431    #[serde(skip_serializing_if = "Option::is_none")]
432    pub detail: Option<String>,
433}
434
435#[derive(Clone, Serialize, Deserialize, Debug, Eq, PartialEq)]
436pub struct ToolCall {
437    pub id: String,
438    #[serde(flatten)]
439    pub content: ToolCallContent,
440}
441
442#[derive(Clone, Serialize, Deserialize, Debug, Eq, PartialEq)]
443#[serde(tag = "type", rename_all = "lowercase")]
444pub enum ToolCallContent {
445    Function { function: FunctionContent },
446}
447
448#[derive(Clone, Serialize, Deserialize, Debug, Eq, PartialEq)]
449pub struct FunctionContent {
450    pub name: String,
451    pub arguments: String,
452}
453
454#[derive(Clone, Serialize, Deserialize, Debug)]
455pub struct Response {
456    pub id: String,
457    pub object: String,
458    pub created: u64,
459    pub model: String,
460    pub choices: Vec<Choice>,
461    pub usage: Usage,
462}
463
464#[derive(Clone, Serialize, Deserialize, Debug)]
465pub struct Choice {
466    pub index: u32,
467    pub message: RequestMessage,
468    pub finish_reason: Option<String>,
469}
470
471#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
472pub struct ResponseMessageDelta {
473    pub role: Option<Role>,
474    pub content: Option<String>,
475    #[serde(default, skip_serializing_if = "is_none_or_empty")]
476    pub tool_calls: Option<Vec<ToolCallChunk>>,
477    #[serde(default, skip_serializing_if = "is_none_or_empty")]
478    pub reasoning_content: Option<String>,
479}
480
481#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
482pub struct ToolCallChunk {
483    pub index: usize,
484    pub id: Option<String>,
485
486    // There is also an optional `type` field that would determine if a
487    // function is there. Sometimes this streams in with the `function` before
488    // it streams in the `type`
489    pub function: Option<FunctionChunk>,
490}
491
492#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
493pub struct FunctionChunk {
494    pub name: Option<String>,
495    pub arguments: Option<String>,
496}
497
498#[derive(Clone, Serialize, Deserialize, Debug)]
499pub struct Usage {
500    pub prompt_tokens: u64,
501    pub completion_tokens: u64,
502    pub total_tokens: u64,
503}
504
505#[derive(Serialize, Deserialize, Debug)]
506pub struct ChoiceDelta {
507    pub index: u32,
508    pub delta: Option<ResponseMessageDelta>,
509    pub finish_reason: Option<String>,
510}
511
512#[derive(Error, Debug)]
513pub enum RequestError {
514    #[error("HTTP response error from {provider}'s API: status {status_code} - {body:?}")]
515    HttpResponseError {
516        provider: String,
517        status_code: StatusCode,
518        body: String,
519        headers: HeaderMap<HeaderValue>,
520    },
521    #[error(transparent)]
522    Other(#[from] anyhow::Error),
523}
524
525#[derive(Serialize, Deserialize, Debug)]
526pub struct ResponseStreamError {
527    message: String,
528}
529
530#[derive(Serialize, Deserialize, Debug)]
531#[serde(untagged)]
532pub enum ResponseStreamResult {
533    Ok(ResponseStreamEvent),
534    Err { error: ResponseStreamError },
535}
536
537#[derive(Serialize, Deserialize, Debug)]
538pub struct ResponseStreamEvent {
539    pub choices: Vec<ChoiceDelta>,
540    pub usage: Option<Usage>,
541}
542
543pub async fn non_streaming_completion(
544    client: &dyn HttpClient,
545    api_url: &str,
546    api_key: &str,
547    request: Request,
548) -> Result<Response, RequestError> {
549    let uri = format!("{api_url}/chat/completions");
550    let request_builder = HttpRequest::builder()
551        .method(Method::POST)
552        .uri(uri)
553        .header("Content-Type", "application/json")
554        .header("Authorization", format!("Bearer {}", api_key.trim()));
555
556    let request = request_builder
557        .body(AsyncBody::from(
558            serde_json::to_string(&request).map_err(|e| RequestError::Other(e.into()))?,
559        ))
560        .map_err(|e| RequestError::Other(e.into()))?;
561
562    let mut response = client.send(request).await?;
563    if response.status().is_success() {
564        let mut body = String::new();
565        response
566            .body_mut()
567            .read_to_string(&mut body)
568            .await
569            .map_err(|e| RequestError::Other(e.into()))?;
570
571        serde_json::from_str(&body).map_err(|e| RequestError::Other(e.into()))
572    } else {
573        let mut body = String::new();
574        response
575            .body_mut()
576            .read_to_string(&mut body)
577            .await
578            .map_err(|e| RequestError::Other(e.into()))?;
579
580        Err(RequestError::HttpResponseError {
581            provider: "openai".to_owned(),
582            status_code: response.status(),
583            body,
584            headers: response.headers().clone(),
585        })
586    }
587}
588
589pub async fn stream_completion(
590    client: &dyn HttpClient,
591    provider_name: &str,
592    api_url: &str,
593    api_key: &str,
594    request: Request,
595) -> Result<BoxStream<'static, Result<ResponseStreamEvent>>, RequestError> {
596    let uri = format!("{api_url}/chat/completions");
597    let request_builder = HttpRequest::builder()
598        .method(Method::POST)
599        .uri(uri)
600        .header("Content-Type", "application/json")
601        .header("Authorization", format!("Bearer {}", api_key.trim()));
602
603    let request = request_builder
604        .body(AsyncBody::from(
605            serde_json::to_string(&request).map_err(|e| RequestError::Other(e.into()))?,
606        ))
607        .map_err(|e| RequestError::Other(e.into()))?;
608
609    let mut response = client.send(request).await?;
610    if response.status().is_success() {
611        let reader = BufReader::new(response.into_body());
612        Ok(reader
613            .lines()
614            .filter_map(|line| async move {
615                match line {
616                    Ok(line) => {
617                        let line = line.strip_prefix("data: ").or_else(|| line.strip_prefix("data:"))?;
618                        if line == "[DONE]" {
619                            None
620                        } else {
621                            match serde_json::from_str(line) {
622                                Ok(ResponseStreamResult::Ok(response)) => Some(Ok(response)),
623                                Ok(ResponseStreamResult::Err { error }) => {
624                                    Some(Err(anyhow!(error.message)))
625                                }
626                                Err(error) => {
627                                    log::error!(
628                                        "Failed to parse OpenAI response into ResponseStreamResult: `{}`\n\
629                                        Response: `{}`",
630                                        error,
631                                        line,
632                                    );
633                                    Some(Err(anyhow!(error)))
634                                }
635                            }
636                        }
637                    }
638                    Err(error) => Some(Err(anyhow!(error))),
639                }
640            })
641            .boxed())
642    } else {
643        let mut body = String::new();
644        response
645            .body_mut()
646            .read_to_string(&mut body)
647            .await
648            .map_err(|e| RequestError::Other(e.into()))?;
649
650        Err(RequestError::HttpResponseError {
651            provider: provider_name.to_owned(),
652            status_code: response.status(),
653            body,
654            headers: response.headers().clone(),
655        })
656    }
657}
658
659#[derive(Copy, Clone, Serialize, Deserialize)]
660pub enum OpenAiEmbeddingModel {
661    #[serde(rename = "text-embedding-3-small")]
662    TextEmbedding3Small,
663    #[serde(rename = "text-embedding-3-large")]
664    TextEmbedding3Large,
665}
666
667#[derive(Serialize)]
668struct OpenAiEmbeddingRequest<'a> {
669    model: OpenAiEmbeddingModel,
670    input: Vec<&'a str>,
671}
672
673#[derive(Deserialize)]
674pub struct OpenAiEmbeddingResponse {
675    pub data: Vec<OpenAiEmbedding>,
676}
677
678#[derive(Deserialize)]
679pub struct OpenAiEmbedding {
680    pub embedding: Vec<f32>,
681}
682
683pub fn embed<'a>(
684    client: &dyn HttpClient,
685    api_url: &str,
686    api_key: &str,
687    model: OpenAiEmbeddingModel,
688    texts: impl IntoIterator<Item = &'a str>,
689) -> impl 'static + Future<Output = Result<OpenAiEmbeddingResponse>> {
690    let uri = format!("{api_url}/embeddings");
691
692    let request = OpenAiEmbeddingRequest {
693        model,
694        input: texts.into_iter().collect(),
695    };
696    let body = AsyncBody::from(serde_json::to_string(&request).unwrap());
697    let request = HttpRequest::builder()
698        .method(Method::POST)
699        .uri(uri)
700        .header("Content-Type", "application/json")
701        .header("Authorization", format!("Bearer {}", api_key.trim()))
702        .body(body)
703        .map(|request| client.send(request));
704
705    async move {
706        let mut response = request?.await?;
707        let mut body = String::new();
708        response.body_mut().read_to_string(&mut body).await?;
709
710        anyhow::ensure!(
711            response.status().is_success(),
712            "error during embedding, status: {:?}, body: {:?}",
713            response.status(),
714            body
715        );
716        let response: OpenAiEmbeddingResponse =
717            serde_json::from_str(&body).context("failed to parse OpenAI embedding response")?;
718        Ok(response)
719    }
720}
721
722// -- Conversions to `language_model_core` types --
723
724impl From<RequestError> for language_model_core::LanguageModelCompletionError {
725    fn from(error: RequestError) -> Self {
726        match error {
727            RequestError::HttpResponseError {
728                provider,
729                status_code,
730                body,
731                headers,
732            } => {
733                let retry_after = headers
734                    .get(http_client::http::header::RETRY_AFTER)
735                    .and_then(|val| val.to_str().ok()?.parse::<u64>().ok())
736                    .map(std::time::Duration::from_secs);
737
738                Self::from_http_status(provider.into(), status_code, body, retry_after)
739            }
740            RequestError::Other(e) => Self::Other(e),
741        }
742    }
743}