use serde::Deserialize;
use serde::Serialize;
use super::common::CacheControl;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(untagged)]
pub enum ToolResultContent {
String(String),
Blocks(Vec<ToolResultContentBlock>),
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ToolResultContentBlock {
Text {
text: String,
#[serde(skip_serializing_if = "Option::is_none")]
cache_control: Option<CacheControl>,
},
Image {
source: ImageSource,
#[serde(skip_serializing_if = "Option::is_none")]
cache_control: Option<CacheControl>,
},
}
impl From<&str> for ToolResultContent {
fn from(s: &str) -> Self {
Self::String(s.to_string())
}
}
impl From<String> for ToolResultContent {
fn from(s: String) -> Self {
Self::String(s)
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ImageSource {
Base64 {
media_type: String,
data: String,
},
Url {
url: String,
},
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum DocumentSource {
Base64 {
media_type: String,
data: String,
},
Url {
url: String,
},
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum MessageRole {
User,
Assistant,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(tag = "type", rename_all = "snake_case")]
#[non_exhaustive]
pub enum ContentBlockParam {
Text {
text: String,
#[serde(skip_serializing_if = "Option::is_none")]
citations: Option<Vec<serde_json::Value>>,
#[serde(skip_serializing_if = "Option::is_none")]
cache_control: Option<CacheControl>,
},
Image {
source: ImageSource,
#[serde(skip_serializing_if = "Option::is_none")]
cache_control: Option<CacheControl>,
},
Document {
source: DocumentSource,
#[serde(skip_serializing_if = "Option::is_none")]
cache_control: Option<CacheControl>,
},
ToolUse {
id: String,
name: String,
input: serde_json::Value,
#[serde(skip_serializing_if = "Option::is_none")]
cache_control: Option<CacheControl>,
},
ToolResult {
tool_use_id: String,
#[serde(skip_serializing_if = "Option::is_none")]
content: Option<ToolResultContent>,
#[serde(skip_serializing_if = "Option::is_none")]
is_error: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
cache_control: Option<CacheControl>,
},
Thinking {
thinking: String,
signature: String,
},
RedactedThinking {
data: String,
},
ServerToolUse {
id: String,
name: String,
input: serde_json::Value,
#[serde(skip_serializing_if = "Option::is_none")]
cache_control: Option<CacheControl>,
},
SearchResult {
content: Vec<serde_json::Value>,
source: String,
title: String,
#[serde(skip_serializing_if = "Option::is_none")]
citations: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
cache_control: Option<CacheControl>,
},
WebSearchToolResult {
tool_use_id: String,
content: serde_json::Value,
#[serde(skip_serializing_if = "Option::is_none")]
cache_control: Option<CacheControl>,
},
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(tag = "type", rename_all = "snake_case")]
#[non_exhaustive]
pub enum ContentBlock {
Text {
text: String,
#[serde(skip_serializing_if = "Option::is_none")]
citations: Option<Vec<serde_json::Value>>,
},
ToolUse {
id: String,
name: String,
input: serde_json::Value,
},
Thinking {
thinking: String,
signature: String,
},
RedactedThinking {
data: String,
},
ServerToolUse {
id: String,
name: String,
input: serde_json::Value,
},
WebSearchToolResult {
tool_use_id: String,
content: serde_json::Value,
},
#[serde(other)]
Unknown,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(untagged)]
pub enum SystemParam {
String(String),
Blocks(Vec<TextBlockParam>),
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(untagged)]
pub enum MessageContentParam {
String(String),
Blocks(Vec<ContentBlockParam>),
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct TextBlockParam {
pub text: String,
#[serde(
rename = "type",
default = "text_type",
skip_serializing_if = "is_text"
)]
pub kind: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub cache_control: Option<CacheControl>,
}
fn text_type() -> String {
"text".to_string()
}
fn is_text(s: &str) -> bool {
s == "text"
}
impl TextBlockParam {
#[must_use]
pub fn new(text: impl Into<String>) -> Self {
Self {
text: text.into(),
kind: "text".to_string(),
cache_control: None,
}
}
#[must_use]
pub fn with_cache_control(text: impl Into<String>, cache_control: CacheControl) -> Self {
Self {
text: text.into(),
kind: "text".to_string(),
cache_control: Some(cache_control),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct MessageParam {
pub role: MessageRole,
pub content: MessageContentParam,
}
impl From<&str> for MessageContentParam {
fn from(s: &str) -> Self {
Self::String(s.to_string())
}
}
impl From<String> for MessageContentParam {
fn from(s: String) -> Self {
Self::String(s)
}
}
impl From<&str> for SystemParam {
fn from(s: &str) -> Self {
Self::String(s.to_string())
}
}
impl From<String> for SystemParam {
fn from(s: String) -> Self {
Self::String(s)
}
}
#[derive(Debug, Clone, thiserror::Error)]
pub enum ContentBlockConversionError {
#[error("cannot convert unknown content block to request content block param")]
UnknownContentBlock,
}
impl TryFrom<&ContentBlock> for ContentBlockParam {
type Error = ContentBlockConversionError;
fn try_from(block: &ContentBlock) -> Result<Self, Self::Error> {
match block {
ContentBlock::Text { text, citations } => Ok(Self::Text {
text: text.clone(),
citations: citations.clone(),
cache_control: None,
}),
ContentBlock::ToolUse { id, name, input } => Ok(Self::ToolUse {
id: id.clone(),
name: name.clone(),
input: input.clone(),
cache_control: None,
}),
ContentBlock::Thinking {
thinking,
signature,
} => Ok(Self::Thinking {
thinking: thinking.clone(),
signature: signature.clone(),
}),
ContentBlock::RedactedThinking { data } => {
Ok(Self::RedactedThinking { data: data.clone() })
}
ContentBlock::ServerToolUse { id, name, input } => Ok(Self::ServerToolUse {
id: id.clone(),
name: name.clone(),
input: input.clone(),
cache_control: None,
}),
ContentBlock::WebSearchToolResult {
tool_use_id,
content,
} => Ok(Self::WebSearchToolResult {
tool_use_id: tool_use_id.clone(),
content: content.clone(),
cache_control: None,
}),
ContentBlock::Unknown => Err(ContentBlockConversionError::UnknownContentBlock),
}
}
}
impl TryFrom<ContentBlock> for ContentBlockParam {
type Error = ContentBlockConversionError;
fn try_from(block: ContentBlock) -> Result<Self, Self::Error> {
Self::try_from(&block)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn message_role_ser() {
assert_eq!(
serde_json::to_string(&MessageRole::User).unwrap(),
r#""user""#
);
assert_eq!(
serde_json::to_string(&MessageRole::Assistant).unwrap(),
r#""assistant""#
);
}
#[test]
fn content_block_param_text_ser() {
let cb = ContentBlockParam::Text {
text: "hello".into(),
citations: None,
cache_control: None,
};
let s = serde_json::to_string(&cb).unwrap();
assert!(s.contains(r#""type":"text""#));
assert!(s.contains(r#""text":"hello""#));
assert!(!s.contains("cache_control"));
assert!(!s.contains("citations"));
}
#[test]
fn content_block_response_text_ser() {
let cb = ContentBlock::Text {
text: "response".into(),
citations: None,
};
let s = serde_json::to_string(&cb).unwrap();
assert!(s.contains(r#""type":"text""#));
assert!(s.contains(r#""text":"response""#));
assert!(!s.contains("citations"));
}
#[test]
fn content_block_unknown_deser() {
let json = r#"{"type":"future_block","foo":"bar"}"#;
let block: ContentBlock = serde_json::from_str(json).unwrap();
assert!(matches!(block, ContentBlock::Unknown));
}
#[test]
fn content_block_param_tool_use_ser() {
let cb = ContentBlockParam::ToolUse {
id: "toolu_123".into(),
name: "get_weather".into(),
input: serde_json::json!({"city": "Paris"}),
cache_control: None,
};
let s = serde_json::to_string(&cb).unwrap();
assert!(s.contains(r#""type":"tool_use""#));
assert!(s.contains(r#""id":"toolu_123""#));
assert!(s.contains(r#""name":"get_weather""#));
}
#[test]
fn content_block_thinking_ser_deser() {
let cb = ContentBlock::Thinking {
thinking: "Let me analyze this...".into(),
signature: "sig_abc123".into(),
};
let s = serde_json::to_string(&cb).unwrap();
assert!(s.contains(r#""type":"thinking""#));
assert!(s.contains(r#""thinking":"Let me analyze this...""#));
assert!(s.contains(r#""signature":"sig_abc123""#));
let parsed: ContentBlock = serde_json::from_str(&s).unwrap();
match parsed {
ContentBlock::Thinking {
thinking,
signature,
} => {
assert_eq!(thinking, "Let me analyze this...");
assert_eq!(signature, "sig_abc123");
}
_ => panic!("Expected Thinking variant"),
}
}
#[test]
fn content_block_param_thinking_ser() {
let cb = ContentBlockParam::Thinking {
thinking: "Analyzing...".into(),
signature: "sig_xyz".into(),
};
let s = serde_json::to_string(&cb).unwrap();
assert!(s.contains(r#""type":"thinking""#));
assert!(s.contains(r#""thinking":"Analyzing...""#));
assert!(s.contains(r#""signature":"sig_xyz""#));
}
#[test]
fn content_block_redacted_thinking_deser() {
let json = r#"{"type":"redacted_thinking","data":"redacted_data_here"}"#;
let block: ContentBlock = serde_json::from_str(json).unwrap();
match block {
ContentBlock::RedactedThinking { data } => {
assert_eq!(data, "redacted_data_here");
}
_ => panic!("Expected RedactedThinking variant"),
}
}
#[test]
fn content_block_server_tool_use_deser() {
let json = r#"{"type":"server_tool_use","id":"tool_123","name":"web_search","input":{"query":"rust"}}"#;
let block: ContentBlock = serde_json::from_str(json).unwrap();
match block {
ContentBlock::ServerToolUse { id, name, input } => {
assert_eq!(id, "tool_123");
assert_eq!(name, "web_search");
assert_eq!(input["query"], "rust");
}
_ => panic!("Expected ServerToolUse variant"),
}
}
#[test]
fn content_block_web_search_result_deser() {
let json = r#"{"type":"web_search_tool_result","tool_use_id":"tool_123","content":{"results":[]}}"#;
let block: ContentBlock = serde_json::from_str(json).unwrap();
match block {
ContentBlock::WebSearchToolResult {
tool_use_id,
content,
} => {
assert_eq!(tool_use_id, "tool_123");
assert!(content.is_object());
}
_ => panic!("Expected WebSearchToolResult variant"),
}
}
#[test]
fn system_param_string() {
let sys: SystemParam = "You are helpful".into();
let s = serde_json::to_string(&sys).unwrap();
assert_eq!(s, r#""You are helpful""#);
}
#[test]
fn system_param_blocks() {
let sys = SystemParam::Blocks(vec![TextBlockParam::new("test")]);
let s = serde_json::to_string(&sys).unwrap();
assert!(s.contains(r#""text":"test""#));
}
#[test]
fn message_content_param_string() {
let content: MessageContentParam = "hello".into();
let s = serde_json::to_string(&content).unwrap();
assert_eq!(s, r#""hello""#);
}
#[test]
fn message_content_param_blocks() {
let content = MessageContentParam::Blocks(vec![ContentBlockParam::Text {
text: "test".into(),
citations: None,
cache_control: None,
}]);
let s = serde_json::to_string(&content).unwrap();
assert!(s.contains(r#""type":"text""#));
assert!(s.contains(r#""text":"test""#));
}
#[test]
fn text_block_param_with_cache() {
let tb = TextBlockParam::with_cache_control("cached", CacheControl::ephemeral_1h());
let s = serde_json::to_string(&tb).unwrap();
assert!(s.contains(r#""text":"cached""#));
assert!(s.contains(r#""cache_control""#));
}
#[test]
fn content_block_param_search_result_roundtrip() {
let cb = ContentBlockParam::SearchResult {
content: vec![serde_json::json!({"text": "search result content"})],
source: "https://example.com".into(),
title: "Example Result".into(),
citations: Some(serde_json::json!({"citation": "data"})),
cache_control: None,
};
let s = serde_json::to_string(&cb).unwrap();
assert!(s.contains(r#""type":"search_result""#));
assert!(s.contains(r#""source":"https://example.com""#));
assert!(s.contains(r#""title":"Example Result""#));
let parsed: ContentBlockParam = serde_json::from_str(&s).unwrap();
match parsed {
ContentBlockParam::SearchResult {
source,
title,
content,
..
} => {
assert_eq!(source, "https://example.com");
assert_eq!(title, "Example Result");
assert_eq!(content.len(), 1);
}
_ => panic!("Expected SearchResult variant"),
}
}
#[test]
fn content_block_param_web_search_tool_result_roundtrip() {
let cb = ContentBlockParam::WebSearchToolResult {
tool_use_id: "toolu_abc".into(),
content: serde_json::json!({"results": [{"url": "https://example.com"}]}),
cache_control: None,
};
let s = serde_json::to_string(&cb).unwrap();
assert!(s.contains(r#""type":"web_search_tool_result""#));
assert!(s.contains(r#""tool_use_id":"toolu_abc""#));
let parsed: ContentBlockParam = serde_json::from_str(&s).unwrap();
match parsed {
ContentBlockParam::WebSearchToolResult {
tool_use_id,
content,
..
} => {
assert_eq!(tool_use_id, "toolu_abc");
assert!(content["results"].is_array());
}
_ => panic!("Expected WebSearchToolResult variant"),
}
}
#[test]
fn tryfrom_content_block_text_to_param() {
let block = ContentBlock::Text {
text: "hello world".into(),
citations: Some(vec![serde_json::json!({"url": "https://example.com"})]),
};
let param = ContentBlockParam::try_from(&block).unwrap();
match param {
ContentBlockParam::Text {
text,
citations,
cache_control,
} => {
assert_eq!(text, "hello world");
assert!(citations.is_some());
assert!(cache_control.is_none());
}
_ => panic!("Expected Text variant"),
}
}
#[test]
fn tryfrom_content_block_tool_use_to_param() {
let block = ContentBlock::ToolUse {
id: "toolu_test".into(),
name: "get_weather".into(),
input: serde_json::json!({"city": "Paris"}),
};
let param = ContentBlockParam::try_from(&block).unwrap();
match param {
ContentBlockParam::ToolUse {
id,
name,
input,
cache_control,
} => {
assert_eq!(id, "toolu_test");
assert_eq!(name, "get_weather");
assert_eq!(input["city"], "Paris");
assert!(cache_control.is_none());
}
_ => panic!("Expected ToolUse variant"),
}
}
#[test]
fn tryfrom_content_block_thinking_to_param() {
let block = ContentBlock::Thinking {
thinking: "Let me think...".into(),
signature: "sig_123".into(),
};
let param = ContentBlockParam::try_from(&block).unwrap();
match param {
ContentBlockParam::Thinking {
thinking,
signature,
} => {
assert_eq!(thinking, "Let me think...");
assert_eq!(signature, "sig_123");
}
_ => panic!("Expected Thinking variant"),
}
}
#[test]
fn tryfrom_content_block_redacted_thinking_to_param() {
let block = ContentBlock::RedactedThinking {
data: "redacted".into(),
};
let param = ContentBlockParam::try_from(&block).unwrap();
match param {
ContentBlockParam::RedactedThinking { data } => {
assert_eq!(data, "redacted");
}
_ => panic!("Expected RedactedThinking variant"),
}
}
#[test]
fn tryfrom_content_block_server_tool_use_to_param() {
let block = ContentBlock::ServerToolUse {
id: "srv_123".into(),
name: "web_search".into(),
input: serde_json::json!({"query": "rust"}),
};
let param = ContentBlockParam::try_from(&block).unwrap();
match param {
ContentBlockParam::ServerToolUse {
id,
name,
input,
cache_control,
} => {
assert_eq!(id, "srv_123");
assert_eq!(name, "web_search");
assert_eq!(input["query"], "rust");
assert!(cache_control.is_none());
}
_ => panic!("Expected ServerToolUse variant"),
}
}
#[test]
fn tryfrom_content_block_web_search_result_to_param() {
let block = ContentBlock::WebSearchToolResult {
tool_use_id: "tool_456".into(),
content: serde_json::json!({"results": []}),
};
let param = ContentBlockParam::try_from(&block).unwrap();
match param {
ContentBlockParam::WebSearchToolResult {
tool_use_id,
content,
cache_control,
} => {
assert_eq!(tool_use_id, "tool_456");
assert!(content["results"].is_array());
assert!(cache_control.is_none());
}
_ => panic!("Expected WebSearchToolResult variant"),
}
}
#[test]
fn tryfrom_content_block_unknown_fails() {
let block = ContentBlock::Unknown;
let result = ContentBlockParam::try_from(&block);
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
ContentBlockConversionError::UnknownContentBlock
));
}
#[test]
fn tryfrom_content_block_owned_works() {
let block = ContentBlock::Text {
text: "owned".into(),
citations: None,
};
let param = ContentBlockParam::try_from(block).unwrap();
match param {
ContentBlockParam::Text { text, .. } => {
assert_eq!(text, "owned");
}
_ => panic!("Expected Text variant"),
}
}
}