use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use validator::Validate;
use crate::validated::Normalizable;
#[serde_with::skip_serializing_none]
#[derive(Debug, Clone, Serialize, Deserialize, Validate, schemars::JsonSchema)]
#[validate(schema(function = "validate_message_request"))]
pub struct CreateMessageRequest {
#[validate(length(min = 1, message = "model field is required and cannot be empty"))]
pub model: String,
#[validate(length(min = 1, message = "messages array is required and cannot be empty"))]
pub messages: Vec<InputMessage>,
#[validate(range(min = 1, message = "max_tokens must be greater than 0"))]
pub max_tokens: u32,
pub metadata: Option<Metadata>,
pub service_tier: Option<ServiceTier>,
pub stop_sequences: Option<Vec<String>>,
pub stream: Option<bool>,
pub system: Option<SystemContent>,
pub temperature: Option<f64>,
pub thinking: Option<ThinkingConfig>,
pub tool_choice: Option<ToolChoice>,
pub tools: Option<Vec<Tool>>,
pub top_k: Option<u32>,
pub top_p: Option<f64>,
pub container: Option<ContainerConfig>,
pub mcp_servers: Option<Vec<McpServerConfig>>,
}
impl Normalizable for CreateMessageRequest {
}
impl CreateMessageRequest {
pub fn is_stream(&self) -> bool {
self.stream.unwrap_or(false)
}
pub fn get_model(&self) -> &str {
&self.model
}
pub fn has_mcp_toolset(&self) -> bool {
self.tools
.as_ref()
.is_some_and(|tools| tools.iter().any(|t| matches!(t, Tool::McpToolset(_))))
}
pub fn mcp_server_configs(&self) -> Option<&[McpServerConfig]> {
self.mcp_servers
.as_deref()
.filter(|servers| !servers.is_empty())
}
}
impl Tool {
fn matches_tool_choice_name(&self, name: &str) -> bool {
match self {
Self::Custom(tool) => tool.name == name,
Self::ToolSearch(tool) => tool.name == name,
Self::Bash(tool) => tool.name == name,
Self::TextEditor(tool) => tool.name == name,
Self::WebSearch(tool) => tool.name == name,
Self::McpToolset(toolset) => {
let default_enabled = toolset
.default_config
.as_ref()
.and_then(|config| config.enabled)
.unwrap_or(true);
toolset
.configs
.as_ref()
.and_then(|configs| configs.get(name))
.and_then(|config| config.enabled)
.unwrap_or(default_enabled)
}
}
}
}
fn validate_message_request(req: &CreateMessageRequest) -> Result<(), validator::ValidationError> {
if req.has_mcp_toolset() && req.mcp_server_configs().is_none() {
let mut e = validator::ValidationError::new("mcp_servers_required");
e.message = Some("mcp_servers is required when mcp_toolset tools are present".into());
return Err(e);
}
let Some(tool_choice) = &req.tool_choice else {
return Ok(());
};
let has_tools = req.tools.as_ref().is_some_and(|tools| !tools.is_empty());
let requires_tools = !matches!(tool_choice, ToolChoice::None);
if requires_tools && !has_tools {
let mut e = validator::ValidationError::new("tool_choice_requires_tools");
e.message = Some(
"Invalid value for 'tool_choice': 'tool_choice' is only allowed when 'tools' are specified."
.into(),
);
return Err(e);
}
if let ToolChoice::Tool { name, .. } = tool_choice {
let tool_exists = req
.tools
.as_ref()
.is_some_and(|tools| tools.iter().any(|tool| tool.matches_tool_choice_name(name)));
if !tool_exists {
let mut e = validator::ValidationError::new("tool_choice_tool_not_found");
e.message = Some(
format!("Invalid value for 'tool_choice': tool '{name}' not found in 'tools'.")
.into(),
);
return Err(e);
}
}
Ok(())
}
#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
pub struct Metadata {
#[serde(skip_serializing_if = "Option::is_none")]
pub user_id: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum ServiceTier {
Auto,
StandardOnly,
}
#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
#[serde(untagged)]
pub enum SystemContent {
String(String),
Blocks(Vec<TextBlock>),
}
#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
pub struct InputMessage {
pub role: Role,
pub content: InputContent,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, schemars::JsonSchema)]
#[serde(rename_all = "lowercase")]
pub enum Role {
User,
Assistant,
}
#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
#[serde(untagged)]
pub enum InputContent {
String(String),
Blocks(Vec<InputContentBlock>),
}
#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum InputContentBlock {
Text(TextBlock),
Image(ImageBlock),
Document(DocumentBlock),
ToolUse(ToolUseBlock),
ToolResult(ToolResultBlock),
Thinking(ThinkingBlock),
RedactedThinking(RedactedThinkingBlock),
ServerToolUse(ServerToolUseBlock),
SearchResult(SearchResultBlock),
WebSearchToolResult(WebSearchToolResultBlock),
ToolSearchToolResult(ToolSearchToolResultBlock),
ToolReference(ToolReferenceBlock),
}
#[serde_with::skip_serializing_none]
#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
pub struct TextBlock {
pub text: String,
pub cache_control: Option<CacheControl>,
pub citations: Option<Vec<Citation>>,
}
#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
pub struct ImageBlock {
pub source: ImageSource,
#[serde(skip_serializing_if = "Option::is_none")]
pub cache_control: Option<CacheControl>,
}
#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ImageSource {
Base64 { media_type: String, data: String },
Url { url: String },
}
#[serde_with::skip_serializing_none]
#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
pub struct DocumentBlock {
pub source: DocumentSource,
pub cache_control: Option<CacheControl>,
pub title: Option<String>,
pub context: Option<String>,
pub citations: Option<CitationsConfig>,
}
#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum DocumentSource {
Base64 { media_type: String, data: String },
Text { data: String },
Url { url: String },
Content { content: Vec<InputContentBlock> },
}
#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
pub struct ToolUseBlock {
pub id: String,
pub name: String,
pub input: Value,
#[serde(skip_serializing_if = "Option::is_none")]
pub cache_control: Option<CacheControl>,
}
#[serde_with::skip_serializing_none]
#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
pub struct ToolResultBlock {
pub tool_use_id: String,
pub content: Option<ToolResultContent>,
pub is_error: Option<bool>,
pub cache_control: Option<CacheControl>,
}
#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
#[serde(untagged)]
pub enum ToolResultContent {
String(String),
Blocks(Vec<ToolResultContentBlock>),
}
#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ToolResultContentBlock {
Text(TextBlock),
Image(ImageBlock),
Document(DocumentBlock),
SearchResult(SearchResultBlock),
}
#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
pub struct ThinkingBlock {
pub thinking: String,
pub signature: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
pub struct RedactedThinkingBlock {
pub data: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
pub struct ServerToolUseBlock {
pub id: String,
pub name: String,
pub input: Value,
#[serde(skip_serializing_if = "Option::is_none")]
pub cache_control: Option<CacheControl>,
}
#[serde_with::skip_serializing_none]
#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
pub struct SearchResultBlock {
pub source: String,
pub title: String,
pub content: Vec<TextBlock>,
pub cache_control: Option<CacheControl>,
pub citations: Option<CitationsConfig>,
}
#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
pub struct WebSearchToolResultBlock {
pub tool_use_id: String,
pub content: WebSearchToolResultContent,
#[serde(skip_serializing_if = "Option::is_none")]
pub cache_control: Option<CacheControl>,
}
#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
#[serde(untagged)]
pub enum WebSearchToolResultContent {
Results(Vec<WebSearchResultBlock>),
Error(WebSearchToolResultError),
}
#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
pub struct WebSearchResultBlock {
pub title: String,
pub url: String,
pub encrypted_content: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub page_age: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
pub struct WebSearchToolResultError {
#[serde(rename = "type")]
pub error_type: String,
pub error_code: WebSearchToolResultErrorCode,
}
#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum WebSearchToolResultErrorCode {
InvalidToolInput,
Unavailable,
MaxUsesExceeded,
TooManyRequests,
QueryTooLong,
}
#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum CacheControl {
Ephemeral,
}
#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
pub struct CitationsConfig {
#[serde(skip_serializing_if = "Option::is_none")]
pub enabled: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
#[serde(tag = "type", rename_all = "snake_case")]
#[expect(
clippy::enum_variant_names,
reason = "variant names match the OpenAI API citation type discriminators (char_location, page_location, etc.)"
)]
pub enum Citation {
CharLocation(CharLocationCitation),
PageLocation(PageLocationCitation),
ContentBlockLocation(ContentBlockLocationCitation),
WebSearchResultLocation(WebSearchResultLocationCitation),
SearchResultLocation(SearchResultLocationCitation),
}
#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
pub struct CharLocationCitation {
pub cited_text: String,
pub document_index: u32,
pub document_title: Option<String>,
pub start_char_index: u32,
pub end_char_index: u32,
#[serde(skip_serializing_if = "Option::is_none")]
pub file_id: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
pub struct PageLocationCitation {
pub cited_text: String,
pub document_index: u32,
pub document_title: Option<String>,
pub start_page_number: u32,
pub end_page_number: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
pub struct ContentBlockLocationCitation {
pub cited_text: String,
pub document_index: u32,
pub document_title: Option<String>,
pub start_block_index: u32,
pub end_block_index: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
pub struct WebSearchResultLocationCitation {
pub cited_text: String,
pub url: String,
pub title: Option<String>,
pub encrypted_index: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
pub struct SearchResultLocationCitation {
pub cited_text: String,
pub search_result_index: u32,
pub source: String,
pub title: Option<String>,
pub start_block_index: u32,
pub end_block_index: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
#[serde(untagged)]
#[expect(
clippy::enum_variant_names,
reason = "ToolSearch matches Anthropic API naming"
)]
#[schemars(rename = "MessagesTool")]
pub enum Tool {
McpToolset(McpToolset),
Custom(CustomTool),
ToolSearch(ToolSearchTool),
Bash(BashTool),
TextEditor(TextEditorTool),
WebSearch(WebSearchTool),
}
#[serde_with::skip_serializing_none]
#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
pub struct CustomTool {
pub name: String,
#[serde(rename = "type")]
pub tool_type: Option<String>,
pub description: Option<String>,
pub input_schema: InputSchema,
pub defer_loading: Option<bool>,
pub cache_control: Option<CacheControl>,
}
#[serde_with::skip_serializing_none]
#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
pub struct InputSchema {
#[serde(rename = "type")]
pub schema_type: String,
pub properties: Option<HashMap<String, Value>>,
pub required: Option<Vec<String>>,
#[serde(flatten)]
pub additional: HashMap<String, Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
pub struct BashTool {
#[serde(rename = "type")]
pub tool_type: String,
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub cache_control: Option<CacheControl>,
}
#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
pub struct TextEditorTool {
#[serde(rename = "type")]
pub tool_type: String,
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub cache_control: Option<CacheControl>,
}
#[serde_with::skip_serializing_none]
#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
pub struct WebSearchTool {
#[serde(rename = "type")]
pub tool_type: String,
pub name: String,
pub allowed_domains: Option<Vec<String>>,
pub blocked_domains: Option<Vec<String>>,
pub max_uses: Option<u32>,
pub user_location: Option<UserLocation>,
pub cache_control: Option<CacheControl>,
}
#[serde_with::skip_serializing_none]
#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
pub struct UserLocation {
#[serde(rename = "type")]
pub location_type: String,
pub city: Option<String>,
pub region: Option<String>,
pub country: Option<String>,
pub timezone: Option<String>,
}
#[serde_with::skip_serializing_none]
#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
#[serde(tag = "type", rename_all = "snake_case")]
#[schemars(rename = "MessagesToolChoice")]
pub enum ToolChoice {
Auto {
disable_parallel_tool_use: Option<bool>,
},
Any {
disable_parallel_tool_use: Option<bool>,
},
Tool {
name: String,
disable_parallel_tool_use: Option<bool>,
},
None,
}
#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ThinkingConfig {
Enabled {
budget_tokens: u32,
},
Disabled,
}
#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
pub struct Message {
pub id: String,
#[serde(rename = "type")]
pub message_type: String,
pub role: String,
pub content: Vec<ContentBlock>,
pub model: String,
pub stop_reason: Option<StopReason>,
pub stop_sequence: Option<String>,
pub usage: Usage,
}
#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ContentBlock {
Text {
text: String,
#[serde(skip_serializing_if = "Option::is_none")]
citations: Option<Vec<Citation>>,
},
ToolUse {
id: String,
name: String,
input: Value,
},
Thinking { thinking: String, signature: String },
RedactedThinking { data: String },
ServerToolUse {
id: String,
name: String,
input: Value,
},
WebSearchToolResult {
tool_use_id: String,
content: WebSearchToolResultContent,
},
ToolSearchToolResult {
tool_use_id: String,
content: ToolSearchResultContent,
},
ToolReference {
tool_name: String,
#[serde(skip_serializing_if = "Option::is_none")]
description: Option<String>,
},
McpToolUse {
id: String,
name: String,
server_name: String,
input: Value,
},
McpToolResult {
tool_use_id: String,
content: Option<ToolResultContent>,
is_error: Option<bool>,
},
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, schemars::JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum StopReason {
EndTurn,
MaxTokens,
StopSequence,
ToolUse,
PauseTurn,
Refusal,
}
#[serde_with::skip_serializing_none]
#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
#[schemars(rename = "MessagesUsage")]
pub struct Usage {
pub input_tokens: u32,
pub output_tokens: u32,
pub cache_creation_input_tokens: Option<u32>,
pub cache_read_input_tokens: Option<u32>,
pub cache_creation: Option<CacheCreation>,
pub server_tool_use: Option<ServerToolUsage>,
pub service_tier: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
pub struct CacheCreation {
#[serde(flatten)]
pub tokens_by_ttl: HashMap<String, u32>,
}
#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
pub struct ServerToolUsage {
pub web_search_requests: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum MessageStreamEvent {
MessageStart { message: Message },
MessageDelta {
delta: MessageDelta,
usage: MessageDeltaUsage,
},
MessageStop,
ContentBlockStart {
index: u32,
content_block: ContentBlock,
},
ContentBlockDelta {
index: u32,
delta: ContentBlockDelta,
},
ContentBlockStop { index: u32 },
Ping,
Error { error: ErrorResponse },
}
#[serde_with::skip_serializing_none]
#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
pub struct MessageDelta {
pub stop_reason: Option<StopReason>,
pub stop_sequence: Option<String>,
}
#[serde_with::skip_serializing_none]
#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
pub struct MessageDeltaUsage {
pub output_tokens: u32,
pub input_tokens: Option<u32>,
pub cache_creation_input_tokens: Option<u32>,
pub cache_read_input_tokens: Option<u32>,
pub server_tool_use: Option<ServerToolUsage>,
}
#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
#[serde(tag = "type", rename_all = "snake_case")]
#[expect(
clippy::enum_variant_names,
reason = "variant names match the OpenAI/Anthropic streaming delta type discriminators (text_delta, input_json_delta, etc.)"
)]
pub enum ContentBlockDelta {
TextDelta { text: String },
InputJsonDelta { partial_json: String },
ThinkingDelta { thinking: String },
SignatureDelta { signature: String },
CitationsDelta { citation: Citation },
}
#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
#[schemars(rename = "MessagesErrorResponse")]
pub struct ErrorResponse {
#[serde(rename = "type")]
pub error_type: String,
pub message: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
#[serde(tag = "type", rename_all = "snake_case")]
#[expect(
clippy::enum_variant_names,
reason = "variant names match the OpenAI API error type discriminators (invalid_request_error, authentication_error, etc.)"
)]
pub enum ApiError {
InvalidRequestError { message: String },
AuthenticationError { message: String },
BillingError { message: String },
PermissionError { message: String },
NotFoundError { message: String },
RateLimitError { message: String },
TimeoutError { message: String },
ApiError { message: String },
OverloadedError { message: String },
}
#[serde_with::skip_serializing_none]
#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
pub struct CountMessageTokensRequest {
pub model: String,
pub messages: Vec<InputMessage>,
pub system: Option<SystemContent>,
pub thinking: Option<ThinkingConfig>,
pub tool_choice: Option<ToolChoice>,
pub tools: Option<Vec<Tool>>,
}
#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
pub struct CountMessageTokensResponse {
pub input_tokens: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
pub struct ModelInfo {
#[serde(rename = "type")]
pub model_type: String,
pub id: String,
pub display_name: String,
pub created_at: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
pub struct ListModelsResponse {
pub data: Vec<ModelInfo>,
pub has_more: bool,
pub first_id: Option<String>,
pub last_id: Option<String>,
}
#[serde_with::skip_serializing_none]
#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
pub struct ContainerConfig {
pub id: Option<String>,
pub skills: Option<Vec<String>>,
}
#[serde_with::skip_serializing_none]
#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
pub struct McpServerConfig {
#[serde(rename = "type", default = "McpServerConfig::default_type")]
pub server_type: String,
pub name: String,
pub url: String,
pub authorization_token: Option<String>,
pub tool_configuration: Option<McpToolConfiguration>,
}
impl McpServerConfig {
fn default_type() -> String {
"url".to_string()
}
}
#[serde_with::skip_serializing_none]
#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
pub struct McpToolConfiguration {
pub enabled: Option<bool>,
pub allowed_tools: Option<Vec<String>>,
}
#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
pub struct McpToolUseBlock {
pub id: String,
pub name: String,
pub server_name: String,
pub input: Value,
#[serde(skip_serializing_if = "Option::is_none")]
pub cache_control: Option<CacheControl>,
}
#[serde_with::skip_serializing_none]
#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
pub struct McpToolResultBlock {
pub tool_use_id: String,
pub content: Option<ToolResultContent>,
pub is_error: Option<bool>,
pub cache_control: Option<CacheControl>,
}
#[serde_with::skip_serializing_none]
#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
pub struct McpToolset {
#[serde(rename = "type")]
pub toolset_type: String,
pub mcp_server_name: String,
pub default_config: Option<McpToolDefaultConfig>,
pub configs: Option<HashMap<String, McpToolConfig>>,
pub cache_control: Option<CacheControl>,
}
#[serde_with::skip_serializing_none]
#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
pub struct McpToolDefaultConfig {
pub enabled: Option<bool>,
pub defer_loading: Option<bool>,
}
#[serde_with::skip_serializing_none]
#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
pub struct McpToolConfig {
pub enabled: Option<bool>,
pub defer_loading: Option<bool>,
}
#[serde_with::skip_serializing_none]
#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
pub struct CodeExecutionTool {
#[serde(rename = "type")]
pub tool_type: String,
pub name: String,
pub allowed_callers: Option<Vec<String>>,
pub defer_loading: Option<bool>,
pub strict: Option<bool>,
pub cache_control: Option<CacheControl>,
}
#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
pub struct CodeExecutionResultBlock {
pub stdout: String,
pub stderr: String,
pub return_code: i32,
pub content: Vec<CodeExecutionOutputBlock>,
}
#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
pub struct CodeExecutionOutputBlock {
#[serde(rename = "type")]
pub block_type: String,
pub file_id: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
pub struct CodeExecutionToolResultBlock {
pub tool_use_id: String,
pub content: CodeExecutionToolResultContent,
#[serde(skip_serializing_if = "Option::is_none")]
pub cache_control: Option<CacheControl>,
}
#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
#[serde(untagged)]
pub enum CodeExecutionToolResultContent {
Success(CodeExecutionResultBlock),
Error(CodeExecutionToolResultError),
}
#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
pub struct CodeExecutionToolResultError {
#[serde(rename = "type")]
pub error_type: String,
pub error_code: CodeExecutionToolResultErrorCode,
}
#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum CodeExecutionToolResultErrorCode {
Unavailable,
CodeExecutionExceededTimeout,
ContainerExpired,
InvalidToolInput,
}
#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
pub struct BashCodeExecutionResultBlock {
pub stdout: String,
pub stderr: String,
pub return_code: i32,
pub content: Vec<BashCodeExecutionOutputBlock>,
}
#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
pub struct BashCodeExecutionOutputBlock {
#[serde(rename = "type")]
pub block_type: String,
pub file_id: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
pub struct BashCodeExecutionToolResultBlock {
pub tool_use_id: String,
pub content: BashCodeExecutionToolResultContent,
#[serde(skip_serializing_if = "Option::is_none")]
pub cache_control: Option<CacheControl>,
}
#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
#[serde(untagged)]
pub enum BashCodeExecutionToolResultContent {
Success(BashCodeExecutionResultBlock),
Error(BashCodeExecutionToolResultError),
}
#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
pub struct BashCodeExecutionToolResultError {
#[serde(rename = "type")]
pub error_type: String,
pub error_code: BashCodeExecutionToolResultErrorCode,
}
#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum BashCodeExecutionToolResultErrorCode {
Unavailable,
CodeExecutionExceededTimeout,
ContainerExpired,
InvalidToolInput,
}
#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
pub struct TextEditorCodeExecutionToolResultBlock {
pub tool_use_id: String,
pub content: TextEditorCodeExecutionToolResultContent,
#[serde(skip_serializing_if = "Option::is_none")]
pub cache_control: Option<CacheControl>,
}
#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
#[serde(untagged)]
pub enum TextEditorCodeExecutionToolResultContent {
CreateResult(TextEditorCodeExecutionCreateResultBlock),
StrReplaceResult(TextEditorCodeExecutionStrReplaceResultBlock),
ViewResult(TextEditorCodeExecutionViewResultBlock),
Error(TextEditorCodeExecutionToolResultError),
}
#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
pub struct TextEditorCodeExecutionCreateResultBlock {
#[serde(rename = "type")]
pub block_type: String, }
#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
pub struct TextEditorCodeExecutionStrReplaceResultBlock {
#[serde(rename = "type")]
pub block_type: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub snippet: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
pub struct TextEditorCodeExecutionViewResultBlock {
#[serde(rename = "type")]
pub block_type: String,
pub content: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
pub struct TextEditorCodeExecutionToolResultError {
#[serde(rename = "type")]
pub error_type: String,
pub error_code: TextEditorCodeExecutionToolResultErrorCode,
}
#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum TextEditorCodeExecutionToolResultErrorCode {
Unavailable,
InvalidToolInput,
FileNotFound,
ContainerExpired,
}
#[serde_with::skip_serializing_none]
#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
pub struct WebFetchTool {
#[serde(rename = "type")]
pub tool_type: String,
pub name: String,
pub allowed_callers: Option<Vec<String>>,
pub max_uses: Option<u32>,
pub cache_control: Option<CacheControl>,
}
#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
pub struct WebFetchResultBlock {
#[serde(rename = "type")]
pub block_type: String,
pub url: String,
pub content: DocumentBlock,
#[serde(skip_serializing_if = "Option::is_none")]
pub retrieved_at: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
pub struct WebFetchToolResultBlock {
pub tool_use_id: String,
pub content: WebFetchToolResultContent,
#[serde(skip_serializing_if = "Option::is_none")]
pub cache_control: Option<CacheControl>,
}
#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
#[serde(untagged)]
pub enum WebFetchToolResultContent {
Success(WebFetchResultBlock),
Error(WebFetchToolResultError),
}
#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
pub struct WebFetchToolResultError {
#[serde(rename = "type")]
pub error_type: String,
pub error_code: WebFetchToolResultErrorCode,
}
#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum WebFetchToolResultErrorCode {
InvalidToolInput,
Unavailable,
MaxUsesExceeded,
TooManyRequests,
UrlNotAllowed,
FetchFailed,
ContentTooLarge,
}
#[serde_with::skip_serializing_none]
#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
pub struct ToolSearchTool {
#[serde(rename = "type")]
pub tool_type: String,
pub name: String,
pub allowed_callers: Option<Vec<String>>,
pub cache_control: Option<CacheControl>,
}
#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
pub struct ToolReferenceBlock {
#[serde(rename = "type")]
pub block_type: String,
pub tool_name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
pub struct ToolSearchResultContent {
#[serde(rename = "type")]
pub block_type: String,
pub tool_references: Vec<ToolReferenceBlock>,
}
#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
pub struct ToolSearchToolResultBlock {
pub tool_use_id: String,
pub content: ToolSearchResultContent,
#[serde(skip_serializing_if = "Option::is_none")]
pub cache_control: Option<CacheControl>,
}
#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
pub struct ContainerUploadBlock {
#[serde(rename = "type")]
pub block_type: String,
pub file_id: String,
pub file_name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub file_path: Option<String>,
}
#[serde_with::skip_serializing_none]
#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
pub struct MemoryTool {
#[serde(rename = "type")]
pub tool_type: String,
pub name: String,
pub allowed_callers: Option<Vec<String>>,
pub defer_loading: Option<bool>,
pub strict: Option<bool>,
pub input_examples: Option<Vec<Value>>,
pub cache_control: Option<CacheControl>,
}
#[serde_with::skip_serializing_none]
#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
pub struct ComputerUseTool {
#[serde(rename = "type")]
pub tool_type: String,
pub name: String,
pub display_width_px: u32,
pub display_height_px: u32,
pub display_number: Option<u32>,
pub allowed_callers: Option<Vec<String>>,
pub cache_control: Option<CacheControl>,
}
#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum BetaInputContentBlock {
Text(TextBlock),
Image(ImageBlock),
Document(DocumentBlock),
ToolUse(ToolUseBlock),
ToolResult(ToolResultBlock),
Thinking(ThinkingBlock),
RedactedThinking(RedactedThinkingBlock),
ServerToolUse(ServerToolUseBlock),
SearchResult(SearchResultBlock),
WebSearchToolResult(WebSearchToolResultBlock),
McpToolUse(McpToolUseBlock),
McpToolResult(McpToolResultBlock),
CodeExecutionToolResult(CodeExecutionToolResultBlock),
BashCodeExecutionToolResult(BashCodeExecutionToolResultBlock),
TextEditorCodeExecutionToolResult(TextEditorCodeExecutionToolResultBlock),
WebFetchToolResult(WebFetchToolResultBlock),
ToolSearchToolResult(ToolSearchToolResultBlock),
ToolReference(ToolReferenceBlock),
ContainerUpload(ContainerUploadBlock),
}
#[serde_with::skip_serializing_none]
#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum BetaContentBlock {
Text {
text: String,
citations: Option<Vec<Citation>>,
},
ToolUse {
id: String,
name: String,
input: Value,
},
Thinking {
thinking: String,
signature: String,
},
RedactedThinking {
data: String,
},
ServerToolUse {
id: String,
name: String,
input: Value,
},
WebSearchToolResult {
tool_use_id: String,
content: WebSearchToolResultContent,
},
McpToolUse {
id: String,
name: String,
server_name: String,
input: Value,
},
McpToolResult {
tool_use_id: String,
content: Option<ToolResultContent>,
is_error: Option<bool>,
},
CodeExecutionToolResult {
tool_use_id: String,
content: CodeExecutionToolResultContent,
},
BashCodeExecutionToolResult {
tool_use_id: String,
content: BashCodeExecutionToolResultContent,
},
TextEditorCodeExecutionToolResult {
tool_use_id: String,
content: TextEditorCodeExecutionToolResultContent,
},
WebFetchToolResult {
tool_use_id: String,
content: WebFetchToolResultContent,
},
ToolSearchToolResult {
tool_use_id: String,
content: ToolSearchResultContent,
},
ToolReference {
tool_name: String,
description: Option<String>,
},
ContainerUpload {
file_id: String,
file_name: String,
file_path: Option<String>,
},
}
#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
#[serde(untagged)]
pub enum BetaTool {
Custom(CustomTool),
Bash(BashTool),
TextEditor(TextEditorTool),
WebSearch(WebSearchTool),
CodeExecution(CodeExecutionTool),
McpToolset(McpToolset),
WebFetch(WebFetchTool),
ToolSearch(ToolSearchTool),
Memory(MemoryTool),
ComputerUse(ComputerUseTool),
}
#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum BetaServerToolName {
WebSearch,
WebFetch,
CodeExecution,
BashCodeExecution,
TextEditorCodeExecution,
ToolSearchToolRegex,
ToolSearchToolBm25,
}
#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ServerToolCaller {
Direct,
#[serde(rename = "code_execution_20250825")]
CodeExecution20250825,
}
#[cfg(test)]
mod tests {
use serde_json;
use super::*;
fn base_request() -> CreateMessageRequest {
CreateMessageRequest {
model: "claude-test".to_string(),
messages: vec![InputMessage {
role: Role::User,
content: InputContent::String("hello".to_string()),
}],
max_tokens: 16,
metadata: None,
service_tier: None,
stop_sequences: None,
stream: None,
system: None,
temperature: None,
thinking: None,
tool_choice: None,
tools: None,
top_k: None,
top_p: None,
container: None,
mcp_servers: None,
}
}
fn custom_tool(name: &str) -> Tool {
Tool::Custom(CustomTool {
name: name.to_string(),
tool_type: None,
description: Some("test tool".to_string()),
input_schema: InputSchema {
schema_type: "object".to_string(),
properties: None,
required: None,
additional: HashMap::new(),
},
defer_loading: None,
cache_control: None,
})
}
fn mcp_toolset(configs: Option<HashMap<String, McpToolConfig>>) -> Tool {
Tool::McpToolset(McpToolset {
toolset_type: "mcp_toolset".to_string(),
mcp_server_name: "brave".to_string(),
default_config: None,
configs,
cache_control: None,
})
}
fn mcp_server_config() -> McpServerConfig {
McpServerConfig {
server_type: "url".to_string(),
name: "brave".to_string(),
url: "https://example.com/mcp".to_string(),
authorization_token: None,
tool_configuration: None,
}
}
#[test]
fn test_tool_mcp_toolset_defer_loading_deserialization() {
let json = r#"{
"type": "mcp_toolset",
"mcp_server_name": "brave",
"default_config": {"defer_loading": true}
}"#;
let tool: Tool = serde_json::from_str(json).expect("Failed to deserialize McpToolset Tool");
match tool {
Tool::McpToolset(ts) => {
assert_eq!(ts.mcp_server_name, "brave");
let default_config = ts.default_config.expect("default_config should be Some");
assert_eq!(default_config.defer_loading, Some(true));
}
other => panic!(
"Expected McpToolset, got {:?}",
std::mem::discriminant(&other)
),
}
}
#[test]
fn test_tool_search_tool_deserialization() {
let json = r#"{
"type": "tool_search_tool_regex_20251119",
"name": "tool_search_tool_regex"
}"#;
let tool: Tool = serde_json::from_str(json).expect("Failed to deserialize ToolSearch Tool");
match tool {
Tool::ToolSearch(ts) => {
assert_eq!(ts.name, "tool_search_tool_regex");
assert_eq!(ts.tool_type, "tool_search_tool_regex_20251119");
}
other => panic!(
"Expected ToolSearch, got {:?}",
std::mem::discriminant(&other)
),
}
}
#[test]
fn test_content_block_tool_search_tool_result_deserialization() {
let json = r#"{
"type": "tool_search_tool_result",
"tool_use_id": "srvtoolu_015dw5iXvktXLmqwpyzo4Dp2",
"content": {
"type": "tool_search_tool_search_result",
"tool_references": [
{"type": "tool_reference", "tool_name": "get_weather"}
]
}
}"#;
let block: ContentBlock = serde_json::from_str(json)
.expect("Failed to deserialize tool_search_tool_result ContentBlock");
match block {
ContentBlock::ToolSearchToolResult {
tool_use_id,
content,
} => {
assert_eq!(tool_use_id, "srvtoolu_015dw5iXvktXLmqwpyzo4Dp2");
assert_eq!(content.tool_references.len(), 1);
assert_eq!(content.tool_references[0].tool_name, "get_weather");
}
_ => panic!("Expected ToolSearchToolResult variant"),
}
}
#[test]
fn test_content_block_server_tool_use_deserialization() {
let json = r#"{
"type": "server_tool_use",
"id": "srvtoolu_015dw5iXvktXLmqwpyzo4Dp2",
"name": "tool_search_tool_regex",
"input": {"query": "weather"}
}"#;
let block: ContentBlock =
serde_json::from_str(json).expect("Failed to deserialize server_tool_use ContentBlock");
match block {
ContentBlock::ServerToolUse { id, name, input: _ } => {
assert_eq!(id, "srvtoolu_015dw5iXvktXLmqwpyzo4Dp2");
assert_eq!(name, "tool_search_tool_regex");
}
_ => panic!("Expected ServerToolUse variant"),
}
}
#[test]
fn test_content_block_tool_reference_deserialization() {
let json = r#"{
"type": "tool_reference",
"tool_name": "get_weather",
"description": "Get the weather for a location"
}"#;
let block: ContentBlock =
serde_json::from_str(json).expect("Failed to deserialize tool_reference ContentBlock");
match block {
ContentBlock::ToolReference {
tool_name,
description,
} => {
assert_eq!(tool_name, "get_weather");
assert_eq!(description.unwrap(), "Get the weather for a location");
}
_ => panic!("Expected ToolReference variant"),
}
}
#[test]
fn test_tool_choice_auto_requires_tools() {
let mut request = base_request();
request.tool_choice = Some(ToolChoice::Auto {
disable_parallel_tool_use: None,
});
assert!(request.validate().is_err());
}
#[test]
fn test_tool_choice_any_requires_tools() {
let mut request = base_request();
request.tool_choice = Some(ToolChoice::Any {
disable_parallel_tool_use: None,
});
assert!(request.validate().is_err());
}
#[test]
fn test_tool_choice_auto_with_tools_is_valid() {
let mut request = base_request();
request.tool_choice = Some(ToolChoice::Auto {
disable_parallel_tool_use: None,
});
request.tools = Some(vec![custom_tool("get_weather")]);
assert!(request.validate().is_ok());
}
#[test]
fn test_tool_choice_any_with_tools_is_valid() {
let mut request = base_request();
request.tool_choice = Some(ToolChoice::Any {
disable_parallel_tool_use: None,
});
request.tools = Some(vec![custom_tool("get_weather")]);
assert!(request.validate().is_ok());
}
#[test]
fn test_tool_choice_specific_tool_requires_tools() {
let mut request = base_request();
request.tool_choice = Some(ToolChoice::Tool {
name: "get_weather".to_string(),
disable_parallel_tool_use: None,
});
assert!(request.validate().is_err());
}
#[test]
fn test_tool_choice_specific_tool_must_exist() {
let mut request = base_request();
request.tool_choice = Some(ToolChoice::Tool {
name: "get_weather".to_string(),
disable_parallel_tool_use: None,
});
request.tools = Some(vec![custom_tool("search_web")]);
assert!(request.validate().is_err());
}
#[test]
fn test_tool_choice_none_without_tools_is_valid() {
let mut request = base_request();
request.tool_choice = Some(ToolChoice::None);
assert!(request.validate().is_ok());
}
#[test]
fn test_tool_choice_specific_tool_is_valid_when_declared() {
let mut request = base_request();
request.tool_choice = Some(ToolChoice::Tool {
name: "get_weather".to_string(),
disable_parallel_tool_use: None,
});
request.tools = Some(vec![custom_tool("get_weather")]);
assert!(request.validate().is_ok());
}
#[test]
fn test_tool_choice_specific_tool_is_valid_with_mcp_toolset() {
let mut request = base_request();
request.tool_choice = Some(ToolChoice::Tool {
name: "get_weather".to_string(),
disable_parallel_tool_use: None,
});
request.tools = Some(vec![mcp_toolset(None)]);
request.mcp_servers = Some(vec![mcp_server_config()]);
assert!(request.validate().is_ok());
}
#[test]
fn test_tool_choice_specific_tool_uses_mcp_toolset_default_when_override_missing() {
let mut request = base_request();
request.tool_choice = Some(ToolChoice::Tool {
name: "get_weather".to_string(),
disable_parallel_tool_use: None,
});
request.tools = Some(vec![mcp_toolset(Some(HashMap::from([(
"search_web".to_string(),
McpToolConfig {
enabled: Some(false),
defer_loading: None,
},
)])))]);
request.mcp_servers = Some(vec![mcp_server_config()]);
assert!(request.validate().is_ok());
}
#[test]
fn test_tool_choice_specific_tool_must_be_enabled_in_mcp_toolset_configs() {
let mut request = base_request();
request.tool_choice = Some(ToolChoice::Tool {
name: "get_weather".to_string(),
disable_parallel_tool_use: None,
});
request.tools = Some(vec![mcp_toolset(Some(HashMap::from([(
"get_weather".to_string(),
McpToolConfig {
enabled: Some(false),
defer_loading: None,
},
)])))]);
request.mcp_servers = Some(vec![mcp_server_config()]);
assert!(request.validate().is_err());
}
#[test]
fn test_full_message_with_tool_search_flow_deserialization() {
let json = r#"{
"id": "msg_01TEST",
"type": "message",
"role": "assistant",
"model": "claude-sonnet-4-5-20250929",
"content": [
{
"type": "server_tool_use",
"id": "srvtoolu_015dw5iXvktXLmqwpyzo4Dp2",
"name": "tool_search_tool_regex",
"input": {"query": "weather"}
},
{
"type": "tool_search_tool_result",
"tool_use_id": "srvtoolu_015dw5iXvktXLmqwpyzo4Dp2",
"content": {
"type": "tool_search_tool_search_result",
"tool_references": [
{"type": "tool_reference", "tool_name": "get_weather"}
]
}
},
{
"type": "tool_use",
"id": "toolu_01ABC",
"name": "get_weather",
"input": {"location": "San Francisco"}
}
],
"stop_reason": "tool_use",
"stop_sequence": null,
"usage": {
"input_tokens": 100,
"output_tokens": 50
}
}"#;
let msg: Message = serde_json::from_str(json)
.expect("Failed to deserialize Message with tool search flow");
assert_eq!(msg.content.len(), 3);
assert!(matches!(msg.content[0], ContentBlock::ServerToolUse { .. }));
assert!(matches!(
msg.content[1],
ContentBlock::ToolSearchToolResult { .. }
));
assert!(matches!(msg.content[2], ContentBlock::ToolUse { .. }));
}
}