use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "role")]
pub enum Message {
#[serde(rename = "system")]
System(SystemMessage),
#[serde(rename = "user")]
User(UserMessage),
#[serde(rename = "assistant")]
Assistant(AssistantMessage),
#[serde(rename = "tool")]
Tool(ToolMessage),
#[serde(rename = "developer")]
Developer(DeveloperMessage),
#[serde(rename = "function")]
Function(FunctionMessage),
}
#[cfg_attr(alef, alef(skip))]
impl Default for Message {
fn default() -> Self {
Self::Assistant(AssistantMessage::default())
}
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct SystemMessage {
pub content: UserContent,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct UserMessage {
pub content: UserContent,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum UserContent {
Text(String),
Parts(Vec<ContentPart>),
}
#[cfg_attr(alef, alef(skip))]
impl Default for UserContent {
fn default() -> Self {
Self::Text(String::new())
}
}
impl UserContent {
pub fn as_text(&self) -> Option<String> {
match self {
UserContent::Text(s) => Some(s.clone()),
UserContent::Parts(parts) => {
let texts: Vec<&str> = parts
.iter()
.filter_map(|p| match p {
ContentPart::Text { text } => Some(text.as_str()),
_ => None,
})
.collect();
if texts.is_empty() { None } else { Some(texts.join("")) }
}
}
}
}
impl From<String> for UserContent {
fn from(s: String) -> Self {
Self::Text(s)
}
}
impl From<&str> for UserContent {
fn from(s: &str) -> Self {
Self::Text(s.to_owned())
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum ContentPart {
#[serde(rename = "text")]
Text { text: String },
#[serde(rename = "image_url")]
ImageUrl { image_url: ImageUrl },
#[serde(rename = "document")]
Document { document: DocumentContent },
#[serde(rename = "input_audio")]
InputAudio { input_audio: AudioContent },
}
#[cfg_attr(alef, alef(skip))]
impl Default for ContentPart {
fn default() -> Self {
Self::Text { text: String::new() }
}
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ImageUrl {
pub url: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub detail: Option<ImageDetail>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ImageDetail {
Low,
High,
Auto,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct DocumentContent {
pub data: String,
pub media_type: String,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct AudioContent {
pub data: String,
pub format: String,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum AssistantContent {
Text(String),
Parts(Vec<AssistantPart>),
}
#[cfg_attr(alef, alef(skip))]
impl Default for AssistantContent {
fn default() -> Self {
Self::Text(String::new())
}
}
impl AssistantContent {
pub fn as_text(&self) -> Option<String> {
match self {
AssistantContent::Text(s) => Some(s.clone()),
AssistantContent::Parts(parts) => {
let texts: Vec<&str> = parts
.iter()
.filter_map(|p| match p {
AssistantPart::Text { text } => Some(text.as_str()),
_ => None,
})
.collect();
if texts.is_empty() { None } else { Some(texts.join("")) }
}
}
}
}
impl std::fmt::Display for AssistantContent {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_text().as_deref().unwrap_or(""))
}
}
impl From<String> for AssistantContent {
fn from(s: String) -> Self {
Self::Text(s)
}
}
impl From<&str> for AssistantContent {
fn from(s: &str) -> Self {
Self::Text(s.to_owned())
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum AssistantPart {
Text {
text: String,
},
Refusal {
refusal: String,
},
OutputImage {
image_url: ImageUrl,
},
OutputAudio {
audio: AudioContent,
},
}
#[cfg_attr(alef, alef(skip))]
impl Default for AssistantPart {
fn default() -> Self {
Self::Text { text: String::new() }
}
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct AssistantMessage {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub content: Option<AssistantContent>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tool_calls: Option<Vec<ToolCall>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub refusal: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub function_call: Option<FunctionCall>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub reasoning_content: Option<String>,
}
impl AssistantMessage {
pub fn text(&self) -> Option<String> {
match self.content.as_ref()? {
AssistantContent::Text(s) => Some(s.clone()),
AssistantContent::Parts(parts) => {
let texts: Vec<&str> = parts
.iter()
.filter_map(|p| match p {
AssistantPart::Text { text } => Some(text.as_str()),
_ => None,
})
.collect();
if texts.is_empty() { None } else { Some(texts.join("")) }
}
}
}
pub fn refusal_text(&self) -> Option<&str> {
if let Some(r) = self.refusal.as_deref() {
return Some(r);
}
if let Some(AssistantContent::Parts(parts)) = self.content.as_ref() {
for part in parts {
if let AssistantPart::Refusal { refusal } = part {
return Some(refusal.as_str());
}
}
}
None
}
pub fn reasoning_text(&self) -> Option<&str> {
self.reasoning_content.as_deref()
}
pub fn output_images(&self) -> Vec<ImageUrl> {
let Some(AssistantContent::Parts(parts)) = self.content.as_ref() else {
return vec![];
};
parts
.iter()
.filter_map(|p| match p {
AssistantPart::OutputImage { image_url } => Some(image_url.clone()),
_ => None,
})
.collect()
}
pub fn output_audio(&self) -> Vec<AudioContent> {
let Some(AssistantContent::Parts(parts)) = self.content.as_ref() else {
return vec![];
};
parts
.iter()
.filter_map(|p| match p {
AssistantPart::OutputAudio { audio } => Some(audio.clone()),
_ => None,
})
.collect()
}
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct ToolMessage {
pub content: String,
pub tool_call_id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct DeveloperMessage {
pub content: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct FunctionMessage {
pub content: String,
pub name: String,
}
impl Message {
pub fn user_with_parts(parts: Vec<ContentPart>) -> Self {
Self::User(UserMessage {
content: UserContent::Parts(parts),
name: None,
})
}
pub fn system_with_parts(parts: Vec<ContentPart>) -> Self {
Self::System(SystemMessage {
content: UserContent::Parts(parts),
name: None,
})
}
pub fn assistant_with_parts(parts: Vec<AssistantPart>) -> Self {
Self::Assistant(AssistantMessage {
content: Some(AssistantContent::Parts(parts)),
name: None,
tool_calls: None,
refusal: None,
function_call: None,
reasoning_content: None,
})
}
}
impl ContentPart {
pub fn text(s: impl Into<String>) -> Self {
Self::Text { text: s.into() }
}
pub fn image_data_url(url: impl Into<String>) -> Self {
Self::ImageUrl {
image_url: ImageUrl {
url: url.into(),
detail: None,
},
}
}
pub fn image_url(url: impl Into<String>) -> Self {
Self::ImageUrl {
image_url: ImageUrl {
url: url.into(),
detail: None,
},
}
}
pub fn image_with_detail(url: impl Into<String>, detail: ImageDetail) -> Self {
Self::ImageUrl {
image_url: ImageUrl {
url: url.into(),
detail: Some(detail),
},
}
}
pub fn image_png(bytes: &[u8]) -> Self {
Self::image_data_url(crate::image::encode_data_url(bytes, Some(crate::image::IMAGE_PNG)))
}
pub fn image_jpeg(bytes: &[u8]) -> Self {
Self::image_data_url(crate::image::encode_data_url(bytes, Some(crate::image::IMAGE_JPEG)))
}
pub fn image_webp(bytes: &[u8]) -> Self {
Self::image_data_url(crate::image::encode_data_url(bytes, Some(crate::image::IMAGE_WEBP)))
}
pub fn image_tiff(bytes: &[u8]) -> Self {
Self::image_data_url(crate::image::encode_data_url(bytes, Some(crate::image::IMAGE_TIFF)))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn content_part_text_constructor() {
let part = ContentPart::text("hi");
let json = serde_json::to_string(&part).expect("serialization should not fail");
assert_eq!(json, r#"{"type":"text","text":"hi"}"#);
}
#[test]
fn content_part_image_data_url_constructor() {
let part = ContentPart::image_data_url("data:image/png;base64,aGk=");
let json = serde_json::to_string(&part).expect("serialization should not fail");
assert_eq!(
json,
r#"{"type":"image_url","image_url":{"url":"data:image/png;base64,aGk="}}"#
);
}
#[test]
fn content_part_image_with_detail() {
let part = ContentPart::image_with_detail("https://example.com/img.png", ImageDetail::High);
let json = serde_json::to_string(&part).expect("serialization should not fail");
assert_eq!(
json,
r#"{"type":"image_url","image_url":{"url":"https://example.com/img.png","detail":"high"}}"#
);
}
#[test]
fn content_part_image_png_round_trip() {
let part = ContentPart::image_png(b"hi");
match &part {
ContentPart::ImageUrl { image_url } => {
assert!(
image_url.url.starts_with("data:image/png;base64,"),
"expected png data URL, got: {}",
image_url.url
);
}
other => panic!("expected ImageUrl variant, got: {other:?}"),
}
}
#[test]
fn message_user_with_parts() {
let msg = Message::user_with_parts(vec![
ContentPart::text("hello"),
ContentPart::image_data_url("data:image/png;base64,aGk="),
]);
let json = serde_json::to_string(&msg).expect("serialization should not fail");
assert_eq!(
json,
r#"{"role":"user","content":[{"type":"text","text":"hello"},{"type":"image_url","image_url":{"url":"data:image/png;base64,aGk="}}]}"#
);
}
#[test]
fn json_schema_new_defaults_strict_true() {
let fmt = JsonSchemaFormat::new("S", serde_json::json!({}));
assert_eq!(fmt.strict, Some(true));
assert_eq!(fmt.description, None);
assert_eq!(fmt.name, "S");
}
#[test]
fn json_schema_strict_toggle() {
let fmt = JsonSchemaFormat::new("S", serde_json::json!({})).strict(false);
assert_eq!(fmt.strict, Some(false));
}
#[test]
fn json_schema_description_attaches() {
let fmt = JsonSchemaFormat::new("S", serde_json::json!({})).description("d");
assert_eq!(fmt.description.as_deref(), Some("d"));
}
#[test]
fn response_format_json_schema_serializes() {
let fmt = ResponseFormat::json_schema("S", serde_json::json!({"type": "object"}));
let value = serde_json::to_value(&fmt).expect("serialization must succeed");
assert_eq!(
value,
serde_json::json!({
"type": "json_schema",
"json_schema": {
"name": "S",
"schema": {"type": "object"},
"strict": true
}
})
);
assert!(value["json_schema"].get("description").is_none());
}
#[test]
fn response_format_json_object_serializes() {
let value = serde_json::to_value(ResponseFormat::json_object()).expect("serialization must succeed");
assert_eq!(value, serde_json::json!({"type": "json_object"}));
}
#[test]
fn response_format_text_serializes() {
let value = serde_json::to_value(ResponseFormat::text()).expect("serialization must succeed");
assert_eq!(value, serde_json::json!({"type": "text"}));
}
#[test]
fn chat_request_serializes_response_format() {
use crate::types::chat::ChatCompletionRequest;
let request = ChatCompletionRequest {
model: "gpt-4o".into(),
messages: vec![],
response_format: Some(ResponseFormat::json_schema(
"PersonSchema",
serde_json::json!({"type": "object", "properties": {"name": {"type": "string"}}}),
)),
..Default::default()
};
let value = serde_json::to_value(&request).expect("serialization must succeed");
let rf = &value["response_format"];
assert_eq!(rf["type"], "json_schema");
assert_eq!(rf["json_schema"]["name"], "PersonSchema");
assert_eq!(rf["json_schema"]["strict"], true);
}
#[test]
fn assistant_content_text_deserializes_from_scalar_string() {
let json = r#"{"role":"assistant","content":"hi"}"#;
let msg: Message = serde_json::from_str(json).expect("must deserialise");
let Message::Assistant(a) = msg else {
panic!("expected assistant")
};
assert_eq!(a.content, Some(AssistantContent::Text("hi".into())));
}
#[test]
fn assistant_content_parts_deserializes_from_array() {
let json = r#"{"role":"assistant","content":[{"type":"text","text":"hi"}]}"#;
let msg: Message = serde_json::from_str(json).expect("must deserialise");
let Message::Assistant(a) = msg else {
panic!("expected assistant")
};
assert_eq!(
a.content,
Some(AssistantContent::Parts(vec![AssistantPart::Text { text: "hi".into() }]))
);
}
#[test]
fn assistant_content_display_renders_text() {
assert_eq!(AssistantContent::Text("hi there".into()).to_string(), "hi there");
let parts = AssistantContent::Parts(vec![
AssistantPart::Text { text: "a".into() },
AssistantPart::OutputImage {
image_url: ImageUrl::default(),
},
AssistantPart::Text { text: "b".into() },
]);
assert_eq!(parts.to_string(), "ab");
let image_only = AssistantContent::Parts(vec![AssistantPart::OutputImage {
image_url: ImageUrl::default(),
}]);
assert_eq!(image_only.to_string(), "");
}
#[test]
fn assistant_message_text_helper_with_text() {
let a = AssistantMessage {
content: Some(AssistantContent::Text("hello".into())),
..Default::default()
};
assert_eq!(a.text(), Some("hello".into()));
}
#[test]
fn assistant_message_text_helper_with_parts() {
let a = AssistantMessage {
content: Some(AssistantContent::Parts(vec![
AssistantPart::Text { text: "foo".into() },
AssistantPart::Text { text: "bar".into() },
])),
..Default::default()
};
assert_eq!(a.text(), Some("foobar".into()));
}
#[test]
fn assistant_message_text_helper_with_refusal_only_is_none() {
let a = AssistantMessage {
content: Some(AssistantContent::Parts(vec![AssistantPart::Refusal {
refusal: "I cannot do that.".into(),
}])),
..Default::default()
};
assert_eq!(a.text(), None);
}
#[test]
fn assistant_part_output_image_serializes() {
let part = AssistantPart::OutputImage {
image_url: ImageUrl {
url: "data:image/png;base64,aGk=".into(),
detail: None,
},
};
let json = serde_json::to_string(&part).expect("must serialise");
assert_eq!(
json,
r#"{"type":"output_image","image_url":{"url":"data:image/png;base64,aGk="}}"#
);
}
#[test]
fn assistant_part_output_audio_serializes() {
let part = AssistantPart::OutputAudio {
audio: AudioContent {
data: "aGk=".into(),
format: "wav".into(),
},
};
let json = serde_json::to_string(&part).expect("must serialise");
assert_eq!(
json,
r#"{"type":"output_audio","audio":{"data":"aGk=","format":"wav"}}"#
);
}
#[test]
fn message_system_with_parts_serializes() {
let msg = Message::system_with_parts(vec![ContentPart::text("You are helpful.")]);
let json = serde_json::to_string(&msg).expect("must serialise");
assert_eq!(
json,
r#"{"role":"system","content":[{"type":"text","text":"You are helpful."}]}"#
);
}
#[test]
fn message_assistant_with_parts_round_trips() {
let msg = Message::assistant_with_parts(vec![AssistantPart::Text { text: "ok".into() }]);
let json = serde_json::to_string(&msg).expect("must serialise");
assert_eq!(json, r#"{"role":"assistant","content":[{"type":"text","text":"ok"}]}"#);
}
#[test]
fn assistant_output_images_and_audio_helpers() {
let url = ImageUrl {
url: "data:image/png;base64,aGk=".into(),
detail: None,
};
let audio = AudioContent {
data: "aGk=".into(),
format: "wav".into(),
};
let a = AssistantMessage {
content: Some(AssistantContent::Parts(vec![
AssistantPart::OutputImage { image_url: url.clone() },
AssistantPart::OutputAudio { audio: audio.clone() },
])),
..Default::default()
};
assert_eq!(a.output_images(), vec![url.clone()]);
assert_eq!(a.output_audio(), vec![audio.clone()]);
}
#[test]
fn system_message_content_from_string_back_compat() {
let json = r#"{"role":"system","content":"You are a helpful assistant."}"#;
let msg: Message = serde_json::from_str(json).expect("must deserialise");
let Message::System(s) = msg else {
panic!("expected system")
};
assert_eq!(s.content, UserContent::Text("You are a helpful assistant.".into()));
}
#[test]
fn assistant_message_reasoning_content_omitted_when_none() {
let a = AssistantMessage {
content: Some(AssistantContent::Text("answer".into())),
..Default::default()
};
let json = serde_json::to_string(&a).expect("serialization should not fail");
assert!(
!json.contains("reasoning_content"),
"reasoning_content key must be absent when None, got: {json}"
);
}
#[test]
fn assistant_message_reasoning_content_deserializes_and_is_exposed_via_helper() {
let json = r#"{"content":"answer","reasoning_content":"because..."}"#;
let a: AssistantMessage = serde_json::from_str(json).expect("valid assistant message shape");
assert_eq!(a.reasoning_content.as_deref(), Some("because..."));
assert_eq!(a.reasoning_text(), Some("because..."));
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub enum ToolType {
#[default]
#[serde(rename = "function")]
Function,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ChatCompletionTool {
#[serde(rename = "type")]
pub tool_type: ToolType,
pub function: FunctionDefinition,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct FunctionDefinition {
pub name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub parameters: Option<serde_json::Value>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub strict: Option<bool>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ToolCall {
pub id: String,
#[serde(rename = "type")]
pub call_type: ToolType,
pub function: FunctionCall,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct FunctionCall {
pub name: String,
pub arguments: String,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum ToolChoice {
Mode(ToolChoiceMode),
Specific(SpecificToolChoice),
}
#[cfg_attr(alef, alef(skip))]
impl Default for ToolChoice {
fn default() -> Self {
Self::Mode(ToolChoiceMode::default())
}
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ToolChoiceMode {
#[default]
Auto,
Required,
None,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct SpecificToolChoice {
#[serde(rename = "type")]
pub choice_type: ToolType,
pub function: SpecificFunction,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct SpecificFunction {
pub name: String,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum ResponseFormat {
#[default]
#[serde(rename = "text")]
Text,
#[serde(rename = "json_object")]
JsonObject,
#[serde(rename = "json_schema")]
JsonSchema { json_schema: JsonSchemaFormat },
}
impl ResponseFormat {
pub fn json_schema(name: impl Into<String>, schema: serde_json::Value) -> Self {
Self::JsonSchema {
json_schema: JsonSchemaFormat::new(name, schema),
}
}
pub fn json_object() -> Self {
Self::JsonObject
}
pub fn text() -> Self {
Self::Text
}
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct JsonSchemaFormat {
pub name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
pub schema: serde_json::Value,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub strict: Option<bool>,
}
impl JsonSchemaFormat {
pub fn new(name: impl Into<String>, schema: serde_json::Value) -> Self {
Self {
name: name.into(),
description: None,
schema,
strict: Some(true),
}
}
#[cfg_attr(alef, alef(skip))]
#[must_use]
pub fn strict(mut self, on: bool) -> Self {
self.strict = Some(on);
self
}
#[cfg_attr(alef, alef(skip))]
#[must_use]
pub fn description(mut self, d: impl Into<String>) -> Self {
self.description = Some(d.into());
self
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct Usage {
#[serde(default)]
pub prompt_tokens: u64,
#[serde(default)]
pub completion_tokens: u64,
#[serde(default)]
pub total_tokens: u64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub prompt_tokens_details: Option<PromptTokensDetails>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct PromptTokensDetails {
#[serde(default)]
pub cached_tokens: u64,
#[serde(default)]
pub audio_tokens: u64,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum StopSequence {
Single(String),
Multiple(Vec<String>),
}
#[cfg_attr(alef, alef(skip))]
impl Default for StopSequence {
fn default() -> Self {
Self::Single(String::new())
}
}