use serde::Deserialize;
use serde::Serialize;
use crate::json::JsonValue;
use crate::shared::ApprovalId;
use crate::shared::FileData;
use crate::shared::MediaType;
use crate::shared::ProviderMetadata;
use crate::shared::ToolCallId;
use crate::shared::ToolName;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "kebab-case")]
#[non_exhaustive]
pub enum Content {
Text {
text: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
provider_metadata: Option<ProviderMetadata>,
},
Reasoning {
text: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
provider_metadata: Option<ProviderMetadata>,
},
ReasoningFile {
data: FileData,
media_type: MediaType,
#[serde(default, skip_serializing_if = "Option::is_none")]
provider_metadata: Option<ProviderMetadata>,
},
File {
data: FileData,
media_type: MediaType,
#[serde(default, skip_serializing_if = "Option::is_none")]
filename: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
provider_metadata: Option<ProviderMetadata>,
},
Custom {
kind: CustomKind,
#[serde(default, skip_serializing_if = "Option::is_none")]
provider_metadata: Option<ProviderMetadata>,
},
Source(Source),
ToolCall(ToolCall),
ToolResult(ProviderToolResult),
ToolApprovalRequest {
approval_id: ApprovalId,
tool_call_id: ToolCallId,
#[serde(default, skip_serializing_if = "Option::is_none")]
provider_metadata: Option<ProviderMetadata>,
},
}
impl Content {
#[must_use]
pub fn text(text: impl Into<String>) -> Self {
Self::Text {
text: text.into(),
provider_metadata: None,
}
}
#[must_use]
pub fn reasoning(text: impl Into<String>) -> Self {
Self::Reasoning {
text: text.into(),
provider_metadata: None,
}
}
#[must_use]
pub fn as_text(&self) -> Option<&str> {
match self {
Self::Text { text, .. } => Some(text),
_ => None,
}
}
#[must_use]
pub fn as_tool_call(&self) -> Option<&ToolCall> {
match self {
Self::ToolCall(call) => Some(call),
_ => None,
}
}
#[must_use]
pub fn kind_name(&self) -> &'static str {
match self {
Self::Text { .. } => "text",
Self::Reasoning { .. } => "reasoning",
Self::ReasoningFile { .. } => "reasoning-file",
Self::File { .. } => "file",
Self::Custom { .. } => "custom",
Self::Source(_) => "source",
Self::ToolCall(_) => "tool-call",
Self::ToolResult(_) => "tool-result",
Self::ToolApprovalRequest { .. } => "tool-approval-request",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ToolCall {
pub tool_call_id: ToolCallId,
pub tool_name: ToolName,
pub input: String,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub provider_executed: bool,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub dynamic: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub provider_metadata: Option<ProviderMetadata>,
}
impl ToolCall {
#[must_use]
pub fn new(
tool_call_id: impl Into<ToolCallId>,
tool_name: impl Into<ToolName>,
input: impl Into<String>,
) -> Self {
Self {
tool_call_id: tool_call_id.into(),
tool_name: tool_name.into(),
input: input.into(),
provider_executed: false,
dynamic: false,
provider_metadata: None,
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ProviderToolResult {
pub tool_call_id: ToolCallId,
pub tool_name: ToolName,
pub result: JsonValue,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub is_error: bool,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub preliminary: bool,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub dynamic: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub provider_metadata: Option<ProviderMetadata>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "source_type", rename_all = "kebab-case")]
#[non_exhaustive]
pub enum Source {
Url {
id: String,
url: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
title: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
provider_metadata: Option<ProviderMetadata>,
},
Document {
id: String,
media_type: MediaType,
title: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
filename: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
provider_metadata: Option<ProviderMetadata>,
},
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(try_from = "String", into = "String")]
pub struct CustomKind(String);
impl CustomKind {
pub fn parse(text: impl Into<String>) -> Result<Self, InvalidCustomKind> {
let text = text.into();
match text.split_once('.') {
Some((provider, kind))
if !provider.is_empty()
&& !kind.is_empty()
&& !provider.contains(char::is_whitespace)
&& !kind.contains(char::is_whitespace) =>
{
Ok(Self(text))
}
_ => Err(InvalidCustomKind { text }),
}
}
pub fn new(provider: &str, kind: &str) -> Result<Self, InvalidCustomKind> {
if provider.contains('.') {
return Err(InvalidCustomKind {
text: format!("{provider}.{kind}"),
});
}
Self::parse(format!("{provider}.{kind}"))
}
#[must_use]
pub fn provider(&self) -> &str {
self.0.split_once('.').map_or("", |(provider, _)| provider)
}
#[must_use]
pub fn kind(&self) -> &str {
self.0.split_once('.').map_or("", |(_, kind)| kind)
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl std::fmt::Display for CustomKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
impl TryFrom<String> for CustomKind {
type Error = InvalidCustomKind;
fn try_from(value: String) -> Result<Self, Self::Error> {
Self::parse(value)
}
}
impl From<CustomKind> for String {
fn from(kind: CustomKind) -> Self {
kind.0
}
}
impl AsRef<str> for CustomKind {
fn as_ref(&self) -> &str {
&self.0
}
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[error("invalid custom kind `{text}`: expected `<provider>.<type>`")]
pub struct InvalidCustomKind {
pub text: String,
}