use std::fmt;
use base64::Engine;
use serde::{Deserialize, Serialize};
use serde_json::Value;
pub use crate::audio::AudioFormat;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
#[non_exhaustive]
pub enum Role {
System,
#[default]
User,
Assistant,
Tool,
Developer,
}
impl Role {
#[inline]
#[must_use]
pub const fn as_str(&self) -> &'static str {
match self {
Self::System => "system",
Self::User => "user",
Self::Assistant => "assistant",
Self::Tool => "tool",
Self::Developer => "developer",
}
}
#[inline]
#[must_use]
pub const fn is_system(&self) -> bool {
matches!(self, Self::System)
}
#[inline]
#[must_use]
pub const fn is_user(&self) -> bool {
matches!(self, Self::User)
}
#[inline]
#[must_use]
pub const fn is_assistant(&self) -> bool {
matches!(self, Self::Assistant)
}
#[inline]
#[must_use]
pub const fn is_tool(&self) -> bool {
matches!(self, Self::Tool)
}
}
impl fmt::Display for Role {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
#[non_exhaustive]
pub enum ImageMime {
#[default]
Jpeg,
Png,
Gif,
WebP,
}
impl ImageMime {
#[inline]
#[must_use]
pub const fn as_str(&self) -> &'static str {
match self {
Self::Jpeg => "image/jpeg",
Self::Png => "image/png",
Self::Gif => "image/gif",
Self::WebP => "image/webp",
}
}
#[must_use]
pub fn from_extension(ext: &str) -> Option<Self> {
match ext.to_ascii_lowercase().as_str() {
"jpg" | "jpeg" => Some(Self::Jpeg),
"png" => Some(Self::Png),
"gif" => Some(Self::Gif),
"webp" => Some(Self::WebP),
_ => None,
}
}
#[must_use]
pub fn from_bytes(data: &[u8]) -> Option<Self> {
if data.len() < 4 {
return None;
}
match &data[..4] {
[0xFF, 0xD8, 0xFF, ..] => Some(Self::Jpeg),
[0x89, 0x50, 0x4E, 0x47] => Some(Self::Png),
[0x47, 0x49, 0x46, 0x38] => Some(Self::Gif),
[0x52, 0x49, 0x46, 0x46] if data.len() >= 12 && &data[8..12] == b"WEBP" => {
Some(Self::WebP)
}
_ => None,
}
}
}
impl fmt::Display for ImageMime {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ImageDetail {
Low,
High,
#[default]
Auto,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct InputAudio {
pub data: String,
pub format: AudioFormat,
}
impl InputAudio {
#[must_use]
pub fn new(data: impl Into<String>, format: AudioFormat) -> Self {
Self {
data: data.into(),
format,
}
}
#[must_use]
pub fn from_bytes(data: &[u8], format: AudioFormat) -> Self {
let encoded = base64::engine::general_purpose::STANDARD.encode(data);
Self::new(encoded, format)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ContentPart {
Text {
text: String,
},
ImageUrl {
image_url: ImageUrl,
},
InputAudio {
input_audio: InputAudio,
},
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ImageUrl {
pub url: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub detail: Option<ImageDetail>,
}
impl ContentPart {
#[inline]
#[must_use]
pub fn text(text: impl Into<String>) -> Self {
Self::Text { text: text.into() }
}
#[inline]
#[must_use]
pub fn image_url(url: impl Into<String>) -> Self {
Self::ImageUrl {
image_url: ImageUrl {
url: url.into(),
detail: None,
},
}
}
#[inline]
#[must_use]
pub fn image_url_with_detail(url: impl Into<String>, detail: ImageDetail) -> Self {
Self::ImageUrl {
image_url: ImageUrl {
url: url.into(),
detail: Some(detail),
},
}
}
#[must_use]
pub fn image_bytes(data: &[u8], mime: ImageMime) -> Self {
let encoded = base64::engine::general_purpose::STANDARD.encode(data);
let data_url = format!("data:{};base64,{}", mime.as_str(), encoded);
Self::image_url(data_url)
}
#[must_use]
pub fn image_bytes_auto(data: &[u8]) -> Self {
let mime = ImageMime::from_bytes(data).unwrap_or(ImageMime::Jpeg);
Self::image_bytes(data, mime)
}
#[inline]
#[must_use]
pub fn input_audio(data: impl Into<String>, format: AudioFormat) -> Self {
Self::InputAudio {
input_audio: InputAudio::new(data, format),
}
}
#[must_use]
pub fn input_audio_bytes(data: &[u8], format: AudioFormat) -> Self {
Self::InputAudio {
input_audio: InputAudio::from_bytes(data, format),
}
}
#[must_use]
pub fn as_text(&self) -> Option<&str> {
match self {
Self::Text { text } => Some(text),
_ => None,
}
}
#[inline]
#[must_use]
pub const fn is_text(&self) -> bool {
matches!(self, Self::Text { .. })
}
#[inline]
#[must_use]
pub const fn is_image(&self) -> bool {
matches!(self, Self::ImageUrl { .. })
}
#[inline]
#[must_use]
pub const fn is_audio(&self) -> bool {
matches!(self, Self::InputAudio { .. })
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum Content {
Text(String),
Parts(Vec<ContentPart>),
}
impl Content {
#[inline]
#[must_use]
pub fn text(text: impl Into<String>) -> Self {
Self::Text(text.into())
}
#[inline]
#[must_use]
pub const fn parts(parts: Vec<ContentPart>) -> Self {
Self::Parts(parts)
}
#[must_use]
pub fn as_text(&self) -> Option<String> {
match self {
Self::Text(text) => Some(text.clone()),
Self::Parts(parts) => {
let texts: Vec<&str> = parts.iter().filter_map(ContentPart::as_text).collect();
if texts.is_empty() {
None
} else {
Some(texts.join("\n"))
}
}
}
}
#[must_use]
pub fn has_images(&self) -> bool {
match self {
Self::Text(_) => false,
Self::Parts(parts) => parts.iter().any(ContentPart::is_image),
}
}
#[must_use]
pub const fn is_empty(&self) -> bool {
match self {
Self::Text(text) => text.is_empty(),
Self::Parts(parts) => parts.is_empty(),
}
}
}
impl Default for Content {
fn default() -> Self {
Self::Text(String::new())
}
}
impl From<String> for Content {
fn from(text: String) -> Self {
Self::Text(text)
}
}
impl From<&str> for Content {
fn from(text: &str) -> Self {
Self::Text(text.to_owned())
}
}
impl From<Vec<ContentPart>> for Content {
fn from(parts: Vec<ContentPart>) -> Self {
Self::Parts(parts)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct FunctionCall {
pub name: String,
pub arguments: String,
}
impl FunctionCall {
#[inline]
#[must_use]
pub fn new(name: impl Into<String>, arguments: impl Into<String>) -> Self {
Self {
name: name.into(),
arguments: arguments.into(),
}
}
pub fn parse_arguments<T: for<'de> Deserialize<'de>>(&self) -> Result<T, serde_json::Error> {
serde_json::from_str(&self.arguments)
}
#[must_use]
pub fn arguments_value(&self) -> Value {
serde_json::from_str(&self.arguments).unwrap_or(Value::Null)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ToolCall {
pub id: String,
#[serde(rename = "type", default = "default_tool_type")]
pub call_type: String,
pub function: FunctionCall,
}
fn default_tool_type() -> String {
"function".to_owned()
}
impl ToolCall {
#[must_use]
pub fn function(
id: impl Into<String>,
name: impl Into<String>,
arguments: impl Into<String>,
) -> Self {
Self {
id: id.into(),
call_type: "function".to_owned(),
function: FunctionCall::new(name, arguments),
}
}
#[inline]
#[must_use]
pub fn name(&self) -> &str {
&self.function.name
}
#[inline]
#[must_use]
pub fn arguments(&self) -> &str {
&self.function.arguments
}
pub fn parse_arguments<T: for<'de> Deserialize<'de>>(&self) -> Result<T, serde_json::Error> {
self.function.parse_arguments()
}
}
impl fmt::Display for ToolCall {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}({})", self.function.name, self.function.arguments)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ThinkingBlock {
Thinking {
thinking: String,
#[serde(skip_serializing_if = "Option::is_none")]
signature: Option<String>,
},
RedactedThinking {
data: String,
},
}
impl ThinkingBlock {
#[must_use]
pub fn thinking(content: impl Into<String>) -> Self {
Self::Thinking {
thinking: content.into(),
signature: None,
}
}
#[must_use]
pub fn as_thinking(&self) -> Option<&str> {
match self {
Self::Thinking { thinking, .. } => Some(thinking),
Self::RedactedThinking { .. } => None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum Annotation {
UrlCitation {
start_index: usize,
end_index: usize,
url: String,
#[serde(skip_serializing_if = "Option::is_none")]
title: Option<String>,
},
FileCitation {
file_id: String,
#[serde(skip_serializing_if = "Option::is_none")]
quote: Option<String>,
},
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Message {
pub role: Role,
#[serde(skip_serializing_if = "Option::is_none")]
pub content: Option<Content>,
#[serde(skip_serializing_if = "Option::is_none")]
pub refusal: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub annotations: Vec<Annotation>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_calls: Option<Vec<ToolCall>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_call_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub reasoning_content: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub thinking_blocks: Option<Vec<ThinkingBlock>>,
}
impl Message {
#[must_use]
pub fn new(role: Role, content: impl Into<Content>) -> Self {
Self {
role,
content: Some(content.into()),
refusal: None,
annotations: Vec::new(),
tool_calls: None,
tool_call_id: None,
name: None,
reasoning_content: None,
thinking_blocks: None,
}
}
#[inline]
#[must_use]
pub fn system(content: impl Into<String>) -> Self {
Self::new(Role::System, Content::text(content))
}
#[inline]
#[must_use]
pub fn user(content: impl Into<String>) -> Self {
Self::new(Role::User, Content::text(content))
}
#[inline]
#[must_use]
pub fn assistant(content: impl Into<String>) -> Self {
Self::new(Role::Assistant, Content::text(content))
}
#[must_use]
pub const fn assistant_tool_calls(tool_calls: Vec<ToolCall>) -> Self {
Self {
role: Role::Assistant,
content: None,
refusal: None,
annotations: Vec::new(),
tool_calls: Some(tool_calls),
tool_call_id: None,
name: None,
reasoning_content: None,
thinking_blocks: None,
}
}
#[must_use]
pub fn tool(tool_call_id: impl Into<String>, content: impl Into<String>) -> Self {
Self {
role: Role::Tool,
content: Some(Content::text(content)),
refusal: None,
annotations: Vec::new(),
tool_calls: None,
tool_call_id: Some(tool_call_id.into()),
name: None,
reasoning_content: None,
thinking_blocks: None,
}
}
#[inline]
#[must_use]
pub const fn builder(role: Role) -> MessageBuilder {
MessageBuilder::new(role)
}
#[must_use]
pub fn text(&self) -> Option<String> {
self.content.as_ref().and_then(Content::as_text)
}
#[must_use]
pub fn has_tool_calls(&self) -> bool {
self.tool_calls.as_ref().is_some_and(|tc| !tc.is_empty())
}
#[must_use]
pub fn has_images(&self) -> bool {
self.content.as_ref().is_some_and(Content::has_images)
}
#[must_use]
pub fn is_empty(&self) -> bool {
let no_content = self.content.as_ref().is_none_or(Content::is_empty);
let no_tools = !self.has_tool_calls();
no_content && no_tools
}
#[must_use]
pub fn with_name(mut self, name: impl Into<String>) -> Self {
self.name = Some(name.into());
self
}
}
impl Default for Message {
fn default() -> Self {
Self {
role: Role::User,
content: None,
refusal: None,
annotations: Vec::new(),
tool_calls: None,
tool_call_id: None,
name: None,
reasoning_content: None,
thinking_blocks: None,
}
}
}
impl fmt::Display for Message {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "[{}] ", self.role)?;
if let Some(text) = self.text() {
write!(f, "{text}")?;
}
if let Some(tool_calls) = &self.tool_calls {
for tc in tool_calls {
write!(f, " [{tc}]")?;
}
}
Ok(())
}
}
#[derive(Debug, Clone)]
pub struct MessageBuilder {
role: Role,
parts: Vec<ContentPart>,
tool_calls: Vec<ToolCall>,
tool_call_id: Option<String>,
name: Option<String>,
}
impl MessageBuilder {
#[inline]
#[must_use]
pub const fn new(role: Role) -> Self {
Self {
role,
parts: Vec::new(),
tool_calls: Vec::new(),
tool_call_id: None,
name: None,
}
}
#[must_use]
pub fn text(mut self, text: impl Into<String>) -> Self {
self.parts.push(ContentPart::text(text));
self
}
#[must_use]
pub fn image_url(mut self, url: impl Into<String>) -> Self {
self.parts.push(ContentPart::image_url(url));
self
}
#[must_use]
pub fn image_url_with_detail(mut self, url: impl Into<String>, detail: ImageDetail) -> Self {
self.parts
.push(ContentPart::image_url_with_detail(url, detail));
self
}
#[must_use]
pub fn image_bytes(mut self, data: &[u8], mime: ImageMime) -> Self {
self.parts.push(ContentPart::image_bytes(data, mime));
self
}
#[must_use]
pub fn tool_call(
mut self,
id: impl Into<String>,
name: impl Into<String>,
arguments: impl Into<String>,
) -> Self {
self.tool_calls
.push(ToolCall::function(id, name, arguments));
self
}
#[must_use]
pub fn tool_call_id(mut self, id: impl Into<String>) -> Self {
self.tool_call_id = Some(id.into());
self
}
#[must_use]
pub fn name(mut self, name: impl Into<String>) -> Self {
self.name = Some(name.into());
self
}
#[must_use]
pub fn build(self) -> Message {
let content = if self.parts.is_empty() {
None
} else if self.parts.len() == 1 && self.parts[0].is_text() {
self.parts.into_iter().next().and_then(|p| match p {
ContentPart::Text { text } => Some(Content::Text(text)),
ContentPart::ImageUrl { .. } | ContentPart::InputAudio { .. } => None,
})
} else {
Some(Content::Parts(self.parts))
};
let tool_calls = if self.tool_calls.is_empty() {
None
} else {
Some(self.tool_calls)
};
Message {
role: self.role,
content,
refusal: None,
annotations: Vec::new(),
tool_calls,
tool_call_id: self.tool_call_id,
name: self.name,
reasoning_content: None,
thinking_blocks: None,
}
}
}