use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Role {
User,
Assistant,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ContentBlock {
Text {
text: String,
#[serde(skip_serializing_if = "Option::is_none")]
cache_control: Option<CacheControl>,
#[serde(skip_serializing_if = "Option::is_none")]
citations: Option<Vec<Citation>>,
},
Image {
source: ImageSource,
#[serde(skip_serializing_if = "Option::is_none")]
cache_control: Option<CacheControl>,
},
Document {
source: DocumentSource,
#[serde(skip_serializing_if = "Option::is_none")]
title: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
context: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
citations: Option<CitationConfig>,
#[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<String>,
#[serde(skip_serializing_if = "Option::is_none")]
is_error: Option<bool>,
},
Thinking {
thinking: String,
#[serde(skip_serializing_if = "Option::is_none")]
signature: Option<String>,
},
RedactedThinking { data: String },
SearchResult {
source: String,
title: String,
content: Vec<TextBlock>,
#[serde(skip_serializing_if = "Option::is_none")]
citations: Option<CitationConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
cache_control: Option<CacheControl>,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TextBlock {
#[serde(rename = "type")]
pub block_type: String, pub text: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ImageSource {
Base64 { media_type: String, data: String },
Url { url: String },
File { file_id: String },
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum DocumentSource {
File { file_id: String },
Text { media_type: String, data: String },
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct CitationConfig {
pub enabled: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Citation {
#[serde(rename = "type")]
pub citation_type: String,
pub source: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
pub cited_text: String,
pub search_result_index: usize,
pub start_block_index: usize,
pub end_block_index: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct CacheControl {
#[serde(rename = "type")]
pub cache_type: CacheType,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum CacheType {
Ephemeral,
}
impl CacheControl {
pub fn ephemeral() -> Self {
Self {
cache_type: CacheType::Ephemeral,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Message {
pub role: Role,
pub content: Vec<ContentBlock>,
}
impl Message {
pub fn user(text: impl Into<String>) -> Self {
Self {
role: Role::User,
content: vec![ContentBlock::Text {
text: text.into(),
cache_control: None,
citations: None,
}],
}
}
pub fn assistant(text: impl Into<String>) -> Self {
Self {
role: Role::Assistant,
content: vec![ContentBlock::Text {
text: text.into(),
cache_control: None,
citations: None,
}],
}
}
pub fn tool_result(tool_use_id: impl Into<String>, content: impl Into<String>) -> Self {
Self {
role: Role::User,
content: vec![ContentBlock::ToolResult {
tool_use_id: tool_use_id.into(),
content: Some(content.into()),
is_error: None,
}],
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum SystemPrompt {
String(String),
Blocks(Vec<SystemBlock>),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SystemBlock {
#[serde(rename = "type")]
pub block_type: String,
pub text: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub cache_control: Option<CacheControl>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Tool {
pub name: String,
pub description: String,
pub input_schema: serde_json::Value,
#[serde(skip_serializing_if = "Option::is_none")]
pub disable_user_input: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub input_examples: Option<Vec<serde_json::Value>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub cache_control: Option<CacheControl>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(tag = "type", rename_all = "lowercase")]
pub enum ToolChoice {
Auto,
Any,
Tool { name: String },
None,
}
impl ToolChoice {
pub fn auto() -> Self {
Self::Auto
}
pub fn any() -> Self {
Self::Any
}
pub fn tool(name: impl Into<String>) -> Self {
Self::Tool { name: name.into() }
}
pub fn none() -> Self {
Self::None
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub struct Usage {
pub input_tokens: u32,
pub output_tokens: u32,
#[serde(skip_serializing_if = "Option::is_none")]
pub cache_creation_input_tokens: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub cache_read_input_tokens: Option<u32>,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub struct ExtendedUsage {
#[serde(flatten)]
pub base: Usage,
#[serde(skip_serializing_if = "Option::is_none")]
pub thinking_tokens: Option<u32>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MessagesRequest {
pub model: String,
pub max_tokens: u32,
pub messages: Vec<Message>,
#[serde(skip_serializing_if = "Option::is_none")]
pub system: Option<SystemPrompt>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tools: Option<Vec<Tool>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_choice: Option<ToolChoice>,
#[serde(skip_serializing_if = "Option::is_none")]
pub disable_parallel_tool_use: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub temperature: Option<f32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub top_p: Option<f32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub top_k: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub stop_sequences: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub stream: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub output_config: Option<OutputConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
pub thinking: Option<ThinkingConfig>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(tag = "type", rename_all = "lowercase")]
pub enum ThinkingConfig {
Enabled {
budget_tokens: u32,
},
Disabled,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct OutputConfig {
#[serde(skip_serializing_if = "Option::is_none")]
pub effort: Option<EffortLevel>,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum EffortLevel {
High,
Medium,
Low,
}
impl MessagesRequest {
pub fn new(model: impl Into<String>, max_tokens: u32, messages: Vec<Message>) -> Self {
Self {
model: model.into(),
max_tokens,
messages,
system: None,
tools: None,
tool_choice: None,
disable_parallel_tool_use: None,
temperature: None,
top_p: None,
top_k: None,
stop_sequences: None,
stream: None,
output_config: None,
thinking: None,
}
}
pub fn with_system(mut self, system: impl Into<String>) -> Self {
self.system = Some(SystemPrompt::String(system.into()));
self
}
pub fn with_tools(mut self, tools: Vec<Tool>) -> Self {
self.tools = Some(tools);
self
}
pub fn with_tool_choice(mut self, choice: ToolChoice) -> Self {
self.tool_choice = Some(choice);
self
}
pub fn with_disable_parallel_tool_use(mut self, disable: bool) -> Self {
self.disable_parallel_tool_use = Some(disable);
self
}
pub fn with_temperature(mut self, temperature: f32) -> Self {
self.temperature = Some(temperature);
self
}
pub fn with_effort(mut self, effort: EffortLevel) -> Self {
self.output_config = Some(OutputConfig {
effort: Some(effort),
});
self
}
pub fn with_thinking(mut self, budget_tokens: u32) -> Self {
self.thinking = Some(ThinkingConfig::Enabled { budget_tokens });
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum StopReason {
EndTurn,
MaxTokens,
StopSequence,
ToolUse,
PauseTurn,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MessagesResponse {
pub id: String,
#[serde(rename = "type")]
pub response_type: String,
pub role: Role,
pub content: Vec<ContentBlock>,
pub model: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub stop_reason: Option<StopReason>,
#[serde(skip_serializing_if = "Option::is_none")]
pub stop_sequence: Option<String>,
pub usage: Usage,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_cache_control_ephemeral() {
let cache = CacheControl::ephemeral();
assert_eq!(cache.cache_type, CacheType::Ephemeral);
}
#[test]
fn test_cache_control_serialization() {
let cache = CacheControl::ephemeral();
let json = serde_json::to_string(&cache).unwrap();
assert_eq!(json, r#"{"type":"ephemeral"}"#);
}
#[test]
fn test_text_content_with_cache() {
let content = ContentBlock::Text {
text: "test".into(),
cache_control: Some(CacheControl::ephemeral()),
citations: None,
};
let json = serde_json::to_value(&content).unwrap();
assert_eq!(json["type"], "text");
assert_eq!(json["text"], "test");
assert_eq!(json["cache_control"]["type"], "ephemeral");
}
#[test]
fn test_system_block_with_cache() {
let block = SystemBlock {
block_type: "text".into(),
text: "You are helpful".into(),
cache_control: Some(CacheControl::ephemeral()),
};
let json = serde_json::to_value(&block).unwrap();
assert_eq!(json["type"], "text");
assert_eq!(json["cache_control"]["type"], "ephemeral");
}
#[test]
fn test_tool_with_cache() {
let tool = Tool {
name: "test".into(),
description: "test tool".into(),
input_schema: serde_json::json!({"type": "object"}),
disable_user_input: Some(true),
input_examples: None,
cache_control: Some(CacheControl::ephemeral()),
};
let json = serde_json::to_value(&tool).unwrap();
assert_eq!(json["name"], "test");
assert_eq!(json["cache_control"]["type"], "ephemeral");
}
#[test]
fn test_message_constructors() {
let user_msg = Message::user("hello");
assert_eq!(user_msg.role, Role::User);
assert_eq!(user_msg.content.len(), 1);
let assistant_msg = Message::assistant("hi");
assert_eq!(assistant_msg.role, Role::Assistant);
let tool_result = Message::tool_result("id_123", "result");
assert_eq!(tool_result.role, Role::User);
match &tool_result.content[0] {
ContentBlock::ToolResult { tool_use_id, .. } => {
assert_eq!(tool_use_id, "id_123");
}
_ => panic!("Expected ToolResult"),
}
}
#[test]
fn test_messages_request_builder() {
let request = MessagesRequest::new(
"claude-sonnet-4-5-20250929",
1024,
vec![Message::user("test")],
)
.with_system("System prompt")
.with_temperature(0.7);
assert_eq!(request.model, "claude-sonnet-4-5-20250929");
assert_eq!(request.max_tokens, 1024);
assert_eq!(request.messages.len(), 1);
assert!(request.system.is_some());
assert_eq!(request.temperature, Some(0.7));
}
}