use serde::{Deserialize, Serialize};
use std::{convert::Infallible, str::FromStr};
use thiserror::Error;
use super::CompletionError;
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
#[serde(tag = "role", rename_all = "lowercase")]
pub enum Message {
System { content: String },
User { content: Vec<UserContent> },
Assistant {
id: Option<String>,
content: Vec<AssistantContent>,
},
}
pub const EMPTY_RESPONSE_ERROR: &str = "Response contained no message or tool call (empty)";
pub fn require_non_empty<T, E>(items: Vec<T>, error: impl FnOnce() -> E) -> Result<Vec<T>, E> {
if items.is_empty() {
return Err(error());
}
Ok(items)
}
pub fn require_non_empty_response<T>(items: Vec<T>) -> Result<Vec<T>, CompletionError> {
require_non_empty(items, || {
CompletionError::ResponseError(EMPTY_RESPONSE_ERROR.to_owned())
})
}
pub fn non_empty<T>(items: Vec<T>) -> Option<Vec<T>> {
if items.is_empty() { None } else { Some(items) }
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
#[serde(tag = "type", rename_all = "lowercase")]
pub enum UserContent {
Text(Text),
ToolResult(ToolResult),
Image(Image),
Audio(Audio),
Video(Video),
Document(Document),
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
#[serde(tag = "type", rename_all = "lowercase")]
pub enum AssistantContent {
Text(Text),
ToolCall(ToolCall),
Reasoning(Reasoning),
Image(Image),
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
#[serde(tag = "type", content = "content", rename_all = "snake_case")]
pub enum ReasoningContent {
Text {
text: String,
#[serde(skip_serializing_if = "Option::is_none")]
signature: Option<String>,
},
Encrypted(String),
Redacted { data: String },
Summary(String),
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
pub struct Reasoning {
pub id: Option<String>,
pub content: Vec<ReasoningContent>,
}
impl Reasoning {
pub fn new(input: &str) -> Self {
Self::new_with_signature(input, None)
}
pub fn new_with_signature(input: &str, signature: Option<String>) -> Self {
Self {
id: None,
content: vec![ReasoningContent::Text {
text: input.to_string(),
signature,
}],
}
}
pub fn with_id(mut self, id: String) -> Self {
self.id = Some(id);
self
}
pub fn multi(input: Vec<String>) -> Self {
Self {
id: None,
content: input
.into_iter()
.map(|text| ReasoningContent::Text {
text,
signature: None,
})
.collect(),
}
}
pub fn redacted(data: impl Into<String>) -> Self {
Self {
id: None,
content: vec![ReasoningContent::Redacted { data: data.into() }],
}
}
pub fn encrypted(data: impl Into<String>) -> Self {
Self {
id: None,
content: vec![ReasoningContent::Encrypted(data.into())],
}
}
pub fn summaries(input: Vec<String>) -> Self {
Self {
id: None,
content: input.into_iter().map(ReasoningContent::Summary).collect(),
}
}
pub fn display_text(&self) -> String {
self.content
.iter()
.filter_map(|content| match content {
ReasoningContent::Text { text, .. } => Some(text.as_str()),
ReasoningContent::Summary(summary) => Some(summary.as_str()),
ReasoningContent::Redacted { data } => Some(data.as_str()),
ReasoningContent::Encrypted(_) => None,
})
.collect::<Vec<_>>()
.join("\n")
}
pub fn first_text(&self) -> Option<&str> {
self.content.iter().find_map(|content| match content {
ReasoningContent::Text { text, .. } => Some(text.as_str()),
_ => None,
})
}
pub fn first_signature(&self) -> Option<&str> {
self.content.iter().find_map(|content| match content {
ReasoningContent::Text {
signature: Some(signature),
..
} => Some(signature.as_str()),
_ => None,
})
}
pub fn encrypted_content(&self) -> Option<&str> {
self.content.iter().find_map(|content| match content {
ReasoningContent::Encrypted(data) => Some(data.as_str()),
_ => None,
})
}
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
pub struct ToolResult {
pub call: ToolCallId,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub provider: Option<ProviderCallId>,
pub name: String,
pub content: Vec<ToolResultContent>,
}
impl ToolResult {
pub fn wire_call_id(&self) -> &str {
self.provider
.as_ref()
.map_or(self.call.as_str(), |provider| provider.call_id.as_str())
}
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
#[serde(tag = "type", rename_all = "lowercase")]
pub enum ToolResultContent {
Text(Text),
Image(Image),
Json {
value: serde_json::Value,
},
}
impl ToolResultContent {
pub fn as_text(&self) -> Option<&str> {
match self {
Self::Text(text) => Some(&text.text),
Self::Image(_) | Self::Json { .. } => None,
}
}
pub fn as_json(&self) -> Option<&serde_json::Value> {
match self {
Self::Json { value } => Some(value),
Self::Text(_) | Self::Image(_) => None,
}
}
pub fn deserialize_json<T>(&self) -> Result<T, serde_json::Error>
where
T: serde::de::DeserializeOwned,
{
match self {
Self::Json { value } => serde_json::from_value(value.clone()),
Self::Text(text) => serde_json::from_str(&text.text),
Self::Image(_) => Err(<serde_json::Error as serde::de::Error>::custom(
"cannot decode image tool-result content as JSON",
)),
}
}
}
#[derive(Debug, thiserror::Error)]
#[error("a tool-call identifier cannot be the empty string; absence is `None` or a minted id")]
pub struct EmptyToolCallId;
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(try_from = "String", into = "String")]
pub struct ToolCallId(String);
impl ToolCallId {
pub fn new(id: impl Into<String>) -> Option<Self> {
let id = id.into();
if id.is_empty() { None } else { Some(Self(id)) }
}
pub fn mint() -> Self {
Self(crate::id::generate())
}
pub fn new_or_mint(id: impl Into<String>) -> Self {
Self::new(id).unwrap_or_else(Self::mint)
}
pub fn for_provider(provider: Option<&ProviderCallId>) -> Self {
provider
.and_then(|provider| Self::new(provider.call_id.clone()))
.unwrap_or_else(Self::mint)
}
pub fn as_str(&self) -> &str {
&self.0
}
pub fn into_string(self) -> String {
self.0
}
}
impl std::fmt::Display for ToolCallId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
impl AsRef<str> for ToolCallId {
fn as_ref(&self) -> &str {
&self.0
}
}
impl std::ops::Deref for ToolCallId {
type Target = str;
fn deref(&self) -> &str {
&self.0
}
}
impl std::borrow::Borrow<str> for ToolCallId {
fn borrow(&self) -> &str {
&self.0
}
}
impl TryFrom<String> for ToolCallId {
type Error = EmptyToolCallId;
fn try_from(id: String) -> Result<Self, Self::Error> {
Self::new(id).ok_or(EmptyToolCallId)
}
}
impl From<ToolCallId> for String {
fn from(id: ToolCallId) -> Self {
id.0
}
}
impl PartialEq<str> for ToolCallId {
fn eq(&self, other: &str) -> bool {
self.0 == other
}
}
impl PartialEq<&str> for ToolCallId {
fn eq(&self, other: &&str) -> bool {
self.0 == *other
}
}
#[derive(Deserialize)]
struct ProviderCallIdWire {
call_id: String,
#[serde(default)]
item_id: Option<String>,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(try_from = "ProviderCallIdWire")]
pub struct ProviderCallId {
pub call_id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub item_id: Option<String>,
}
impl ProviderCallId {
pub fn new(call_id: impl Into<String>) -> Option<Self> {
let call_id = call_id.into();
if call_id.is_empty() {
None
} else {
Some(Self {
call_id,
item_id: None,
})
}
}
pub fn with_item_id(mut self, item_id: impl Into<String>) -> Self {
let item_id = item_id.into();
self.item_id = (!item_id.is_empty()).then_some(item_id);
self
}
pub fn from_optional_wire(call_id: Option<String>, tool_id: Option<String>) -> Option<Self> {
let call_id = call_id.filter(|call_id| !call_id.is_empty());
match (call_id, tool_id) {
(Some(call_id), tool_id) => Self::new(call_id).map(|provider| match tool_id {
Some(tool_id) => provider.with_item_id(tool_id),
None => provider,
}),
(None, Some(tool_id)) => Self::new(tool_id),
(None, None) => None,
}
}
}
impl TryFrom<ProviderCallIdWire> for ProviderCallId {
type Error = EmptyToolCallId;
fn try_from(wire: ProviderCallIdWire) -> Result<Self, Self::Error> {
let Some(provider) = Self::new(wire.call_id) else {
return Err(EmptyToolCallId);
};
Ok(match wire.item_id {
Some(item_id) => provider.with_item_id(item_id),
None => provider,
})
}
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
pub struct ToolCall {
pub id: ToolCallId,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub provider: Option<ProviderCallId>,
pub function: ToolFunction,
#[serde(default)]
pub signature: Option<String>,
#[serde(default)]
pub additional_params: Option<serde_json::Value>,
}
impl ToolCall {
fn assemble(provider: Option<ProviderCallId>, function: ToolFunction) -> Self {
Self {
id: ToolCallId::for_provider(provider.as_ref()),
provider,
function,
signature: None,
additional_params: None,
}
}
pub fn new(id: ToolCallId, function: ToolFunction) -> Self {
Self {
id,
..Self::assemble(None, function)
}
}
pub fn from_wire(wire_id: impl Into<String>, function: ToolFunction) -> Self {
Self::assemble(ProviderCallId::new(wire_id), function)
}
pub fn from_dual_wire(
item_id: impl Into<String>,
call_id: impl Into<String>,
function: ToolFunction,
) -> Self {
let provider =
ProviderCallId::new(call_id).map(|provider| provider.with_item_id(item_id.into()));
Self::assemble(provider, function)
}
pub fn with_provider(mut self, provider: ProviderCallId) -> Self {
self.provider = Some(provider);
self
}
pub fn wire_call_id(&self) -> &str {
self.provider
.as_ref()
.map_or(self.id.as_str(), |provider| provider.call_id.as_str())
}
pub fn with_signature(mut self, signature: Option<String>) -> Self {
self.signature = signature;
self
}
pub fn with_additional_params(mut self, additional_params: Option<serde_json::Value>) -> Self {
self.additional_params = additional_params;
self
}
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
pub struct ToolFunction {
pub name: String,
pub arguments: serde_json::Value,
}
impl ToolFunction {
pub fn new(name: String, arguments: serde_json::Value) -> Self {
Self { name, arguments }
}
}
#[derive(Clone, Debug, PartialEq, Serialize)]
#[serde(transparent)]
pub struct AdditionalParams(serde_json::Map<String, serde_json::Value>);
impl AdditionalParams {
pub fn new(map: serde_json::Map<String, serde_json::Value>) -> Option<Self> {
if map.is_empty() {
None
} else {
Some(Self(map))
}
}
pub fn from_entries<K, I>(entries: I) -> Option<Self>
where
K: Into<String>,
I: IntoIterator<Item = (K, serde_json::Value)>,
{
Self::new(
entries
.into_iter()
.map(|(key, value)| (key.into(), value))
.collect(),
)
}
pub fn get(&self, key: &str) -> Option<&serde_json::Value> {
self.0.get(key)
}
pub fn as_map(&self) -> &serde_json::Map<String, serde_json::Value> {
&self.0
}
pub fn into_value(self) -> serde_json::Value {
serde_json::Value::Object(self.0)
}
pub fn merge(&mut self, incoming: Self) {
fn merge_maps(
existing: &mut serde_json::Map<String, serde_json::Value>,
incoming: serde_json::Map<String, serde_json::Value>,
) {
for (key, incoming_value) in incoming {
match existing.get_mut(&key) {
Some(existing_value) => merge_value(existing_value, incoming_value),
None => {
existing.insert(key, incoming_value);
}
}
}
}
fn merge_value(existing: &mut serde_json::Value, incoming: serde_json::Value) {
match (existing, incoming) {
(
serde_json::Value::Object(existing_map),
serde_json::Value::Object(incoming_map),
) => merge_maps(existing_map, incoming_map),
(
serde_json::Value::Array(existing_array),
serde_json::Value::Array(mut incoming_array),
) => existing_array.append(&mut incoming_array),
(existing, incoming) => *existing = incoming,
}
}
merge_maps(&mut self.0, incoming.0);
}
pub fn wire_extras(
&self,
wire_key: &str,
) -> Option<&serde_json::Map<String, serde_json::Value>> {
self.0.get(wire_key).and_then(serde_json::Value::as_object)
}
pub fn into_wire_extras(
mut self,
wire_key: &str,
) -> Option<serde_json::Map<String, serde_json::Value>> {
match self.0.remove(wire_key) {
Some(serde_json::Value::Object(map)) => Some(map),
_ => None,
}
}
pub fn try_from_value(value: serde_json::Value) -> Result<Option<Self>, serde_json::Value> {
match value {
serde_json::Value::Null => Ok(None),
serde_json::Value::Object(map) => Ok(Self::new(map)),
other => Err(other),
}
}
}
impl From<AdditionalParams> for serde_json::Value {
fn from(params: AdditionalParams) -> Self {
params.into_value()
}
}
impl std::ops::Index<&str> for AdditionalParams {
type Output = serde_json::Value;
#[allow(clippy::indexing_slicing)]
fn index(&self, key: &str) -> &serde_json::Value {
&self.0[key]
}
}
impl<'de> Deserialize<'de> for AdditionalParams {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
match Self::try_from_value(serde_json::Value::deserialize(deserializer)?) {
Ok(Some(params)) => Ok(params),
Ok(None) => Err(serde::de::Error::custom(
"`additional_params` carries no data — omit the field (an `Option` \
field routed through `optional_additional_params` canonicalizes \
`{}` and `null` to absent)",
)),
Err(_) => Err(serde::de::Error::custom(
"`additional_params` must be a non-empty JSON object",
)),
}
}
}
pub fn keys_lost_in_round_trip(
original: &serde_json::Value,
round_tripped: &serde_json::Value,
) -> Vec<String> {
fn walk(
original: &serde_json::Value,
round_tripped: &serde_json::Value,
path: &mut String,
lost: &mut Vec<String>,
) {
match (original, round_tripped) {
(serde_json::Value::Object(original_map), serde_json::Value::Object(round_map)) => {
for (key, original_value) in original_map {
if original_value.is_null() {
continue;
}
let checkpoint = path.len();
if !path.is_empty() {
path.push('.');
}
path.push_str(key);
match round_map.get(key) {
Some(round_value) => walk(original_value, round_value, path, lost),
None => {
if !original_value
.as_object()
.is_some_and(serde_json::Map::is_empty)
{
lost.push(path.clone());
}
}
}
path.truncate(checkpoint);
}
}
(serde_json::Value::Array(original_items), serde_json::Value::Array(round_items)) => {
for (index, original_value) in original_items.iter().enumerate() {
let checkpoint = path.len();
if !path.is_empty() {
path.push('.');
}
path.push_str(&index.to_string());
match round_items.get(index) {
Some(round_value) => walk(original_value, round_value, path, lost),
None => lost.push(path.clone()),
}
path.truncate(checkpoint);
}
}
(original, round_tripped) => {
if original != round_tripped {
lost.push(path.clone());
}
}
}
}
let mut lost = Vec::new();
walk(original, round_tripped, &mut String::new(), &mut lost);
lost
}
pub fn optional_additional_params<'de, D>(
deserializer: D,
) -> Result<Option<AdditionalParams>, D::Error>
where
D: serde::Deserializer<'de>,
{
match Option::<serde_json::Value>::deserialize(deserializer)? {
None => Ok(None),
Some(value) => AdditionalParams::try_from_value(value).map_err(|_| {
serde::de::Error::custom("`additional_params` must be a JSON object (or null)")
}),
}
}
#[derive(Default, Clone, Debug, Deserialize, Serialize, PartialEq)]
pub struct Text {
pub text: String,
#[serde(
default,
deserialize_with = "optional_additional_params",
skip_serializing_if = "Option::is_none"
)]
pub additional_params: Option<AdditionalParams>,
}
impl Text {
pub fn new(text: impl Into<String>) -> Self {
Self {
text: text.into(),
additional_params: None,
}
}
pub fn text(&self) -> &str {
&self.text
}
}
impl std::fmt::Display for Text {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let Self { text, .. } = self;
write!(f, "{text}")
}
}
#[derive(Default, Clone, Debug, Deserialize, Serialize, PartialEq)]
pub struct Image {
pub data: DocumentSourceKind,
#[serde(skip_serializing_if = "Option::is_none")]
pub media_type: Option<ImageMediaType>,
#[serde(skip_serializing_if = "Option::is_none")]
pub detail: Option<ImageDetail>,
#[serde(
default,
deserialize_with = "optional_additional_params",
skip_serializing_if = "Option::is_none"
)]
pub additional_params: Option<AdditionalParams>,
}
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Default)]
#[serde(tag = "type", content = "value", rename_all = "camelCase")]
pub enum DocumentSourceKind {
Url(String),
Base64(String),
FileId(String),
Raw(Vec<u8>),
String(String),
#[default]
Unknown,
}
impl DocumentSourceKind {
pub fn url(url: &str) -> Self {
Self::Url(url.to_string())
}
pub fn base64(base64_string: &str) -> Self {
Self::Base64(base64_string.to_string())
}
pub fn file_id(file_id: &str) -> Self {
Self::FileId(file_id.to_string())
}
pub fn string(input: &str) -> Self {
Self::String(input.into())
}
pub fn try_into_inner(self) -> Option<String> {
match self {
Self::Url(s) | Self::Base64(s) | Self::FileId(s) => Some(s),
_ => None,
}
}
}
impl std::fmt::Display for DocumentSourceKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Url(string) => write!(f, "{string}"),
Self::Base64(string) => write!(f, "{string}"),
Self::FileId(string) => write!(f, "{string}"),
Self::String(string) => write!(f, "{string}"),
Self::Raw(_) => write!(f, "<binary data>"),
Self::Unknown => write!(f, "<unknown>"),
}
}
}
#[derive(Default, Clone, Debug, Deserialize, Serialize, PartialEq)]
pub struct Audio {
pub data: DocumentSourceKind,
#[serde(skip_serializing_if = "Option::is_none")]
pub media_type: Option<AudioMediaType>,
#[serde(
default,
deserialize_with = "optional_additional_params",
skip_serializing_if = "Option::is_none"
)]
pub additional_params: Option<AdditionalParams>,
}
#[derive(Default, Clone, Debug, Deserialize, Serialize, PartialEq)]
pub struct Video {
pub data: DocumentSourceKind,
#[serde(skip_serializing_if = "Option::is_none")]
pub media_type: Option<VideoMediaType>,
#[serde(
default,
deserialize_with = "optional_additional_params",
skip_serializing_if = "Option::is_none"
)]
pub additional_params: Option<AdditionalParams>,
}
#[derive(Default, Clone, Debug, Deserialize, Serialize, PartialEq)]
pub struct Document {
pub data: DocumentSourceKind,
#[serde(skip_serializing_if = "Option::is_none")]
pub media_type: Option<DocumentMediaType>,
#[serde(
default,
deserialize_with = "optional_additional_params",
skip_serializing_if = "Option::is_none"
)]
pub additional_params: Option<AdditionalParams>,
}
#[derive(Default, Clone, Debug, Deserialize, Serialize, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum ContentFormat {
#[default]
Base64,
String,
Url,
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
pub enum MediaType {
Image(ImageMediaType),
Audio(AudioMediaType),
Document(DocumentMediaType),
Video(VideoMediaType),
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum ImageMediaType {
JPEG,
PNG,
GIF,
WEBP,
HEIC,
HEIF,
SVG,
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum DocumentMediaType {
PDF,
TXT,
RTF,
HTML,
CSS,
MARKDOWN,
CSV,
XML,
Javascript,
Python,
}
impl DocumentMediaType {
pub fn is_code(&self) -> bool {
matches!(self, Self::Javascript | Self::Python)
}
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum AudioMediaType {
WAV,
MP3,
AIFF,
AAC,
OGG,
FLAC,
M4A,
PCM16,
PCM24,
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum VideoMediaType {
AVI,
MP4,
MPEG,
MOV,
WEBM,
}
#[derive(Default, Clone, Debug, Deserialize, Serialize, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum ImageDetail {
Low,
High,
#[default]
Auto,
}
impl Message {
pub fn rag_text(&self) -> Option<String> {
match self {
Message::User { content } => {
for item in content.iter() {
if let UserContent::Text(Text { text, .. }) = item {
return Some(text.clone());
}
}
None
}
Message::System { .. } => None,
_ => None,
}
}
pub fn system(text: impl Into<String>) -> Self {
Message::System {
content: text.into(),
}
}
pub fn user(text: impl Into<String>) -> Self {
Message::User {
content: vec![UserContent::text(text)],
}
}
pub fn assistant(text: impl Into<String>) -> Self {
Message::Assistant {
id: None,
content: vec![AssistantContent::text(text)],
}
}
pub fn tool_result(
call: impl Into<String>,
name: impl Into<String>,
content: impl Into<String>,
) -> Self {
Message::User {
content: vec![UserContent::tool_result(
call,
name,
vec![ToolResultContent::text(content)],
)],
}
}
}
macro_rules! media_ctors {
() => {};
(
$(#[$meta:meta])* $name:ident => Image($kind:ident: $data:ty);
$($rest:tt)*
) => {
$(#[$meta])*
pub fn $name(
data: impl Into<$data>,
media_type: Option<ImageMediaType>,
detail: Option<ImageDetail>,
) -> Self {
Self::Image(Image {
data: DocumentSourceKind::$kind(data.into()),
media_type,
detail,
additional_params: None,
})
}
media_ctors! { $($rest)* }
};
(
$(#[$meta:meta])* $name:ident => $variant:ident($mt:ty, $kind:ident: $data:ty);
$($rest:tt)*
) => {
$(#[$meta])*
pub fn $name(data: impl Into<$data>, media_type: Option<$mt>) -> Self {
Self::$variant($variant {
data: DocumentSourceKind::$kind(data.into()),
media_type,
additional_params: None,
})
}
media_ctors! { $($rest)* }
};
}
impl UserContent {
pub fn text(text: impl Into<String>) -> Self {
UserContent::Text(text.into().into())
}
media_ctors! {
image_base64 => Image(Base64: String);
image_raw => Image(Raw: Vec<u8>);
image_url => Image(Url: String);
audio => Audio(AudioMediaType, Base64: String);
audio_raw => Audio(AudioMediaType, Raw: Vec<u8>);
audio_url => Audio(AudioMediaType, Url: String);
video => Video(VideoMediaType, Base64: String);
video_raw => Video(VideoMediaType, Raw: Vec<u8>);
video_url => Video(VideoMediaType, Url: String);
document_raw => Document(DocumentMediaType, Raw: Vec<u8>);
document_url => Document(DocumentMediaType, Url: String);
}
pub fn document(data: impl Into<String>, media_type: Option<DocumentMediaType>) -> Self {
let data: String = data.into();
UserContent::Document(Document {
data: DocumentSourceKind::string(&data),
media_type,
additional_params: None,
})
}
pub fn tool_result(
call: impl Into<String>,
name: impl Into<String>,
content: Vec<ToolResultContent>,
) -> Self {
UserContent::ToolResult(ToolResult {
call: ToolCallId::new_or_mint(call),
provider: None,
name: name.into(),
content,
})
}
pub fn tool_result_from_wire(
wire_id: impl Into<String>,
name: impl Into<String>,
content: Vec<ToolResultContent>,
) -> Self {
let provider = ProviderCallId::new(wire_id);
let call = ToolCallId::for_provider(provider.as_ref());
Self::tool_result_for(call, provider, name, content)
}
pub fn tool_result_for(
call: ToolCallId,
provider: Option<ProviderCallId>,
name: impl Into<String>,
content: Vec<ToolResultContent>,
) -> Self {
UserContent::ToolResult(ToolResult {
call,
provider,
name: name.into(),
content,
})
}
pub fn tool_result_with_call_id(
item_id: impl Into<String>,
call_id: impl Into<String>,
name: impl Into<String>,
content: Vec<ToolResultContent>,
) -> Self {
let provider = ProviderCallId::new(call_id).map(|provider| provider.with_item_id(item_id));
let call = ToolCallId::for_provider(provider.as_ref());
Self::tool_result_for(call, provider, name, content)
}
}
impl AssistantContent {
pub fn text(text: impl Into<String>) -> Self {
AssistantContent::Text(text.into().into())
}
media_ctors! {
image_base64 => Image(Base64: String);
}
pub fn tool_call(
id: impl Into<String>,
name: impl Into<String>,
arguments: serde_json::Value,
) -> Self {
AssistantContent::ToolCall(ToolCall::from_wire(
id,
ToolFunction {
name: name.into(),
arguments,
},
))
}
pub fn tool_call_with_call_id(
id: impl Into<String>,
call_id: String,
name: impl Into<String>,
arguments: serde_json::Value,
) -> Self {
AssistantContent::ToolCall(ToolCall::from_dual_wire(
id,
call_id,
ToolFunction {
name: name.into(),
arguments,
},
))
}
pub fn reasoning(reasoning: impl AsRef<str>) -> Self {
AssistantContent::Reasoning(Reasoning::new(reasoning.as_ref()))
}
}
impl ToolResultContent {
pub fn text(text: impl Into<String>) -> Self {
ToolResultContent::Text(text.into().into())
}
pub fn json(value: serde_json::Value) -> Self {
ToolResultContent::Json { value }
}
media_ctors! {
image_base64 => Image(Base64: String);
image_raw => Image(Raw: Vec<u8>);
image_url => Image(Url: String);
}
}
pub trait MimeType {
fn from_mime_type(mime_type: &str) -> Option<Self>
where
Self: Sized;
fn to_mime_type(&self) -> &'static str;
}
impl MimeType for MediaType {
fn from_mime_type(mime_type: &str) -> Option<Self> {
ImageMediaType::from_mime_type(mime_type)
.map(MediaType::Image)
.or_else(|| DocumentMediaType::from_mime_type(mime_type).map(MediaType::Document))
.or_else(|| AudioMediaType::from_mime_type(mime_type).map(MediaType::Audio))
.or_else(|| VideoMediaType::from_mime_type(mime_type).map(MediaType::Video))
}
fn to_mime_type(&self) -> &'static str {
match self {
MediaType::Image(media_type) => media_type.to_mime_type(),
MediaType::Audio(media_type) => media_type.to_mime_type(),
MediaType::Document(media_type) => media_type.to_mime_type(),
MediaType::Video(media_type) => media_type.to_mime_type(),
}
}
}
macro_rules! impl_mime_type {
($ty:ident { $($variant:ident => $canonical:literal $(| $alias:literal)*),+ $(,)? }) => {
impl MimeType for $ty {
fn from_mime_type(mime_type: &str) -> Option<Self> {
match mime_type {
$($canonical $(| $alias)* => Some($ty::$variant),)+
_ => None,
}
}
fn to_mime_type(&self) -> &'static str {
match self {
$($ty::$variant => $canonical,)+
}
}
}
};
}
impl_mime_type!(ImageMediaType {
JPEG => "image/jpeg",
PNG => "image/png",
GIF => "image/gif",
WEBP => "image/webp",
HEIC => "image/heic",
HEIF => "image/heif",
SVG => "image/svg+xml",
});
impl_mime_type!(DocumentMediaType {
PDF => "application/pdf",
TXT => "text/plain",
RTF => "text/rtf",
HTML => "text/html",
CSS => "text/css",
MARKDOWN => "text/markdown" | "text/md",
CSV => "text/csv",
XML => "text/xml",
Javascript => "application/x-javascript" | "text/x-javascript",
Python => "application/x-python" | "text/x-python",
});
impl_mime_type!(AudioMediaType {
WAV => "audio/wav",
MP3 => "audio/mp3",
AIFF => "audio/aiff",
AAC => "audio/aac",
OGG => "audio/ogg",
FLAC => "audio/flac",
M4A => "audio/m4a",
PCM16 => "audio/pcm16",
PCM24 => "audio/pcm24",
});
impl_mime_type!(VideoMediaType {
AVI => "video/avi",
MP4 => "video/mp4",
MPEG => "video/mpeg",
MOV => "video/mov",
WEBM => "video/webm",
});
impl std::str::FromStr for ImageDetail {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"low" => Ok(ImageDetail::Low),
"high" => Ok(ImageDetail::High),
"auto" => Ok(ImageDetail::Auto),
_ => Err(()),
}
}
}
macro_rules! text_from {
($($src:ty),+ $(,)?) => {$(
impl From<$src> for Text {
fn from(text: $src) -> Self {
Text {
text: text.into(),
additional_params: None,
}
}
}
)+};
}
text_from!(String, &String, &str);
macro_rules! text_content_from_string {
($($ty:ident),+ $(,)?) => {$(
impl From<String> for $ty {
fn from(text: String) -> Self {
$ty::text(text)
}
}
)+};
}
text_content_from_string!(ToolResultContent, AssistantContent, UserContent);
macro_rules! single_content_message_from {
(User { $($src:ty => $variant:ident),+ $(,)? }) => {$(
impl From<$src> for Message {
fn from(value: $src) -> Self {
Message::User {
content: vec![UserContent::$variant(value.into())],
}
}
}
)+};
(Assistant { $($src:ty => $variant:ident),+ $(,)? }) => {$(
impl From<$src> for Message {
fn from(value: $src) -> Self {
Message::Assistant {
id: None,
content: vec![AssistantContent::$variant(value.into())],
}
}
}
)+};
}
single_content_message_from!(User {
String => Text,
&str => Text,
&String => Text,
Text => Text,
Image => Image,
Audio => Audio,
Document => Document,
ToolResult => ToolResult,
});
single_content_message_from!(Assistant {
ToolCall => ToolCall,
});
impl FromStr for Text {
type Err = Infallible;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(s.into())
}
}
impl From<&Message> for Message {
fn from(msg: &Message) -> Self {
msg.clone()
}
}
impl From<AssistantContent> for Message {
fn from(content: AssistantContent) -> Self {
Message::Assistant {
id: None,
content: vec![content],
}
}
}
impl From<UserContent> for Message {
fn from(content: UserContent) -> Self {
Message::User {
content: vec![content],
}
}
}
impl From<Vec<AssistantContent>> for Message {
fn from(content: Vec<AssistantContent>) -> Self {
Message::Assistant { id: None, content }
}
}
impl From<Vec<UserContent>> for Message {
fn from(content: Vec<UserContent>) -> Self {
Message::User { content }
}
}
impl From<ToolResultContent> for Message {
fn from(tool_result_content: ToolResultContent) -> Self {
Message::User {
content: vec![UserContent::ToolResult(ToolResult {
call: ToolCallId::mint(),
provider: None,
name: String::new(),
content: vec![tool_result_content],
})],
}
}
}
#[derive(Default, Clone, Debug, Deserialize, Serialize, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum ToolChoice {
#[default]
Auto,
None,
Required,
Specific {
function_names: Vec<String>,
},
}
#[derive(Debug, Error)]
pub enum MessageError {
#[error("Message conversion error: {0}")]
ConversionError(String),
}
impl From<MessageError> for CompletionError {
fn from(error: MessageError) -> Self {
CompletionError::RequestError(error.into())
}
}
#[cfg(test)]
mod tests {
use serde::{Deserialize, Serialize};
use super::{AdditionalParams, Message, Reasoning, ReasoningContent, Text, ToolResultContent};
mod vec_content_serde {
use super::super::{AssistantContent, Message, UserContent};
#[test]
fn message_content_still_serializes_as_a_plain_sequence() {
let message = Message::User {
content: vec![UserContent::text("hi")],
};
let json = serde_json::to_value(&message).expect("serialize");
assert_eq!(
json,
serde_json::json!({
"role": "user",
"content": [{"type": "text", "text": "hi"}],
})
);
}
#[test]
fn message_content_round_trips_byte_identically() {
let message = Message::Assistant {
id: Some("msg_1".to_owned()),
content: vec![AssistantContent::text("hello")],
};
let encoded = serde_json::to_string(&message).expect("serialize");
let decoded: Message = serde_json::from_str(&encoded).expect("deserialize");
assert_eq!(
serde_json::to_string(&decoded).expect("re-serialize"),
encoded
);
}
#[test]
fn an_empty_content_array_now_deserializes() {
let message: Message =
serde_json::from_value(serde_json::json!({"role": "user", "content": []}))
.expect("an empty content list is representable now");
let Message::User { content } = message else {
panic!("expected a user message");
};
assert!(content.is_empty());
}
}
#[test]
fn reasoning_constructors_and_accessors_work() {
let single = Reasoning::new("think");
assert_eq!(single.first_text(), Some("think"));
assert_eq!(single.first_signature(), None);
let signed = Reasoning::new_with_signature("signed", Some("sig-1".to_string()));
assert_eq!(signed.first_text(), Some("signed"));
assert_eq!(signed.first_signature(), Some("sig-1"));
let multi = Reasoning::multi(vec!["a".to_string(), "b".to_string()]);
assert_eq!(multi.display_text(), "a\nb");
assert_eq!(multi.first_text(), Some("a"));
let redacted = Reasoning::redacted("redacted-value");
assert_eq!(redacted.display_text(), "redacted-value");
assert_eq!(redacted.first_text(), None);
let encrypted = Reasoning::encrypted("enc");
assert_eq!(encrypted.encrypted_content(), Some("enc"));
assert_eq!(encrypted.display_text(), "");
let summaries = Reasoning::summaries(vec!["s1".to_string(), "s2".to_string()]);
assert_eq!(summaries.display_text(), "s1\ns2");
assert_eq!(summaries.encrypted_content(), None);
}
#[test]
fn reasoning_content_serde_roundtrip() {
let variants = vec![
ReasoningContent::Text {
text: "plain".to_string(),
signature: Some("sig".to_string()),
},
ReasoningContent::Encrypted("opaque".to_string()),
ReasoningContent::Redacted {
data: "redacted".to_string(),
},
ReasoningContent::Summary("summary".to_string()),
];
for variant in variants {
let json = serde_json::to_string(&variant).expect("serialize");
let roundtrip: ReasoningContent = serde_json::from_str(&json).expect("deserialize");
assert_eq!(roundtrip, variant);
}
}
#[test]
fn system_message_constructor_and_serde_roundtrip() {
let message = Message::system("You are concise.");
match &message {
Message::System { content } => assert_eq!(content, "You are concise."),
_ => panic!("Expected system message"),
}
let json = serde_json::to_string(&message).expect("serialize");
let roundtrip: Message = serde_json::from_str(&json).expect("deserialize");
assert_eq!(roundtrip, message);
}
#[test]
fn current_schema_tool_call_json_round_trips_without_provider_promotion() {
let call = super::ToolCall::new(
super::ToolCallId::new("minted-handle").expect("non-empty"),
super::ToolFunction {
name: "add".to_string(),
arguments: serde_json::json!({}),
},
);
let json = serde_json::to_value(&call).expect("serialize");
assert!(json.get("call_id").is_none());
let roundtrip: super::ToolCall = serde_json::from_value(json).expect("deserialize");
assert_eq!(roundtrip.provider, None);
assert_eq!(roundtrip, call);
}
#[test]
fn empty_params_canonicalize_to_none_in_both_serde_directions() {
for empty_spelling in [serde_json::json!({}), serde_json::Value::Null] {
let text: Text = serde_json::from_value(
serde_json::json!({"text": "x", "additional_params": empty_spelling}),
)
.expect("deserialize");
assert_eq!(text.additional_params, None);
}
let text: Text = serde_json::from_value(
serde_json::json!({"text": "x", "additional_params": {"citations": [1]}}),
)
.expect("deserialize");
assert_eq!(
text.additional_params,
AdditionalParams::from_entries([("citations", serde_json::json!([1]))])
);
assert_eq!(
text.additional_params
.as_ref()
.and_then(|params| params.get("citations")),
Some(&serde_json::json!([1]))
);
let round: Text = serde_json::from_value(serde_json::to_value(&text).expect("serialize"))
.expect("round trip");
assert_eq!(round, text);
assert_eq!(AdditionalParams::new(serde_json::Map::new()), None);
assert_eq!(
AdditionalParams::try_from_value(serde_json::json!({})).expect("object"),
None
);
let tolerant: Text = serde_json::from_value(
serde_json::json!({"text": "x", "citations": ["stray"], "future_field": 1}),
)
.expect("unknown keys on a block must not fail the decode");
assert_eq!(tolerant.text, "x");
assert_eq!(tolerant.additional_params, None);
for malformed in [serde_json::json!([]), serde_json::json!("title")] {
let err = serde_json::from_value::<Text>(
serde_json::json!({"text": "x", "additional_params": malformed}),
)
.expect_err("non-object params must be a decode error");
assert!(
err.to_string().contains("must be a JSON object"),
"unexpected error: {err}"
);
assert!(
AdditionalParams::try_from_value(serde_json::json!([])).is_err(),
"try_from_value must hand a non-object back, not swallow it"
);
}
}
#[test]
fn round_trip_diff_recipe_detects_every_dropped_key() {
let migrated = serde_json::json!({
"role": "assistant",
"content": [
{"type": "text", "text": "cited", "citations": ["not re-nested"]},
{"type": "text", "text": "clean",
"additional_params": {"citations": ["re-nested"]}},
],
});
let loaded: Message =
serde_json::from_value(migrated.clone()).expect("tolerant decode must succeed");
let reserialized = serde_json::to_value(&loaded).expect("serialize");
assert_eq!(
super::keys_lost_in_round_trip(&migrated, &reserialized),
vec!["content.0.citations".to_string()],
"every dropped key must be reported by path, and only dropped keys \
— writer-added defaults are not differences"
);
let clean = serde_json::json!({
"role": "assistant",
"content": [
{"type": "text", "text": "clean",
"additional_params": {"citations": ["re-nested"]}},
{"type": "text", "text": "mechanically migrated",
"additional_params": {}},
],
});
let loaded: Message = serde_json::from_value(clean.clone()).expect("decode");
let reserialized = serde_json::to_value(&loaded).expect("serialize");
assert_eq!(
super::keys_lost_in_round_trip(&clean, &reserialized),
Vec::<String>::new(),
"clean history must survive the round trip whole"
);
}
#[test]
fn legacy_call_id_key_is_ignored_not_lifted() {
let legacy = serde_json::json!({
"id": "fc_123",
"call_id": "call_abc",
"function": {"name": "add", "arguments": {"x": 1}},
});
let call: super::ToolCall = serde_json::from_value(legacy).expect("deserialize");
assert_eq!(call.id, "fc_123");
assert_eq!(call.provider, None);
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
struct ExecutorLikeResponse {
output: serde_json::Value,
logs: Vec<String>,
execution_time_ms: u64,
}
#[test]
fn tool_result_content_decodes_structured_and_legacy_json() {
let response = ExecutorLikeResponse {
output: serde_json::json!({"answer": 42}),
logs: vec!["computed".to_string()],
execution_time_ms: 7,
};
let value = serde_json::to_value(&response).expect("serialize response");
let structured = ToolResultContent::json(value.clone());
assert_eq!(structured.as_json(), Some(&value));
assert_eq!(structured.as_text(), None);
assert_eq!(
structured
.deserialize_json::<ExecutorLikeResponse>()
.expect("decode structured response"),
response
);
let legacy_json = value.to_string();
let legacy_text = ToolResultContent::Text(Text::new(legacy_json.clone()));
assert_eq!(legacy_text.as_text(), Some(legacy_json.as_str()));
assert_eq!(legacy_text.as_json(), None);
assert_eq!(
legacy_text
.deserialize_json::<ExecutorLikeResponse>()
.expect("decode legacy response"),
response
);
let image = ToolResultContent::image_url("https://example.com/result.png", None, None);
let image_error = image.deserialize_json::<ExecutorLikeResponse>();
assert!(image_error.is_err());
if let Err(error) = image_error {
assert_eq!(
error.to_string(),
"cannot decode image tool-result content as JSON"
);
}
}
}