pub mod batches;pub mod responses; use anyhow::{Context as _, Result, anyhow};use futures::{AsyncBufReadExt, AsyncReadExt, StreamExt, io::BufReader, stream::BoxStream};use http_client::{ AsyncBody, HttpClient, Method, Request as HttpRequest, StatusCode, http::{HeaderMap, HeaderValue},};use serde::{Deserialize, Serialize};use serde_json::Value;pub use settings::OpenAiReasoningEffort as ReasoningEffort;use std::{convert::TryFrom, future::Future};use strum::EnumIter;use thiserror::Error; pub const OPEN_AI_API_URL: &str = "https://api.openai.com/v1"; fn is_none_or_empty<T: AsRef<[U]>, U>(opt: &Option<T>) -> bool { opt.as_ref().is_none_or(|v| v.as_ref().is_empty())} #[derive(Clone, Copy, Serialize, Deserialize, Debug, Eq, PartialEq)]#[serde(rename_all = "lowercase")]pub enum Role { User, Assistant, System, Tool,} impl TryFrom<String> for Role { type Error = anyhow::Error; fn try_from(value: String) -> Result<Self> { match value.as_str() { "user" => Ok(Self::User), "assistant" => Ok(Self::Assistant), "system" => Ok(Self::System), "tool" => Ok(Self::Tool), _ => anyhow::bail!("invalid role '{value}'"), } }} impl From<Role> for String { fn from(val: Role) -> Self { match val { Role::User => "user".to_owned(), Role::Assistant => "assistant".to_owned(), Role::System => "system".to_owned(), Role::Tool => "tool".to_owned(), } }} #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, EnumIter)]pub enum Model { #[serde(rename = "gpt-3.5-turbo")] ThreePointFiveTurbo, #[serde(rename = "gpt-4")] Four, #[serde(rename = "gpt-4-turbo")] FourTurbo, #[serde(rename = "gpt-4o-mini")] FourOmniMini, #[serde(rename = "gpt-4.1-nano")] FourPointOneNano, #[serde(rename = "o1")] O1, #[serde(rename = "o3-mini")] O3Mini, #[serde(rename = "o3")] O3, #[serde(rename = "gpt-5")] Five, #[serde(rename = "gpt-5-codex")] FiveCodex, #[serde(rename = "gpt-5-mini")] #[default] FiveMini, #[serde(rename = "gpt-5-nano")] FiveNano, #[serde(rename = "gpt-5.1")] FivePointOne, #[serde(rename = "gpt-5.2")] FivePointTwo, #[serde(rename = "gpt-5.2-codex")] FivePointTwoCodex, #[serde(rename = "custom")] Custom { name: String, /// The name displayed in the UI, such as in the assistant panel model dropdown menu. display_name: Option<String>, max_tokens: u64, max_output_tokens: Option<u64>, max_completion_tokens: Option<u64>, reasoning_effort: Option<ReasoningEffort>, #[serde(default = "default_supports_chat_completions")] supports_chat_completions: bool, },} const fn default_supports_chat_completions() -> bool { true} impl Model { pub fn default_fast() -> Self { Self::FiveMini } pub fn from_id(id: &str) -> Result<Self> { match id { "gpt-3.5-turbo" => Ok(Self::ThreePointFiveTurbo), "gpt-4" => Ok(Self::Four), "gpt-4-turbo-preview" => Ok(Self::FourTurbo), "gpt-4o-mini" => Ok(Self::FourOmniMini), "gpt-4.1-nano" => Ok(Self::FourPointOneNano), "o1" => Ok(Self::O1), "o3-mini" => Ok(Self::O3Mini), "o3" => Ok(Self::O3), "gpt-5" => Ok(Self::Five), "gpt-5-codex" => Ok(Self::FiveCodex), "gpt-5-mini" => Ok(Self::FiveMini), "gpt-5-nano" => Ok(Self::FiveNano), "gpt-5.1" => Ok(Self::FivePointOne), "gpt-5.2" => Ok(Self::FivePointTwo), "gpt-5.2-codex" => Ok(Self::FivePointTwoCodex), invalid_id => anyhow::bail!("invalid model id '{invalid_id}'"), } } pub fn id(&self) -> &str { match self { Self::ThreePointFiveTurbo => "gpt-3.5-turbo", Self::Four => "gpt-4", Self::FourTurbo => "gpt-4-turbo", Self::FourOmniMini => "gpt-4o-mini", Self::FourPointOneNano => "gpt-4.1-nano", Self::O1 => "o1", Self::O3Mini => "o3-mini", Self::O3 => "o3", Self::Five => "gpt-5", Self::FiveCodex => "gpt-5-codex", Self::FiveMini => "gpt-5-mini", Self::FiveNano => "gpt-5-nano", Self::FivePointOne => "gpt-5.1", Self::FivePointTwo => "gpt-5.2", Self::FivePointTwoCodex => "gpt-5.2-codex", Self::Custom { name, .. } => name, } } pub fn display_name(&self) -> &str { match self { Self::ThreePointFiveTurbo => "gpt-3.5-turbo", Self::Four => "gpt-4", Self::FourTurbo => "gpt-4-turbo", Self::FourOmniMini => "gpt-4o-mini", Self::FourPointOneNano => "gpt-4.1-nano", Self::O1 => "o1", Self::O3Mini => "o3-mini", Self::O3 => "o3", Self::Five => "gpt-5", Self::FiveCodex => "gpt-5-codex", Self::FiveMini => "gpt-5-mini", Self::FiveNano => "gpt-5-nano", Self::FivePointOne => "gpt-5.1", Self::FivePointTwo => "gpt-5.2", Self::FivePointTwoCodex => "gpt-5.2-codex", Self::Custom { display_name, .. } => display_name.as_deref().unwrap_or(&self.id()), } } pub fn max_token_count(&self) -> u64 { match self { Self::ThreePointFiveTurbo => 16_385, Self::Four => 8_192, Self::FourTurbo => 128_000, Self::FourOmniMini => 128_000, Self::FourPointOneNano => 1_047_576, Self::O1 => 200_000, Self::O3Mini => 200_000, Self::O3 => 200_000, Self::Five => 272_000, Self::FiveCodex => 272_000, Self::FiveMini => 272_000, Self::FiveNano => 272_000, Self::FivePointOne => 400_000, Self::FivePointTwo => 400_000, Self::FivePointTwoCodex => 400_000, Self::Custom { max_tokens, .. } => *max_tokens, } } pub fn max_output_tokens(&self) -> Option<u64> { match self { Self::Custom { max_output_tokens, .. } => *max_output_tokens, Self::ThreePointFiveTurbo => Some(4_096), Self::Four => Some(8_192), Self::FourTurbo => Some(4_096), Self::FourOmniMini => Some(16_384), Self::FourPointOneNano => Some(32_768), Self::O1 => Some(100_000), Self::O3Mini => Some(100_000), Self::O3 => Some(100_000), Self::Five => Some(128_000), Self::FiveCodex => Some(128_000), Self::FiveMini => Some(128_000), Self::FiveNano => Some(128_000), Self::FivePointOne => Some(128_000), Self::FivePointTwo => Some(128_000), Self::FivePointTwoCodex => Some(128_000), } } pub fn reasoning_effort(&self) -> Option<ReasoningEffort> { match self { Self::Custom { reasoning_effort, .. } => reasoning_effort.to_owned(), _ => None, } } pub fn supports_chat_completions(&self) -> bool { match self { Self::Custom { supports_chat_completions, .. } => *supports_chat_completions, Self::FiveCodex | Self::FivePointTwoCodex => false, _ => true, } } /// Returns whether the given model supports the `parallel_tool_calls` parameter. /// /// If the model does not support the parameter, do not pass it up, or the API will return an error. pub fn supports_parallel_tool_calls(&self) -> bool { match self { Self::ThreePointFiveTurbo | Self::Four | Self::FourTurbo | Self::FourOmniMini | Self::FourPointOneNano | Self::Five | Self::FiveCodex | Self::FiveMini | Self::FivePointOne | Self::FivePointTwo | Self::FivePointTwoCodex | Self::FiveNano => true, Self::O1 | Self::O3 | Self::O3Mini | Model::Custom { .. } => false, } } /// Returns whether the given model supports the `prompt_cache_key` parameter. /// /// If the model does not support the parameter, do not pass it up. pub fn supports_prompt_cache_key(&self) -> bool { true }} #[derive(Debug, Serialize, Deserialize)]pub struct Request { pub model: String, pub messages: Vec<RequestMessage>, pub stream: bool, #[serde(default, skip_serializing_if = "Option::is_none")] pub max_completion_tokens: Option<u64>, #[serde(default, skip_serializing_if = "Vec::is_empty")] pub stop: Vec<String>, #[serde(default, skip_serializing_if = "Option::is_none")] pub temperature: Option<f32>, #[serde(default, skip_serializing_if = "Option::is_none")] pub tool_choice: Option<ToolChoice>, /// Whether to enable parallel function calling during tool use. #[serde(default, skip_serializing_if = "Option::is_none")] pub parallel_tool_calls: Option<bool>, #[serde(default, skip_serializing_if = "Vec::is_empty")] pub tools: Vec<ToolDefinition>, #[serde(default, skip_serializing_if = "Option::is_none")] pub prompt_cache_key: Option<String>, #[serde(default, skip_serializing_if = "Option::is_none")] pub reasoning_effort: Option<ReasoningEffort>,} #[derive(Debug, Serialize, Deserialize)]#[serde(rename_all = "lowercase")]pub enum ToolChoice { Auto, Required, None, #[serde(untagged)] Other(ToolDefinition),} #[derive(Clone, Deserialize, Serialize, Debug)]#[serde(tag = "type", rename_all = "snake_case")]pub enum ToolDefinition { #[allow(dead_code)] Function { function: FunctionDefinition },} #[derive(Clone, Debug, Serialize, Deserialize)]pub struct FunctionDefinition { pub name: String, pub description: Option<String>, pub parameters: Option<Value>,} #[derive(Clone, Serialize, Deserialize, Debug, Eq, PartialEq)]#[serde(tag = "role", rename_all = "lowercase")]pub enum RequestMessage { Assistant { content: Option<MessageContent>, #[serde(default, skip_serializing_if = "Vec::is_empty")] tool_calls: Vec<ToolCall>, }, User { content: MessageContent, }, System { content: MessageContent, }, Tool { content: MessageContent, tool_call_id: String, },} #[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)]#[serde(untagged)]pub enum MessageContent { Plain(String), Multipart(Vec<MessagePart>),} impl MessageContent { pub fn empty() -> Self { MessageContent::Multipart(vec![]) } pub fn push_part(&mut self, part: MessagePart) { match self { MessageContent::Plain(text) => { *self = MessageContent::Multipart(vec![MessagePart::Text { text: text.clone() }, part]); } MessageContent::Multipart(parts) if parts.is_empty() => match part { MessagePart::Text { text } => *self = MessageContent::Plain(text), MessagePart::Image { .. } => *self = MessageContent::Multipart(vec![part]), }, MessageContent::Multipart(parts) => parts.push(part), } }} impl From<Vec<MessagePart>> for MessageContent { fn from(mut parts: Vec<MessagePart>) -> Self { if let [MessagePart::Text { text }] = parts.as_mut_slice() { MessageContent::Plain(std::mem::take(text)) } else { MessageContent::Multipart(parts) } }} #[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)]#[serde(tag = "type")]pub enum MessagePart { #[serde(rename = "text")] Text { text: String }, #[serde(rename = "image_url")] Image { image_url: ImageUrl },} #[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)]pub struct ImageUrl { pub url: String, #[serde(skip_serializing_if = "Option::is_none")] pub detail: Option<String>,} #[derive(Clone, Serialize, Deserialize, Debug, Eq, PartialEq)]pub struct ToolCall { pub id: String, #[serde(flatten)] pub content: ToolCallContent,} #[derive(Clone, Serialize, Deserialize, Debug, Eq, PartialEq)]#[serde(tag = "type", rename_all = "lowercase")]pub enum ToolCallContent { Function { function: FunctionContent },} #[derive(Clone, Serialize, Deserialize, Debug, Eq, PartialEq)]pub struct FunctionContent { pub name: String, pub arguments: String,} #[derive(Clone, Serialize, Deserialize, Debug)]pub struct Response { pub id: String, pub object: String, pub created: u64, pub model: String, pub choices: Vec<Choice>, pub usage: Usage,} #[derive(Clone, Serialize, Deserialize, Debug)]pub struct Choice { pub index: u32, pub message: RequestMessage, pub finish_reason: Option<String>,} #[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]pub struct ResponseMessageDelta { pub role: Option<Role>, pub content: Option<String>, #[serde(default, skip_serializing_if = "is_none_or_empty")] pub tool_calls: Option<Vec<ToolCallChunk>>, #[serde(default, skip_serializing_if = "is_none_or_empty")] pub reasoning_content: Option<String>,} #[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]pub struct ToolCallChunk { pub index: usize, pub id: Option<String>, // There is also an optional `type` field that would determine if a // function is there. Sometimes this streams in with the `function` before // it streams in the `type` pub function: Option<FunctionChunk>,} #[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]pub struct FunctionChunk { pub name: Option<String>, pub arguments: Option<String>,} #[derive(Clone, Serialize, Deserialize, Debug)]pub struct Usage { pub prompt_tokens: u64, pub completion_tokens: u64, pub total_tokens: u64,} #[derive(Serialize, Deserialize, Debug)]pub struct ChoiceDelta { pub index: u32, pub delta: Option<ResponseMessageDelta>, pub finish_reason: Option<String>,} #[derive(Error, Debug)]pub enum RequestError { #[error("HTTP response error from {provider}'s API: status {status_code} - {body:?}")] HttpResponseError { provider: String, status_code: StatusCode, body: String, headers: HeaderMap<HeaderValue>, }, #[error(transparent)] Other(#[from] anyhow::Error),} #[derive(Serialize, Deserialize, Debug)]pub struct ResponseStreamError { message: String,} #[derive(Serialize, Deserialize, Debug)]#[serde(untagged)]pub enum ResponseStreamResult { Ok(ResponseStreamEvent), Err { error: ResponseStreamError },} #[derive(Serialize, Deserialize, Debug)]pub struct ResponseStreamEvent { pub choices: Vec<ChoiceDelta>, pub usage: Option<Usage>,} pub async fn non_streaming_completion( client: &dyn HttpClient,Lines 1–500 of 676. Load more
pub mod batches; pub mod responses; use anyhow::{Context as _, Result, anyhow}; use futures::{AsyncBufReadExt, AsyncReadExt, StreamExt, io::BufReader, stream::BoxStream}; use http_client::{ AsyncBody, HttpClient, Method, Request as HttpRequest, StatusCode, http::{HeaderMap, HeaderValue}, }; use serde::{Deserialize, Serialize}; use serde_json::Value; pub use settings::OpenAiReasoningEffort as ReasoningEffort; use std::{convert::TryFrom, future::Future}; use strum::EnumIter; use thiserror::Error; pub const OPEN_AI_API_URL: &str = "https://api.openai.com/v1"; fn is_none_or_empty<T: AsRef<[U]>, U>(opt: &Option<T>) -> bool { opt.as_ref().is_none_or(|v| v.as_ref().is_empty()) } #[derive(Clone, Copy, Serialize, Deserialize, Debug, Eq, PartialEq)] #[serde(rename_all = "lowercase")] pub enum Role { User, Assistant, System, Tool, } impl TryFrom<String> for Role { type Error = anyhow::Error; fn try_from(value: String) -> Result<Self> { match value.as_str() { "user" => Ok(Self::User), "assistant" => Ok(Self::Assistant), "system" => Ok(Self::System), "tool" => Ok(Self::Tool), _ => anyhow::bail!("invalid role '{value}'"), } } } impl From<Role> for String { fn from(val: Role) -> Self { match val { Role::User => "user".to_owned(), Role::Assistant => "assistant".to_owned(), Role::System => "system".to_owned(), Role::Tool => "tool".to_owned(), } } } #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, EnumIter)] pub enum Model { #[serde(rename = "gpt-3.5-turbo")] ThreePointFiveTurbo, #[serde(rename = "gpt-4")] Four, #[serde(rename = "gpt-4-turbo")] FourTurbo, #[serde(rename = "gpt-4o-mini")] FourOmniMini, #[serde(rename = "gpt-4.1-nano")] FourPointOneNano, #[serde(rename = "o1")] O1, #[serde(rename = "o3-mini")] O3Mini, #[serde(rename = "o3")] O3, #[serde(rename = "gpt-5")] Five, #[serde(rename = "gpt-5-codex")] FiveCodex, #[serde(rename = "gpt-5-mini")] #[default] FiveMini, #[serde(rename = "gpt-5-nano")] FiveNano, #[serde(rename = "gpt-5.1")] FivePointOne, #[serde(rename = "gpt-5.2")] FivePointTwo, #[serde(rename = "gpt-5.2-codex")] FivePointTwoCodex, #[serde(rename = "custom")] Custom { name: String, /// The name displayed in the UI, such as in the assistant panel model dropdown menu. display_name: Option<String>, max_tokens: u64, max_output_tokens: Option<u64>, max_completion_tokens: Option<u64>, reasoning_effort: Option<ReasoningEffort>, #[serde(default = "default_supports_chat_completions")] supports_chat_completions: bool, }, } const fn default_supports_chat_completions() -> bool { true } impl Model { pub fn default_fast() -> Self { Self::FiveMini } pub fn from_id(id: &str) -> Result<Self> { match id { "gpt-3.5-turbo" => Ok(Self::ThreePointFiveTurbo), "gpt-4" => Ok(Self::Four), "gpt-4-turbo-preview" => Ok(Self::FourTurbo), "gpt-4o-mini" => Ok(Self::FourOmniMini), "gpt-4.1-nano" => Ok(Self::FourPointOneNano), "o1" => Ok(Self::O1), "o3-mini" => Ok(Self::O3Mini), "o3" => Ok(Self::O3), "gpt-5" => Ok(Self::Five), "gpt-5-codex" => Ok(Self::FiveCodex), "gpt-5-mini" => Ok(Self::FiveMini), "gpt-5-nano" => Ok(Self::FiveNano), "gpt-5.1" => Ok(Self::FivePointOne), "gpt-5.2" => Ok(Self::FivePointTwo), "gpt-5.2-codex" => Ok(Self::FivePointTwoCodex), invalid_id => anyhow::bail!("invalid model id '{invalid_id}'"), } } pub fn id(&self) -> &str { match self { Self::ThreePointFiveTurbo => "gpt-3.5-turbo", Self::Four => "gpt-4", Self::FourTurbo => "gpt-4-turbo", Self::FourOmniMini => "gpt-4o-mini", Self::FourPointOneNano => "gpt-4.1-nano", Self::O1 => "o1", Self::O3Mini => "o3-mini", Self::O3 => "o3", Self::Five => "gpt-5", Self::FiveCodex => "gpt-5-codex", Self::FiveMini => "gpt-5-mini", Self::FiveNano => "gpt-5-nano", Self::FivePointOne => "gpt-5.1", Self::FivePointTwo => "gpt-5.2", Self::FivePointTwoCodex => "gpt-5.2-codex", Self::Custom { name, .. } => name, } } pub fn display_name(&self) -> &str { match self { Self::ThreePointFiveTurbo => "gpt-3.5-turbo", Self::Four => "gpt-4", Self::FourTurbo => "gpt-4-turbo", Self::FourOmniMini => "gpt-4o-mini", Self::FourPointOneNano => "gpt-4.1-nano", Self::O1 => "o1", Self::O3Mini => "o3-mini", Self::O3 => "o3", Self::Five => "gpt-5", Self::FiveCodex => "gpt-5-codex", Self::FiveMini => "gpt-5-mini", Self::FiveNano => "gpt-5-nano", Self::FivePointOne => "gpt-5.1", Self::FivePointTwo => "gpt-5.2", Self::FivePointTwoCodex => "gpt-5.2-codex", Self::Custom { display_name, .. } => display_name.as_deref().unwrap_or(&self.id()), } } pub fn max_token_count(&self) -> u64 { match self { Self::ThreePointFiveTurbo => 16_385, Self::Four => 8_192, Self::FourTurbo => 128_000, Self::FourOmniMini => 128_000, Self::FourPointOneNano => 1_047_576, Self::O1 => 200_000, Self::O3Mini => 200_000, Self::O3 => 200_000, Self::Five => 272_000, Self::FiveCodex => 272_000, Self::FiveMini => 272_000, Self::FiveNano => 272_000, Self::FivePointOne => 400_000, Self::FivePointTwo => 400_000, Self::FivePointTwoCodex => 400_000, Self::Custom { max_tokens, .. } => *max_tokens, } } pub fn max_output_tokens(&self) -> Option<u64> { match self { Self::Custom { max_output_tokens, .. } => *max_output_tokens, Self::ThreePointFiveTurbo => Some(4_096), Self::Four => Some(8_192), Self::FourTurbo => Some(4_096), Self::FourOmniMini => Some(16_384), Self::FourPointOneNano => Some(32_768), Self::O1 => Some(100_000), Self::O3Mini => Some(100_000), Self::O3 => Some(100_000), Self::Five => Some(128_000), Self::FiveCodex => Some(128_000), Self::FiveMini => Some(128_000), Self::FiveNano => Some(128_000), Self::FivePointOne => Some(128_000), Self::FivePointTwo => Some(128_000), Self::FivePointTwoCodex => Some(128_000), } } pub fn reasoning_effort(&self) -> Option<ReasoningEffort> { match self { Self::Custom { reasoning_effort, .. } => reasoning_effort.to_owned(), _ => None, } } pub fn supports_chat_completions(&self) -> bool { match self { Self::Custom { supports_chat_completions, .. } => *supports_chat_completions, Self::FiveCodex | Self::FivePointTwoCodex => false, _ => true, } } /// Returns whether the given model supports the `parallel_tool_calls` parameter. /// /// If the model does not support the parameter, do not pass it up, or the API will return an error. pub fn supports_parallel_tool_calls(&self) -> bool { match self { Self::ThreePointFiveTurbo | Self::Four | Self::FourTurbo | Self::FourOmniMini | Self::FourPointOneNano | Self::Five | Self::FiveCodex | Self::FiveMini | Self::FivePointOne | Self::FivePointTwo | Self::FivePointTwoCodex | Self::FiveNano => true, Self::O1 | Self::O3 | Self::O3Mini | Model::Custom { .. } => false, } } /// Returns whether the given model supports the `prompt_cache_key` parameter. /// /// If the model does not support the parameter, do not pass it up. pub fn supports_prompt_cache_key(&self) -> bool { true } } #[derive(Debug, Serialize, Deserialize)] pub struct Request { pub model: String, pub messages: Vec<RequestMessage>, pub stream: bool, #[serde(default, skip_serializing_if = "Option::is_none")] pub max_completion_tokens: Option<u64>, #[serde(default, skip_serializing_if = "Vec::is_empty")] pub stop: Vec<String>, #[serde(default, skip_serializing_if = "Option::is_none")] pub temperature: Option<f32>, #[serde(default, skip_serializing_if = "Option::is_none")] pub tool_choice: Option<ToolChoice>, /// Whether to enable parallel function calling during tool use. #[serde(default, skip_serializing_if = "Option::is_none")] pub parallel_tool_calls: Option<bool>, #[serde(default, skip_serializing_if = "Vec::is_empty")] pub tools: Vec<ToolDefinition>, #[serde(default, skip_serializing_if = "Option::is_none")] pub prompt_cache_key: Option<String>, #[serde(default, skip_serializing_if = "Option::is_none")] pub reasoning_effort: Option<ReasoningEffort>, } #[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "lowercase")] pub enum ToolChoice { Auto, Required, None, #[serde(untagged)] Other(ToolDefinition), } #[derive(Clone, Deserialize, Serialize, Debug)] #[serde(tag = "type", rename_all = "snake_case")] pub enum ToolDefinition { #[allow(dead_code)] Function { function: FunctionDefinition }, } #[derive(Clone, Debug, Serialize, Deserialize)] pub struct FunctionDefinition { pub name: String, pub description: Option<String>, pub parameters: Option<Value>, } #[derive(Clone, Serialize, Deserialize, Debug, Eq, PartialEq)] #[serde(tag = "role", rename_all = "lowercase")] pub enum RequestMessage { Assistant { content: Option<MessageContent>, #[serde(default, skip_serializing_if = "Vec::is_empty")] tool_calls: Vec<ToolCall>, }, User { content: MessageContent, }, System { content: MessageContent, }, Tool { content: MessageContent, tool_call_id: String, }, } #[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)] #[serde(untagged)] pub enum MessageContent { Plain(String), Multipart(Vec<MessagePart>), } impl MessageContent { pub fn empty() -> Self { MessageContent::Multipart(vec![]) } pub fn push_part(&mut self, part: MessagePart) { match self { MessageContent::Plain(text) => { *self = MessageContent::Multipart(vec![MessagePart::Text { text: text.clone() }, part]); } MessageContent::Multipart(parts) if parts.is_empty() => match part { MessagePart::Text { text } => *self = MessageContent::Plain(text), MessagePart::Image { .. } => *self = MessageContent::Multipart(vec![part]), }, MessageContent::Multipart(parts) => parts.push(part), } } } impl From<Vec<MessagePart>> for MessageContent { fn from(mut parts: Vec<MessagePart>) -> Self { if let [MessagePart::Text { text }] = parts.as_mut_slice() { MessageContent::Plain(std::mem::take(text)) } else { MessageContent::Multipart(parts) } } } #[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)] #[serde(tag = "type")] pub enum MessagePart { #[serde(rename = "text")] Text { text: String }, #[serde(rename = "image_url")] Image { image_url: ImageUrl }, } #[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)] pub struct ImageUrl { pub url: String, #[serde(skip_serializing_if = "Option::is_none")] pub detail: Option<String>, } #[derive(Clone, Serialize, Deserialize, Debug, Eq, PartialEq)] pub struct ToolCall { pub id: String, #[serde(flatten)] pub content: ToolCallContent, } #[derive(Clone, Serialize, Deserialize, Debug, Eq, PartialEq)] #[serde(tag = "type", rename_all = "lowercase")] pub enum ToolCallContent { Function { function: FunctionContent }, } #[derive(Clone, Serialize, Deserialize, Debug, Eq, PartialEq)] pub struct FunctionContent { pub name: String, pub arguments: String, } #[derive(Clone, Serialize, Deserialize, Debug)] pub struct Response { pub id: String, pub object: String, pub created: u64, pub model: String, pub choices: Vec<Choice>, pub usage: Usage, } #[derive(Clone, Serialize, Deserialize, Debug)] pub struct Choice { pub index: u32, pub message: RequestMessage, pub finish_reason: Option<String>, } #[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] pub struct ResponseMessageDelta { pub role: Option<Role>, pub content: Option<String>, #[serde(default, skip_serializing_if = "is_none_or_empty")] pub tool_calls: Option<Vec<ToolCallChunk>>, #[serde(default, skip_serializing_if = "is_none_or_empty")] pub reasoning_content: Option<String>, } #[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] pub struct ToolCallChunk { pub index: usize, pub id: Option<String>, // There is also an optional `type` field that would determine if a // function is there. Sometimes this streams in with the `function` before // it streams in the `type` pub function: Option<FunctionChunk>, } #[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] pub struct FunctionChunk { pub name: Option<String>, pub arguments: Option<String>, } #[derive(Clone, Serialize, Deserialize, Debug)] pub struct Usage { pub prompt_tokens: u64, pub completion_tokens: u64, pub total_tokens: u64, } #[derive(Serialize, Deserialize, Debug)] pub struct ChoiceDelta { pub index: u32, pub delta: Option<ResponseMessageDelta>, pub finish_reason: Option<String>, } #[derive(Error, Debug)] pub enum RequestError { #[error("HTTP response error from {provider}'s API: status {status_code} - {body:?}")] HttpResponseError { provider: String, status_code: StatusCode, body: String, headers: HeaderMap<HeaderValue>, }, #[error(transparent)] Other(#[from] anyhow::Error), } #[derive(Serialize, Deserialize, Debug)] pub struct ResponseStreamError { message: String, } #[derive(Serialize, Deserialize, Debug)] #[serde(untagged)] pub enum ResponseStreamResult { Ok(ResponseStreamEvent), Err { error: ResponseStreamError }, } #[derive(Serialize, Deserialize, Debug)] pub struct ResponseStreamEvent { pub choices: Vec<ChoiceDelta>, pub usage: Option<Usage>, } pub async fn non_streaming_completion( client: &dyn HttpClient,