use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Container {
pub id: String,
pub expires_at: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Role {
User,
Assistant,
}
#[derive(Debug, Clone, Serialize)]
#[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")]
caller: Option<String>,
#[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>,
},
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>,
},
ServerToolUse {
id: String,
name: String,
input: 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>,
},
CodeExecutionToolResult {
tool_use_id: String,
content: serde_json::Value,
#[serde(skip_serializing_if = "Option::is_none")]
cache_control: Option<CacheControl>,
},
ContainerUpload {
file_id: String,
#[serde(skip_serializing_if = "Option::is_none")]
cache_control: Option<CacheControl>,
},
MidConvSystem {
content: Vec<TextBlock>,
#[serde(skip_serializing_if = "Option::is_none")]
cache_control: Option<CacheControl>,
},
#[serde(untagged)]
Unknown {
block_type: String,
data: serde_json::Value,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum ToolResultContent {
Text(String),
Blocks(Vec<ContentBlock>),
}
#[derive(Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
enum ContentBlockHelper {
Text {
text: String,
cache_control: Option<CacheControl>,
citations: Option<Vec<Citation>>,
},
Image {
source: ImageSource,
cache_control: Option<CacheControl>,
},
Document {
source: DocumentSource,
title: Option<String>,
context: Option<String>,
citations: Option<CitationConfig>,
cache_control: Option<CacheControl>,
},
ToolUse {
id: String,
name: String,
input: serde_json::Value,
caller: Option<String>,
cache_control: Option<CacheControl>,
},
ToolResult {
tool_use_id: String,
content: Option<ToolResultContent>,
is_error: Option<bool>,
},
Thinking {
thinking: String,
signature: Option<String>,
},
RedactedThinking {
data: String,
},
SearchResult {
source: String,
title: String,
content: Vec<TextBlock>,
citations: Option<CitationConfig>,
cache_control: Option<CacheControl>,
},
ServerToolUse {
id: String,
name: String,
input: serde_json::Value,
cache_control: Option<CacheControl>,
},
WebSearchToolResult {
tool_use_id: String,
content: serde_json::Value,
cache_control: Option<CacheControl>,
},
CodeExecutionToolResult {
tool_use_id: String,
content: serde_json::Value,
cache_control: Option<CacheControl>,
},
ContainerUpload {
file_id: String,
cache_control: Option<CacheControl>,
},
MidConvSystem {
content: Vec<TextBlock>,
cache_control: Option<CacheControl>,
},
}
impl<'de> serde::Deserialize<'de> for ContentBlock {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let value = serde_json::Value::deserialize(deserializer)?;
match serde_json::from_value::<ContentBlockHelper>(value.clone()) {
Ok(helper) => Ok(match helper {
ContentBlockHelper::Text {
text,
cache_control,
citations,
} => ContentBlock::Text {
text,
cache_control,
citations,
},
ContentBlockHelper::Image {
source,
cache_control,
} => ContentBlock::Image {
source,
cache_control,
},
ContentBlockHelper::Document {
source,
title,
context,
citations,
cache_control,
} => ContentBlock::Document {
source,
title,
context,
citations,
cache_control,
},
ContentBlockHelper::ToolUse {
id,
name,
input,
caller,
cache_control,
} => ContentBlock::ToolUse {
id,
name,
input,
caller,
cache_control,
},
ContentBlockHelper::ToolResult {
tool_use_id,
content,
is_error,
} => ContentBlock::ToolResult {
tool_use_id,
content,
is_error,
},
ContentBlockHelper::Thinking {
thinking,
signature,
} => ContentBlock::Thinking {
thinking,
signature,
},
ContentBlockHelper::RedactedThinking { data } => {
ContentBlock::RedactedThinking { data }
}
ContentBlockHelper::SearchResult {
source,
title,
content,
citations,
cache_control,
} => ContentBlock::SearchResult {
source,
title,
content,
citations,
cache_control,
},
ContentBlockHelper::ServerToolUse {
id,
name,
input,
cache_control,
} => ContentBlock::ServerToolUse {
id,
name,
input,
cache_control,
},
ContentBlockHelper::WebSearchToolResult {
tool_use_id,
content,
cache_control,
} => ContentBlock::WebSearchToolResult {
tool_use_id,
content,
cache_control,
},
ContentBlockHelper::CodeExecutionToolResult {
tool_use_id,
content,
cache_control,
} => ContentBlock::CodeExecutionToolResult {
tool_use_id,
content,
cache_control,
},
ContentBlockHelper::ContainerUpload {
file_id,
cache_control,
} => ContentBlock::ContainerUpload {
file_id,
cache_control,
},
ContentBlockHelper::MidConvSystem {
content,
cache_control,
} => ContentBlock::MidConvSystem {
content,
cache_control,
},
}),
Err(_) => {
let block_type = value
.get("type")
.and_then(|t| t.as_str())
.unwrap_or("unknown")
.to_string();
Ok(ContentBlock::Unknown {
block_type,
data: value,
})
}
}
}
}
#[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,
#[serde(skip_serializing_if = "Option::is_none")]
pub ttl: Option<CacheTtl>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum CacheType {
Ephemeral,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum CacheTtl {
#[serde(rename = "5m")]
FiveMinutes,
#[serde(rename = "1h")]
OneHour,
}
impl CacheControl {
pub fn ephemeral() -> Self {
Self {
cache_type: CacheType::Ephemeral,
ttl: None,
}
}
pub fn ephemeral_with_ttl(ttl: CacheTtl) -> Self {
Self {
cache_type: CacheType::Ephemeral,
ttl: Some(ttl),
}
}
}
#[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(ToolResultContent::Text(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 CustomTool {
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>,
#[serde(skip_serializing_if = "Option::is_none")]
pub defer_loading: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub eager_input_streaming: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub strict: Option<bool>,
}
impl CustomTool {
pub fn new(
name: impl Into<String>,
description: impl Into<String>,
input_schema: serde_json::Value,
) -> Self {
Self {
name: name.into(),
description: description.into(),
input_schema,
disable_user_input: None,
input_examples: None,
cache_control: None,
defer_loading: None,
eager_input_streaming: None,
strict: None,
}
}
pub fn programmatic(mut self) -> Self {
self.disable_user_input = Some(true);
self
}
pub fn with_strict(mut self) -> Self {
self.strict = Some(true);
self
}
}
#[deprecated(
since = "2.0.0",
note = "Renamed to CustomTool. Use CustomTool directly."
)]
pub type Tool = CustomTool;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum ToolDefinition {
Custom(CustomTool),
Server(serde_json::Value),
}
impl From<CustomTool> for ToolDefinition {
fn from(tool: CustomTool) -> Self {
ToolDefinition::Custom(tool)
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(tag = "type", rename_all = "lowercase")]
pub enum ToolChoice {
Auto {
#[serde(skip_serializing_if = "Option::is_none")]
disable_parallel_tool_use: Option<bool>,
},
Any {
#[serde(skip_serializing_if = "Option::is_none")]
disable_parallel_tool_use: Option<bool>,
},
Tool {
name: String,
#[serde(skip_serializing_if = "Option::is_none")]
disable_parallel_tool_use: Option<bool>,
},
None,
}
impl ToolChoice {
pub fn auto() -> Self {
Self::Auto {
disable_parallel_tool_use: None,
}
}
pub fn any() -> Self {
Self::Any {
disable_parallel_tool_use: None,
}
}
pub fn tool(name: impl Into<String>) -> Self {
Self::Tool {
name: name.into(),
disable_parallel_tool_use: None,
}
}
pub fn none() -> Self {
Self::None
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub struct OutputTokensDetails {
#[serde(skip_serializing_if = "Option::is_none")]
pub thinking_tokens: Option<u32>,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub struct ServerToolUsage {
#[serde(skip_serializing_if = "Option::is_none")]
pub web_search_requests: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub web_fetch_requests: Option<u32>,
}
#[derive(Debug, Clone, 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>,
#[serde(skip_serializing_if = "Option::is_none")]
pub output_tokens_details: Option<OutputTokensDetails>,
#[serde(skip_serializing_if = "Option::is_none")]
pub server_tool_use: Option<ServerToolUsage>,
#[serde(skip_serializing_if = "Option::is_none")]
pub service_tier: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub inference_geo: Option<String>,
}
#[derive(Debug, Clone, 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<ToolDefinition>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_choice: Option<ToolChoice>,
#[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>,
#[serde(skip_serializing_if = "Option::is_none")]
pub metadata: Option<Metadata>,
#[serde(skip_serializing_if = "Option::is_none")]
pub service_tier: Option<ServiceTier>,
#[serde(skip_serializing_if = "Option::is_none")]
pub inference_geo: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub container: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(tag = "type", rename_all = "lowercase")]
pub enum ThinkingConfig {
Enabled {
budget_tokens: u32,
#[serde(skip_serializing_if = "Option::is_none")]
display: Option<ThinkingDisplay>,
},
Disabled,
Adaptive {
#[serde(skip_serializing_if = "Option::is_none")]
display: Option<ThinkingDisplay>,
},
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum ThinkingDisplay {
Summarized,
Omitted,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct OutputConfig {
#[serde(skip_serializing_if = "Option::is_none")]
pub effort: Option<EffortLevel>,
#[serde(skip_serializing_if = "Option::is_none")]
pub format: Option<OutputFormat>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct OutputFormat {
#[serde(rename = "type")]
pub format_type: String,
pub schema: serde_json::Value,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum EffortLevel {
High,
Medium,
Low,
#[serde(rename = "xhigh")]
XHigh,
Max,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Metadata {
#[serde(skip_serializing_if = "Option::is_none")]
pub user_id: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ServiceTier {
Auto,
StandardOnly,
}
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,
temperature: None,
top_p: None,
top_k: None,
stop_sequences: None,
stream: None,
output_config: None,
thinking: None,
metadata: None,
service_tier: None,
inference_geo: None,
container: 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<ToolDefinition>) -> Self {
self.tools = Some(tools);
self
}
pub fn with_custom_tools(mut self, tools: Vec<CustomTool>) -> Self {
self.tools = Some(tools.into_iter().map(ToolDefinition::Custom).collect());
self
}
pub fn with_tool_choice(mut self, choice: ToolChoice) -> Self {
self.tool_choice = Some(choice);
self
}
pub fn with_temperature(mut self, temperature: f32) -> Self {
self.temperature = Some(temperature);
self
}
pub fn with_effort(mut self, effort: EffortLevel) -> Self {
let config = self.output_config.get_or_insert(OutputConfig {
effort: None,
format: None,
});
config.effort = Some(effort);
self
}
pub fn with_json_schema(mut self, schema: serde_json::Value) -> Self {
let config = self.output_config.get_or_insert(OutputConfig {
effort: None,
format: None,
});
config.format = Some(OutputFormat {
format_type: "json_schema".into(),
schema,
});
self
}
pub fn with_thinking(mut self, budget_tokens: u32) -> Self {
self.thinking = Some(ThinkingConfig::Enabled {
budget_tokens,
display: None,
});
self
}
pub fn with_adaptive_thinking(mut self) -> Self {
self.thinking = Some(ThinkingConfig::Adaptive { display: None });
self
}
pub fn with_metadata(mut self, metadata: Metadata) -> Self {
self.metadata = Some(metadata);
self
}
pub fn with_service_tier(mut self, tier: ServiceTier) -> Self {
self.service_tier = Some(tier);
self
}
pub fn with_inference_geo(mut self, geo: impl Into<String>) -> Self {
self.inference_geo = Some(geo.into());
self
}
pub fn with_container(mut self, container_id: impl Into<String>) -> Self {
self.container = Some(container_id.into());
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TokenCount {
pub input_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, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum StopReason {
EndTurn,
MaxTokens,
StopSequence,
ToolUse,
PauseTurn,
Refusal,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StopDetails {
#[serde(rename = "type")]
pub stop_type: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub category: Option<RefusalCategory>,
#[serde(skip_serializing_if = "Option::is_none")]
pub explanation: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum RefusalCategory {
Cyber,
Bio,
ReasoningExtraction,
}
#[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>,
#[serde(skip_serializing_if = "Option::is_none")]
pub stop_details: Option<StopDetails>,
pub usage: Usage,
#[serde(skip_serializing_if = "Option::is_none")]
pub container: Option<Container>,
#[serde(skip)]
pub rate_limit_info: Option<RateLimitInfo>,
}
#[derive(Debug, Clone, Default)]
pub struct RateLimitInfo {
pub requests_remaining: Option<u32>,
pub tokens_remaining: Option<u32>,
pub requests_reset: Option<String>,
pub tokens_reset: Option<String>,
}
#[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 mut tool = CustomTool::new("test", "test tool", serde_json::json!({"type": "object"}))
.programmatic();
tool.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_custom_tool_with_new_fields() {
let mut tool =
CustomTool::new("test", "test", serde_json::json!({"type": "object"})).with_strict();
tool.defer_loading = Some(true);
tool.eager_input_streaming = Some(true);
let json = serde_json::to_value(&tool).unwrap();
assert_eq!(json["defer_loading"], true);
assert_eq!(json["eager_input_streaming"], true);
assert_eq!(json["strict"], true);
}
#[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));
}
#[test]
fn test_metadata_serialization() {
let request = MessagesRequest::new(
"claude-sonnet-4-5-20250929",
1024,
vec![Message::user("Hello")],
)
.with_metadata(Metadata {
user_id: Some("user-abc-123".into()),
});
let json = serde_json::to_value(&request).unwrap();
assert_eq!(json["metadata"]["user_id"], "user-abc-123");
}
#[test]
fn test_service_tier_serialization() {
let json = serde_json::to_string(&ServiceTier::StandardOnly).unwrap();
assert_eq!(json, r#""standard_only""#);
let json = serde_json::to_string(&ServiceTier::Auto).unwrap();
assert_eq!(json, r#""auto""#);
}
#[test]
fn test_refusal_stop_reason_deserialization() {
let json = r#"{
"id": "msg_123",
"type": "message",
"role": "assistant",
"content": [],
"model": "claude-sonnet-4-5-20250929",
"stop_reason": "refusal",
"stop_details": {
"type": "refusal",
"category": "cyber",
"explanation": "Request involves prohibited content"
},
"usage": { "input_tokens": 10, "output_tokens": 0 }
}"#;
let response: MessagesResponse = serde_json::from_str(json).unwrap();
assert_eq!(response.stop_reason, Some(StopReason::Refusal));
let details = response.stop_details.as_ref().unwrap();
assert_eq!(details.category, Some(RefusalCategory::Cyber));
assert_eq!(
details.explanation.as_deref(),
Some("Request involves prohibited content")
);
}
#[test]
fn test_effort_xhigh_and_max() {
let json_xhigh = serde_json::to_string(&EffortLevel::XHigh).unwrap();
assert_eq!(json_xhigh, r#""xhigh""#);
let json_max = serde_json::to_string(&EffortLevel::Max).unwrap();
assert_eq!(json_max, r#""max""#);
let parsed: EffortLevel = serde_json::from_str(r#""xhigh""#).unwrap();
assert_eq!(parsed, EffortLevel::XHigh);
let parsed: EffortLevel = serde_json::from_str(r#""max""#).unwrap();
assert_eq!(parsed, EffortLevel::Max);
}
#[test]
fn test_cache_control_with_ttl() {
let cache = CacheControl::ephemeral_with_ttl(CacheTtl::OneHour);
let json = serde_json::to_value(&cache).unwrap();
assert_eq!(json["type"], "ephemeral");
assert_eq!(json["ttl"], "1h");
}
#[test]
fn test_cache_control_ttl_deserialization() {
let json = r#"{"type": "ephemeral", "ttl": "5m"}"#;
let cache: CacheControl = serde_json::from_str(json).unwrap();
assert_eq!(cache.ttl, Some(CacheTtl::FiveMinutes));
}
#[test]
fn test_cache_control_without_ttl_still_works() {
let cache = CacheControl::ephemeral();
let json = serde_json::to_string(&cache).unwrap();
assert_eq!(json, r#"{"type":"ephemeral"}"#);
assert_eq!(cache.ttl, None);
}
#[test]
fn test_usage_with_details_deserialization() {
let json = r#"{
"input_tokens": 100,
"output_tokens": 50,
"cache_creation_input_tokens": 10,
"cache_read_input_tokens": 5,
"output_tokens_details": {
"thinking_tokens": 20
},
"server_tool_use": {
"web_search_requests": 3
},
"service_tier": "priority",
"inference_geo": "us"
}"#;
let usage: Usage = serde_json::from_str(json).unwrap();
assert_eq!(usage.input_tokens, 100);
assert_eq!(usage.output_tokens, 50);
let details = usage.output_tokens_details.unwrap();
assert_eq!(details.thinking_tokens, Some(20));
let server = usage.server_tool_use.unwrap();
assert_eq!(server.web_search_requests, Some(3));
assert_eq!(usage.service_tier.as_deref(), Some("priority"));
}
#[test]
fn test_usage_without_new_fields_still_works() {
let json = r#"{"input_tokens": 10, "output_tokens": 5}"#;
let usage: Usage = serde_json::from_str(json).unwrap();
assert_eq!(usage.input_tokens, 10);
assert!(usage.output_tokens_details.is_none());
assert!(usage.server_tool_use.is_none());
}
#[test]
fn test_token_count_deserialization() {
let json = r#"{
"input_tokens": 1234,
"cache_creation_input_tokens": 100,
"cache_read_input_tokens": 50
}"#;
let count: TokenCount = serde_json::from_str(json).unwrap();
assert_eq!(count.input_tokens, 1234);
assert_eq!(count.cache_creation_input_tokens, Some(100));
assert_eq!(count.cache_read_input_tokens, Some(50));
}
#[test]
fn test_token_count_minimal() {
let json = r#"{"input_tokens": 42}"#;
let count: TokenCount = serde_json::from_str(json).unwrap();
assert_eq!(count.input_tokens, 42);
assert!(count.cache_creation_input_tokens.is_none());
}
#[test]
fn test_rate_limit_info_default() {
let info = RateLimitInfo::default();
assert!(info.requests_remaining.is_none());
assert!(info.tokens_remaining.is_none());
assert!(info.requests_reset.is_none());
assert!(info.tokens_reset.is_none());
}
#[test]
fn test_response_deserialization_with_skipped_rate_limit() {
let json = r#"{
"id": "msg_123",
"type": "message",
"role": "assistant",
"content": [{"type": "text", "text": "hello"}],
"model": "claude-sonnet-4-5-20250929",
"stop_reason": "end_turn",
"usage": {"input_tokens": 10, "output_tokens": 5}
}"#;
let response: MessagesResponse = serde_json::from_str(json).unwrap();
assert!(response.rate_limit_info.is_none()); }
#[test]
fn test_unknown_content_block_deserializes() {
let json = r#"{"type": "some_future_block", "id": "fb_123", "data": "test"}"#;
let block: ContentBlock = serde_json::from_str(json).unwrap();
match block {
ContentBlock::Unknown { block_type, data } => {
assert_eq!(block_type, "some_future_block");
assert_eq!(data["id"], "fb_123");
}
_ => panic!("Expected Unknown variant"),
}
}
#[test]
fn test_known_content_blocks_still_work() {
let json = r#"{"type": "text", "text": "hello"}"#;
let block: ContentBlock = serde_json::from_str(json).unwrap();
match block {
ContentBlock::Text { text, .. } => assert_eq!(text, "hello"),
_ => panic!("Expected Text variant"),
}
}
#[test]
fn test_content_block_roundtrip() {
let original = ContentBlock::Text {
text: "test".into(),
cache_control: None,
citations: None,
};
let json = serde_json::to_string(&original).unwrap();
let deserialized: ContentBlock = serde_json::from_str(&json).unwrap();
match deserialized {
ContentBlock::Text { text, .. } => assert_eq!(text, "test"),
_ => panic!("Expected Text variant after roundtrip"),
}
}
#[test]
fn test_unknown_block_in_response() {
let json = r#"{
"id": "msg_123",
"type": "message",
"role": "assistant",
"content": [
{"type": "text", "text": "hello"},
{"type": "some_future_block", "id": "fb_1", "data": "test"}
],
"model": "claude-sonnet-4-5-20250929",
"stop_reason": "end_turn",
"usage": {"input_tokens": 10, "output_tokens": 5}
}"#;
let response: MessagesResponse = serde_json::from_str(json).unwrap();
assert_eq!(response.content.len(), 2);
match &response.content[0] {
ContentBlock::Text { text, .. } => assert_eq!(text, "hello"),
_ => panic!("Expected Text variant"),
}
match &response.content[1] {
ContentBlock::Unknown { block_type, .. } => {
assert_eq!(block_type, "some_future_block");
}
_ => panic!("Expected Unknown variant"),
}
}
#[test]
fn test_unknown_block_without_type_field() {
let json = r#"{"foo": "bar"}"#;
let block: ContentBlock = serde_json::from_str(json).unwrap();
match block {
ContentBlock::Unknown { block_type, data } => {
assert_eq!(block_type, "unknown");
assert_eq!(data["foo"], "bar");
}
_ => panic!("Expected Unknown variant"),
}
}
#[test]
fn test_server_tool_use_deserialization() {
let json = r#"{"type": "server_tool_use", "id": "stu_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, "stu_123");
assert_eq!(name, "web_search");
assert_eq!(input["query"], "rust");
}
_ => panic!("Expected ServerToolUse variant"),
}
}
#[test]
fn test_web_search_tool_result_deserialization() {
let json = r#"{"type": "web_search_tool_result", "tool_use_id": "stu_123", "content": [{"type": "web_page", "url": "https://example.com"}]}"#;
let block: ContentBlock = serde_json::from_str(json).unwrap();
match block {
ContentBlock::WebSearchToolResult {
tool_use_id,
content,
..
} => {
assert_eq!(tool_use_id, "stu_123");
assert!(content.is_array());
}
_ => panic!("Expected WebSearchToolResult variant"),
}
}
#[test]
fn test_code_execution_tool_result_deserialization() {
let json = r#"{"type": "code_execution_tool_result", "tool_use_id": "ce_123", "content": {"stdout": "hello"}}"#;
let block: ContentBlock = serde_json::from_str(json).unwrap();
match block {
ContentBlock::CodeExecutionToolResult {
tool_use_id,
content,
..
} => {
assert_eq!(tool_use_id, "ce_123");
assert_eq!(content["stdout"], "hello");
}
_ => panic!("Expected CodeExecutionToolResult variant"),
}
}
#[test]
fn test_structured_output_serialization() {
let schema = serde_json::json!({
"type": "object",
"properties": {
"name": {"type": "string"}
}
});
let request = MessagesRequest::new(
"claude-sonnet-4-5-20250929",
1024,
vec![Message::user("test")],
)
.with_json_schema(schema.clone());
let json = serde_json::to_value(&request).unwrap();
assert_eq!(json["output_config"]["format"]["type"], "json_schema");
assert_eq!(json["output_config"]["format"]["schema"], schema);
}
#[test]
fn test_effort_and_schema_coexist() {
let request = MessagesRequest::new(
"claude-sonnet-4-5-20250929",
1024,
vec![Message::user("test")],
)
.with_effort(EffortLevel::Low)
.with_json_schema(serde_json::json!({"type": "object"}));
let config = request.output_config.as_ref().unwrap();
assert_eq!(config.effort, Some(EffortLevel::Low));
assert!(config.format.is_some());
}
#[test]
fn test_adaptive_thinking_serialization() {
let request = MessagesRequest::new(
"claude-sonnet-4-5-20250929",
1024,
vec![Message::user("test")],
)
.with_adaptive_thinking();
let json = serde_json::to_value(&request).unwrap();
assert_eq!(json["thinking"]["type"], "adaptive");
}
#[test]
fn test_thinking_config_enabled_with_display() {
let config = ThinkingConfig::Enabled {
budget_tokens: 4096,
display: Some(ThinkingDisplay::Summarized),
};
let json = serde_json::to_value(&config).unwrap();
assert_eq!(json["type"], "enabled");
assert_eq!(json["budget_tokens"], 4096);
assert_eq!(json["display"], "summarized");
}
#[test]
fn test_tool_choice_auto_serialization() {
let choice = ToolChoice::auto();
let json = serde_json::to_value(&choice).unwrap();
assert_eq!(json["type"], "auto");
assert!(json.get("disable_parallel_tool_use").is_none());
}
#[test]
fn test_tool_choice_with_disable_parallel() {
let choice = ToolChoice::Auto {
disable_parallel_tool_use: Some(true),
};
let json = serde_json::to_value(&choice).unwrap();
assert_eq!(json["type"], "auto");
assert_eq!(json["disable_parallel_tool_use"], true);
}
#[test]
fn test_tool_choice_tool_serialization() {
let choice = ToolChoice::tool("my_tool");
let json = serde_json::to_value(&choice).unwrap();
assert_eq!(json["type"], "tool");
assert_eq!(json["name"], "my_tool");
assert!(json.get("disable_parallel_tool_use").is_none());
}
#[test]
fn test_tool_result_content_text_serialization() {
let content = ToolResultContent::Text("hello".into());
let json = serde_json::to_value(&content).unwrap();
assert_eq!(json, serde_json::json!("hello"));
}
#[test]
fn test_tool_result_content_blocks_serialization() {
let content = ToolResultContent::Blocks(vec![ContentBlock::Text {
text: "result".into(),
cache_control: None,
citations: None,
}]);
let json = serde_json::to_value(&content).unwrap();
assert!(json.is_array());
assert_eq!(json[0]["type"], "text");
assert_eq!(json[0]["text"], "result");
}
#[test]
fn test_tool_use_with_caller() {
let block = ContentBlock::ToolUse {
id: "tu_123".into(),
name: "my_tool".into(),
input: serde_json::json!({}),
caller: Some("code_execution_20250825".into()),
cache_control: None,
};
let json = serde_json::to_value(&block).unwrap();
assert_eq!(json["caller"], "code_execution_20250825");
}
#[test]
fn test_tool_use_without_caller() {
let block = ContentBlock::ToolUse {
id: "tu_123".into(),
name: "my_tool".into(),
input: serde_json::json!({}),
caller: None,
cache_control: None,
};
let json = serde_json::to_value(&block).unwrap();
assert!(json.get("caller").is_none());
}
#[test]
fn test_container_request_serialization() {
let request = MessagesRequest::new(
"claude-sonnet-4-5-20250929",
1024,
vec![Message::user("Hello")],
)
.with_container("container_123");
let json = serde_json::to_value(&request).unwrap();
assert_eq!(json["container"], "container_123");
}
#[test]
fn test_container_response_deserialization() {
let json = r#"{
"id": "msg_1",
"type": "message",
"role": "assistant",
"content": [],
"model": "claude-sonnet-4-5-20250929",
"stop_reason": "end_turn",
"usage": {"input_tokens": 10, "output_tokens": 5},
"container": {"id": "ctr_abc", "expires_at": "2026-01-01T00:00:00Z"}
}"#;
let response: MessagesResponse = serde_json::from_str(json).unwrap();
let container = response.container.unwrap();
assert_eq!(container.id, "ctr_abc");
}
#[test]
fn test_container_upload_deserialization() {
let json = r#"{"type": "container_upload", "file_id": "file_123"}"#;
let block: ContentBlock = serde_json::from_str(json).unwrap();
match block {
ContentBlock::ContainerUpload { file_id, .. } => {
assert_eq!(file_id, "file_123");
}
_ => panic!("Expected ContainerUpload"),
}
}
#[test]
fn test_mid_conv_system_deserialization() {
let json = r#"{
"type": "mid_conv_system",
"content": [{"type": "text", "text": "New instruction"}]
}"#;
let block: ContentBlock = serde_json::from_str(json).unwrap();
match block {
ContentBlock::MidConvSystem { content, .. } => {
assert_eq!(content[0].text, "New instruction");
}
_ => panic!("Expected MidConvSystem"),
}
}
#[test]
fn test_server_tool_use_in_response() {
let json = r#"{
"id": "msg_123",
"type": "message",
"role": "assistant",
"content": [
{"type": "text", "text": "searching..."},
{"type": "server_tool_use", "id": "stu_1", "name": "web_search", "input": {"query": "rust"}}
],
"model": "claude-sonnet-4-5-20250929",
"stop_reason": "end_turn",
"usage": {"input_tokens": 10, "output_tokens": 5}
}"#;
let response: MessagesResponse = serde_json::from_str(json).unwrap();
assert_eq!(response.content.len(), 2);
match &response.content[1] {
ContentBlock::ServerToolUse { id, name, .. } => {
assert_eq!(id, "stu_1");
assert_eq!(name, "web_search");
}
_ => panic!("Expected ServerToolUse variant"),
}
}
}