use serde::{Deserialize, Serialize};
use crate::types::cache::CacheControl;
use crate::types::tagged_enum;
use crate::types::tool::ToolResultContent;
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
pub struct TextBlock {
pub text: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub citations: Option<Vec<serde_json::Value>>,
}
impl TextBlock {
pub fn new(text: impl Into<String>) -> Self {
Self {
text: text.into(),
citations: None,
}
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct ToolUseBlock {
pub id: String,
pub name: String,
pub input: serde_json::Value,
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct ThinkingBlock {
pub thinking: String,
pub signature: String,
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct RedactedThinkingBlock {
pub data: String,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct WebSearchToolResultBlock {
pub tool_use_id: String,
pub content: serde_json::Value,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct FallbackRef {
pub model: String,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct FallbackBlock {
pub from: FallbackRef,
pub to: FallbackRef,
}
tagged_enum! {
pub enum ContentBlock {
Text(TextBlock) = "text",
ToolUse(ToolUseBlock) = "tool_use",
Thinking(ThinkingBlock) = "thinking",
RedactedThinking(RedactedThinkingBlock) = "redacted_thinking",
ServerToolUse(ToolUseBlock) = "server_tool_use",
WebSearchToolResult(WebSearchToolResultBlock) = "web_search_tool_result",
Fallback(FallbackBlock) = "fallback",
}
}
impl ContentBlock {
pub fn as_text(&self) -> Option<&str> {
match self {
ContentBlock::Text(t) => Some(&t.text),
_ => None,
}
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct TextBlockParam {
pub text: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cache_control: Option<CacheControl>,
}
impl TextBlockParam {
pub fn new(text: impl Into<String>) -> Self {
Self {
text: text.into(),
cache_control: None,
}
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct ImageBlockParam {
pub source: ImageSource,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cache_control: Option<CacheControl>,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct DocumentBlockParam {
pub source: DocumentSource,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub context: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub citations: Option<CitationsConfig>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cache_control: Option<CacheControl>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct CitationsConfig {
pub enabled: bool,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct ToolResultBlockParam {
pub tool_use_id: String,
pub content: ToolResultContent,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub is_error: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cache_control: Option<CacheControl>,
}
impl ToolResultBlockParam {
pub fn new(tool_use_id: impl Into<String>, content: impl Into<ToolResultContent>) -> Self {
Self {
tool_use_id: tool_use_id.into(),
content: content.into(),
is_error: None,
cache_control: None,
}
}
pub fn error(tool_use_id: impl Into<String>, content: impl Into<ToolResultContent>) -> Self {
Self {
tool_use_id: tool_use_id.into(),
content: content.into(),
is_error: Some(true),
cache_control: None,
}
}
}
tagged_enum! {
pub enum ContentBlockParam {
Text(TextBlockParam) = "text",
Image(ImageBlockParam) = "image",
Document(DocumentBlockParam) = "document",
ToolUse(ToolUseBlock) = "tool_use",
ToolResult(ToolResultBlockParam) = "tool_result",
Thinking(ThinkingBlock) = "thinking",
RedactedThinking(RedactedThinkingBlock) = "redacted_thinking",
}
}
impl ContentBlockParam {
pub fn text(text: impl Into<String>) -> Self {
ContentBlockParam::Text(TextBlockParam::new(text))
}
pub fn tool_result(
tool_use_id: impl Into<String>,
content: impl Into<ToolResultContent>,
) -> Self {
ContentBlockParam::ToolResult(ToolResultBlockParam::new(tool_use_id, content))
}
}
impl From<ContentBlock> for ContentBlockParam {
fn from(block: ContentBlock) -> Self {
match block {
ContentBlock::Text(text) => ContentBlockParam::Text(TextBlockParam {
text: text.text,
cache_control: None,
}),
ContentBlock::ToolUse(inner) => ContentBlockParam::ToolUse(inner),
ContentBlock::Thinking(inner) => ContentBlockParam::Thinking(inner),
ContentBlock::RedactedThinking(inner) => ContentBlockParam::RedactedThinking(inner),
other => {
let value = serde_json::to_value(&other).unwrap_or(serde_json::Value::Null);
ContentBlockParam::Unknown(value)
}
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Base64Source {
pub media_type: String,
pub data: String,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct UrlSource {
pub url: String,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct FileSource {
pub file_id: String,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct PlainTextSource {
pub media_type: String,
pub data: String,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct ContentSource {
pub content: Vec<ContentBlockParam>,
}
tagged_enum! {
pub enum ImageSource {
Base64(Base64Source) = "base64",
Url(UrlSource) = "url",
File(FileSource) = "file",
}
}
tagged_enum! {
pub enum DocumentSource {
Base64(Base64Source) = "base64",
Text(PlainTextSource) = "text",
Content(ContentSource) = "content",
Url(UrlSource) = "url",
File(FileSource) = "file",
}
}
#[cfg(test)]
mod tests {
use super::*;
fn rt<T>(json: serde_json::Value) -> serde_json::Value
where
T: serde::Serialize + serde::de::DeserializeOwned + PartialEq + std::fmt::Debug,
{
let parsed: T = serde_json::from_value(json).expect("deserialize");
let out = serde_json::to_value(&parsed).expect("serialize");
let reparsed: T = serde_json::from_value(out.clone()).expect("re-deserialize");
assert_eq!(parsed, reparsed, "struct round-trip mismatch");
out
}
#[test]
fn response_text_block() {
let j = serde_json::json!({"type": "text", "text": "Hello"});
assert_eq!(rt::<ContentBlock>(j.clone()), j);
}
#[test]
fn response_tool_use_block() {
let j = serde_json::json!({
"type": "tool_use",
"id": "toolu_01",
"name": "get_weather",
"input": {"location": "San Francisco"}
});
assert_eq!(rt::<ContentBlock>(j.clone()), j);
}
#[test]
fn response_thinking_block() {
let j = serde_json::json!({
"type": "thinking",
"thinking": "Let me reason about this.",
"signature": "c2ln"
});
assert_eq!(rt::<ContentBlock>(j.clone()), j);
}
#[test]
fn response_redacted_thinking_block() {
let j = serde_json::json!({"type": "redacted_thinking", "data": "encrypted"});
assert_eq!(rt::<ContentBlock>(j.clone()), j);
}
#[test]
fn response_server_tool_use_block() {
let j = serde_json::json!({
"type": "server_tool_use",
"id": "srvtoolu_1",
"name": "web_search",
"input": {"query": "rust sdk"}
});
assert_eq!(rt::<ContentBlock>(j.clone()), j);
}
#[test]
fn response_web_search_tool_result_block() {
let j = serde_json::json!({
"type": "web_search_tool_result",
"tool_use_id": "srvtoolu_1",
"content": [{"type": "web_search_result", "title": "x", "url": "https://x"}]
});
assert_eq!(rt::<ContentBlock>(j.clone()), j);
}
#[test]
fn response_fallback_block() {
let j = serde_json::json!({
"type": "fallback",
"from": {"model": "claude-fable-5"},
"to": {"model": "claude-opus-4-8"}
});
assert_eq!(rt::<ContentBlock>(j.clone()), j);
}
#[test]
fn unknown_response_block_is_preserved() {
let j = serde_json::json!({"type": "brand_new_block", "foo": [1, 2, 3], "bar": "x"});
let parsed: ContentBlock = serde_json::from_value(j.clone()).unwrap();
assert!(matches!(parsed, ContentBlock::Unknown(_)));
assert_eq!(serde_json::to_value(&parsed).unwrap(), j);
}
#[test]
fn object_without_type_falls_through_to_unknown() {
let j = serde_json::json!({"no_type_here": true});
let parsed: ContentBlock = serde_json::from_value(j.clone()).unwrap();
assert!(matches!(parsed, ContentBlock::Unknown(_)));
assert_eq!(serde_json::to_value(&parsed).unwrap(), j);
}
#[test]
fn request_text_block_with_cache_control() {
let j = serde_json::json!({
"type": "text",
"text": "cache me",
"cache_control": {"type": "ephemeral"}
});
assert_eq!(rt::<ContentBlockParam>(j.clone()), j);
}
#[test]
fn request_image_base64_block() {
let j = serde_json::json!({
"type": "image",
"source": {"type": "base64", "media_type": "image/png", "data": "aGVsbG8="}
});
assert_eq!(rt::<ContentBlockParam>(j.clone()), j);
}
#[test]
fn request_document_url_block() {
let j = serde_json::json!({
"type": "document",
"source": {"type": "url", "url": "https://example.com/a.pdf"},
"title": "A PDF",
"citations": {"enabled": true}
});
assert_eq!(rt::<ContentBlockParam>(j.clone()), j);
}
#[test]
fn request_document_content_block() {
let j = serde_json::json!({
"type": "document",
"source": {"type": "content", "content": [{"type": "text", "text": "nested"}]}
});
assert_eq!(rt::<ContentBlockParam>(j.clone()), j);
}
#[test]
fn request_tool_result_string_block() {
let j = serde_json::json!({
"type": "tool_result",
"tool_use_id": "toolu_01",
"content": "72F and sunny"
});
assert_eq!(rt::<ContentBlockParam>(j.clone()), j);
}
#[test]
fn request_tool_result_blocks_with_error() {
let j = serde_json::json!({
"type": "tool_result",
"tool_use_id": "toolu_01",
"content": [{"type": "text", "text": "boom"}],
"is_error": true
});
assert_eq!(rt::<ContentBlockParam>(j.clone()), j);
}
#[test]
fn content_block_into_param_maps_and_preserves() {
let text = ContentBlock::Text(TextBlock {
text: "hi".into(),
citations: Some(vec![serde_json::json!({"cited_text": "x"})]),
});
assert_eq!(
ContentBlockParam::from(text),
ContentBlockParam::Text(TextBlockParam::new("hi"))
);
let call = ToolUseBlock {
id: "toolu_1".into(),
name: "get_weather".into(),
input: serde_json::json!({"location": "Paris"}),
};
assert_eq!(
ContentBlockParam::from(ContentBlock::ToolUse(call.clone())),
ContentBlockParam::ToolUse(call)
);
let stu = ContentBlock::ServerToolUse(ToolUseBlock {
id: "srvtoolu_1".into(),
name: "web_search".into(),
input: serde_json::json!({"query": "rust"}),
});
match ContentBlockParam::from(stu) {
ContentBlockParam::Unknown(value) => {
assert_eq!(value["type"], serde_json::json!("server_tool_use"));
assert_eq!(value["name"], serde_json::json!("web_search"));
}
other => panic!("expected Unknown, got {other:?}"),
}
}
#[test]
fn unknown_request_block_is_preserved() {
let j = serde_json::json!({
"type": "server_tool_use",
"id": "srvtoolu_1",
"name": "web_search",
"input": {"query": "x"}
});
let parsed: ContentBlockParam = serde_json::from_value(j.clone()).unwrap();
assert!(matches!(parsed, ContentBlockParam::Unknown(_)));
assert_eq!(serde_json::to_value(&parsed).unwrap(), j);
}
}