use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum Role {
System,
Developer,
User,
Assistant,
Tool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum MessageContent {
Text(String),
Parts(Vec<ContentPart>),
}
impl MessageContent {
pub fn text(text: impl Into<String>) -> Self {
MessageContent::Text(text.into())
}
pub fn parts(parts: Vec<ContentPart>) -> Self {
MessageContent::Parts(parts)
}
}
impl From<&str> for MessageContent {
fn from(s: &str) -> Self {
MessageContent::Text(s.to_string())
}
}
impl From<String> for MessageContent {
fn from(s: String) -> Self {
MessageContent::Text(s)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ContentPart {
#[serde(rename = "type")]
pub content_type: ContentType,
#[serde(skip_serializing_if = "Option::is_none")]
pub text: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub image_url: Option<ImageUrl>,
#[serde(skip_serializing_if = "Option::is_none")]
pub input_audio: Option<InputAudio>,
#[serde(skip_serializing_if = "Option::is_none")]
pub video_url: Option<VideoUrl>,
}
impl ContentPart {
pub fn text(text: impl Into<String>) -> Self {
Self {
content_type: ContentType::Text,
text: Some(text.into()),
image_url: None,
input_audio: None,
video_url: None,
}
}
pub fn image_url(url: impl Into<String>) -> Self {
Self {
content_type: ContentType::ImageUrl,
text: None,
image_url: Some(ImageUrl {
url: url.into(),
detail: None,
}),
input_audio: None,
video_url: None,
}
}
pub fn image_base64(media_type: &str, data: impl Into<String>) -> Self {
let url = format!("data:{};base64,{}", media_type, data.into());
Self {
content_type: ContentType::ImageUrl,
text: None,
image_url: Some(ImageUrl { url, detail: None }),
input_audio: None,
video_url: None,
}
}
pub fn audio_base64(data: impl Into<String>) -> Self {
Self {
content_type: ContentType::InputAudio,
text: None,
image_url: None,
input_audio: Some(InputAudio {
data: data.into(),
format: AudioInputFormat::Wav,
}),
video_url: None,
}
}
pub fn video_url(url: impl Into<String>) -> Self {
Self {
content_type: ContentType::VideoUrl,
text: None,
image_url: None,
input_audio: None,
video_url: Some(VideoUrl { url: url.into() }),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum ContentType {
Text,
ImageUrl,
InputAudio,
VideoUrl,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ImageUrl {
pub url: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub detail: Option<ImageDetail>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum ImageDetail {
Auto,
Low,
High,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum AudioInputFormat {
Wav,
Mp3,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InputAudio {
pub data: String,
pub format: AudioInputFormat,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VideoUrl {
pub url: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Message {
pub role: Role,
pub content: MessageContent,
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[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 reasoning_content: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub audio: Option<MessageAudio>,
}
impl Message {
pub fn new(role: Role, content: MessageContent) -> Self {
Self {
role,
content,
name: None,
tool_calls: None,
tool_call_id: None,
reasoning_content: None,
audio: None,
}
}
pub fn system(content: impl Into<MessageContent>) -> Self {
Self::new(Role::System, content.into())
}
pub fn developer(content: impl Into<MessageContent>) -> Self {
Self::new(Role::Developer, content.into())
}
pub fn user(content: impl Into<MessageContent>) -> Self {
Self::new(Role::User, content.into())
}
pub fn assistant(content: impl Into<MessageContent>) -> Self {
Self::new(Role::Assistant, content.into())
}
pub fn tool(tool_call_id: impl Into<String>, content: impl Into<String>) -> Self {
Self {
role: Role::Tool,
content: MessageContent::Text(content.into()),
name: None,
tool_calls: None,
tool_call_id: Some(tool_call_id.into()),
reasoning_content: None,
audio: None,
}
}
pub fn with_name(mut self, name: impl Into<String>) -> Self {
self.name = Some(name.into());
self
}
pub fn with_tool_calls(mut self, tool_calls: Vec<ToolCall>) -> Self {
self.tool_calls = Some(tool_calls);
self
}
pub fn with_reasoning_content(mut self, content: impl Into<String>) -> Self {
self.reasoning_content = Some(content.into());
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MessageAudio {
pub id: Option<String>,
pub data: Option<String>,
pub expires_at: Option<i64>,
pub transcript: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolCall {
pub id: String,
#[serde(rename = "type")]
pub tool_type: ToolCallType,
pub function: FunctionCall,
}
impl ToolCall {
pub fn new(id: impl Into<String>, function: FunctionCall) -> Self {
Self {
id: id.into(),
tool_type: ToolCallType::Function,
function,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum ToolCallType {
Function,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FunctionCall {
pub name: String,
pub arguments: String,
}
impl FunctionCall {
pub fn new(name: impl Into<String>, arguments: impl Into<String>) -> Self {
Self {
name: name.into(),
arguments: arguments.into(),
}
}
pub fn parse_arguments<T: serde::de::DeserializeOwned>(&self) -> serde_json::Result<T> {
serde_json::from_str(&self.arguments)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_message_creation() {
let msg = Message::user(MessageContent::Text("Hello".to_string()));
assert_eq!(msg.role, Role::User);
}
#[test]
fn test_message_serialization() {
let msg = Message::user(MessageContent::Text("Hello".to_string()));
let json = serde_json::to_string(&msg).unwrap();
assert!(json.contains("\"role\":\"user\""));
assert!(json.contains("\"content\":\"Hello\""));
}
#[test]
fn test_content_part_text() {
let part = ContentPart::text("Hello");
assert_eq!(part.content_type, ContentType::Text);
assert_eq!(part.text, Some("Hello".to_string()));
}
#[test]
fn test_content_part_image_url() {
let part = ContentPart::image_url("https://example.com/image.png");
assert_eq!(part.content_type, ContentType::ImageUrl);
assert!(part.image_url.is_some());
}
#[test]
fn test_content_part_video_url() {
let part = ContentPart::video_url("https://example.com/video.mp4");
assert_eq!(part.content_type, ContentType::VideoUrl);
assert!(part.video_url.is_some());
}
#[test]
fn test_function_call_parse() {
let fc = FunctionCall::new("test", r#"{"arg": "value"}"#);
let parsed: serde_json::Value = fc.parse_arguments().unwrap();
assert_eq!(parsed["arg"], "value");
}
#[test]
fn test_multimodal_message() {
let content = MessageContent::Parts(vec![
ContentPart::text("What's in this image?"),
ContentPart::image_url("https://example.com/image.png"),
]);
let msg = Message::user(content);
assert_eq!(msg.role, Role::User);
}
#[test]
fn test_tool_message() {
let msg = Message::tool("call_123", "result data");
assert_eq!(msg.role, Role::Tool);
assert_eq!(msg.tool_call_id, Some("call_123".to_string()));
}
}