use derive_builder::Builder;
use serde::Deserialize;
use serde::Serialize;
use super::common::Metadata;
use super::common::Usage;
use super::content::ContentBlock;
use super::content::ContentBlockConversionError;
use super::content::ContentBlockParam;
use super::content::MessageContentParam;
use super::content::MessageParam;
use super::content::MessageRole;
use super::content::SystemParam;
use super::tools::Tool;
use super::tools::ToolChoice;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(tag = "type", rename_all = "snake_case")]
#[expect(clippy::derive_partial_eq_without_eq)] pub enum OutputFormat {
#[serde(rename = "json_schema")]
JsonSchema {
schema: serde_json::Value,
},
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(tag = "type", rename_all = "snake_case")]
#[non_exhaustive]
pub enum ThinkingConfig {
Enabled {
budget_tokens: u32,
},
Disabled,
Adaptive,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
pub struct OutputConfig {
#[serde(skip_serializing_if = "Option::is_none")]
pub format: Option<OutputFormat>,
#[serde(skip_serializing_if = "Option::is_none")]
pub effort: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum ServiceTier {
Auto,
StandardOnly,
}
#[derive(Debug, Clone, Deserialize, PartialEq, Builder, Default)]
#[builder(setter(into, strip_option), default)]
pub struct MessagesCreateRequest {
#[builder(default)]
pub model: String,
#[builder(default)]
pub max_tokens: u32,
#[serde(skip_serializing_if = "Option::is_none")]
pub system: Option<SystemParam>,
#[builder(default)]
pub messages: Vec<MessageParam>,
#[serde(skip_serializing_if = "Option::is_none")]
pub temperature: Option<f32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub stop_sequences: Option<Vec<String>>,
#[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 metadata: Option<Metadata>,
#[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 stream: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub thinking: Option<ThinkingConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
pub output_config: Option<OutputConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
pub service_tier: Option<ServiceTier>,
#[serde(skip_serializing_if = "Option::is_none")]
pub inference_geo: Option<String>,
#[deprecated(note = "Use `output_config` with `format` field instead")]
#[serde(skip)]
pub output_format: Option<OutputFormat>,
}
impl Serialize for MessagesCreateRequest {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
use serde::ser::SerializeMap;
let mut field_count = 2; field_count += usize::from(self.system.is_some());
field_count += 1; field_count += usize::from(self.temperature.is_some());
field_count += usize::from(self.stop_sequences.is_some());
field_count += usize::from(self.top_p.is_some());
field_count += usize::from(self.top_k.is_some());
field_count += usize::from(self.metadata.is_some());
field_count += usize::from(self.tools.is_some());
field_count += usize::from(self.tool_choice.is_some());
field_count += usize::from(self.stream.is_some());
field_count += usize::from(self.thinking.is_some());
field_count += usize::from(self.service_tier.is_some());
field_count += usize::from(self.inference_geo.is_some());
#[expect(deprecated)]
let effective_output_config = if self.output_config.is_some() {
self.output_config.clone()
} else if self.output_format.is_some() {
Some(OutputConfig {
format: self.output_format.clone(),
effort: None,
})
} else {
None
};
field_count += usize::from(effective_output_config.is_some());
let mut map = serializer.serialize_map(Some(field_count))?;
map.serialize_entry("model", &self.model)?;
map.serialize_entry("max_tokens", &self.max_tokens)?;
if let Some(ref system) = self.system {
map.serialize_entry("system", system)?;
}
map.serialize_entry("messages", &self.messages)?;
if let Some(ref temperature) = self.temperature {
map.serialize_entry("temperature", temperature)?;
}
if let Some(ref stop_sequences) = self.stop_sequences {
map.serialize_entry("stop_sequences", stop_sequences)?;
}
if let Some(ref top_p) = self.top_p {
map.serialize_entry("top_p", top_p)?;
}
if let Some(ref top_k) = self.top_k {
map.serialize_entry("top_k", top_k)?;
}
if let Some(ref metadata) = self.metadata {
map.serialize_entry("metadata", metadata)?;
}
if let Some(ref tools) = self.tools {
map.serialize_entry("tools", tools)?;
}
if let Some(ref tool_choice) = self.tool_choice {
map.serialize_entry("tool_choice", tool_choice)?;
}
if let Some(ref stream) = self.stream {
map.serialize_entry("stream", stream)?;
}
if let Some(ref thinking) = self.thinking {
map.serialize_entry("thinking", thinking)?;
}
if let Some(ref output_config) = effective_output_config {
map.serialize_entry("output_config", output_config)?;
}
if let Some(ref service_tier) = self.service_tier {
map.serialize_entry("service_tier", service_tier)?;
}
if let Some(ref inference_geo) = self.inference_geo {
map.serialize_entry("inference_geo", inference_geo)?;
}
map.end()
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct MessagesCreateResponse {
pub id: String,
#[serde(rename = "type")]
pub kind: String,
pub role: MessageRole,
pub content: Vec<ContentBlock>,
pub model: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub stop_reason: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub usage: Option<Usage>,
}
impl MessagesCreateResponse {
pub fn try_into_message_param(&self) -> Result<MessageParam, ContentBlockConversionError> {
let blocks = self
.content
.iter()
.map(ContentBlockParam::try_from)
.collect::<Result<Vec<_>, _>>()?;
Ok(MessageParam {
role: self.role.clone(),
content: MessageContentParam::Blocks(blocks),
})
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct MessageTokensCountRequest {
pub model: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub system: Option<SystemParam>,
pub messages: Vec<MessageParam>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tools: Option<Vec<Tool>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_choice: Option<ToolChoice>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct MessageTokensCountResponse {
pub input_tokens: u64,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::content::ContentBlockParam;
use crate::types::content::MessageContentParam;
#[test]
fn message_request_ser() {
let req = MessagesCreateRequest {
model: "claude-sonnet-4-6".into(),
max_tokens: 128,
messages: vec![MessageParam {
role: MessageRole::User,
content: "Hello".into(),
}],
..Default::default()
};
let s = serde_json::to_string(&req).unwrap();
assert!(s.contains(r#""model":"claude-sonnet-4-6""#));
assert!(s.contains(r#""max_tokens":128"#));
assert!(s.contains(r#""Hello""#));
assert!(!s.contains("stream"));
assert!(!s.contains("output_format"));
assert!(!s.contains("output_config"));
assert!(!s.contains("thinking"));
}
#[test]
fn message_request_with_system_string() {
let req = MessagesCreateRequest {
model: "claude-sonnet-4-6".into(),
max_tokens: 128,
system: Some("You are helpful".into()),
messages: vec![MessageParam {
role: MessageRole::User,
content: "Hello".into(),
}],
..Default::default()
};
let s = serde_json::to_string(&req).unwrap();
assert!(s.contains(r#""system":"You are helpful""#));
}
#[test]
fn message_request_with_blocks() {
let req = MessagesCreateRequest {
model: "claude-sonnet-4-6".into(),
max_tokens: 128,
messages: vec![MessageParam {
role: MessageRole::User,
content: MessageContentParam::Blocks(vec![ContentBlockParam::Text {
text: "Block content".into(),
citations: None,
cache_control: None,
}]),
}],
temperature: Some(0.7),
..Default::default()
};
let s = serde_json::to_string(&req).unwrap();
assert!(s.contains(r#""Block content""#));
assert!(s.contains(r#""temperature":0.7"#));
}
#[test]
fn thinking_config_enabled_ser() {
let t = ThinkingConfig::Enabled {
budget_tokens: 2048,
};
let s = serde_json::to_string(&t).unwrap();
assert!(s.contains(r#""type":"enabled""#));
assert!(s.contains(r#""budget_tokens":2048"#));
}
#[test]
fn thinking_config_disabled_ser() {
let t = ThinkingConfig::Disabled;
let s = serde_json::to_string(&t).unwrap();
assert!(s.contains(r#""type":"disabled""#));
}
#[test]
fn thinking_config_adaptive_ser() {
let t = ThinkingConfig::Adaptive;
let s = serde_json::to_string(&t).unwrap();
assert!(s.contains(r#""type":"adaptive""#));
}
#[test]
fn service_tier_ser() {
assert_eq!(
serde_json::to_string(&ServiceTier::Auto).unwrap(),
r#""auto""#
);
assert_eq!(
serde_json::to_string(&ServiceTier::StandardOnly).unwrap(),
r#""standard_only""#
);
}
#[test]
fn output_config_ser() {
let oc = OutputConfig {
format: Some(OutputFormat::JsonSchema {
schema: serde_json::json!({"type": "object"}),
}),
effort: Some("high".into()),
};
let s = serde_json::to_string(&oc).unwrap();
assert!(s.contains(r#""effort":"high""#));
assert!(s.contains(r#""format""#));
assert!(s.contains(r#""json_schema""#));
}
#[test]
fn message_request_with_thinking() {
let req = MessagesCreateRequest {
model: "claude-3-5-sonnet-20241022".into(),
max_tokens: 16000,
messages: vec![MessageParam {
role: MessageRole::User,
content: "Solve this problem".into(),
}],
thinking: Some(ThinkingConfig::Enabled {
budget_tokens: 4096,
}),
..Default::default()
};
let s = serde_json::to_string(&req).unwrap();
assert!(s.contains(r#""thinking""#));
assert!(s.contains(r#""type":"enabled""#));
assert!(s.contains(r#""budget_tokens":4096"#));
}
#[test]
fn message_request_with_service_tier() {
let req = MessagesCreateRequest {
model: "claude-3-5-sonnet-20241022".into(),
max_tokens: 128,
messages: vec![],
service_tier: Some(ServiceTier::Auto),
inference_geo: Some("us".into()),
..Default::default()
};
let s = serde_json::to_string(&req).unwrap();
assert!(s.contains(r#""service_tier":"auto""#));
assert!(s.contains(r#""inference_geo":"us""#));
}
#[test]
#[expect(deprecated)]
fn output_format_bridges_to_output_config() {
let req = MessagesCreateRequest {
model: "claude-3-5-sonnet-20241022".into(),
max_tokens: 128,
messages: vec![],
output_format: Some(OutputFormat::JsonSchema {
schema: serde_json::json!({"type": "string"}),
}),
..Default::default()
};
let s = serde_json::to_string(&req).unwrap();
assert!(!s.contains(r#""output_format""#));
assert!(s.contains(r#""output_config""#));
assert!(s.contains(r#""format""#));
assert!(s.contains(r#""json_schema""#));
}
#[test]
#[expect(deprecated)]
fn output_config_takes_precedence_over_output_format() {
let req = MessagesCreateRequest {
model: "claude-3-5-sonnet-20241022".into(),
max_tokens: 128,
messages: vec![],
output_config: Some(OutputConfig {
format: None,
effort: Some("high".into()),
}),
output_format: Some(OutputFormat::JsonSchema {
schema: serde_json::json!({"type": "string"}),
}),
..Default::default()
};
let s = serde_json::to_string(&req).unwrap();
assert!(s.contains(r#""effort":"high""#));
assert!(!s.contains(r#""json_schema""#));
}
#[test]
fn neither_output_format_nor_output_config_set() {
let req = MessagesCreateRequest {
model: "claude-3-5-sonnet-20241022".into(),
max_tokens: 128,
messages: vec![],
..Default::default()
};
let s = serde_json::to_string(&req).unwrap();
assert!(!s.contains("output_config"));
assert!(!s.contains("output_format"));
}
#[test]
fn try_into_message_param_with_text() {
use crate::types::content::ContentBlock;
let response = MessagesCreateResponse {
id: "msg_123".into(),
kind: "message".into(),
role: MessageRole::Assistant,
content: vec![ContentBlock::Text {
text: "Hello, world!".into(),
citations: None,
}],
model: "claude-3-5-sonnet-20241022".into(),
stop_reason: Some("end_turn".into()),
usage: None,
};
let message_param = response.try_into_message_param().unwrap();
assert_eq!(message_param.role, MessageRole::Assistant);
match message_param.content {
MessageContentParam::Blocks(blocks) => {
assert_eq!(blocks.len(), 1);
match &blocks[0] {
ContentBlockParam::Text { text, .. } => {
assert_eq!(text, "Hello, world!");
}
_ => panic!("Expected Text block"),
}
}
MessageContentParam::String(_) => panic!("Expected Blocks content"),
}
}
#[test]
fn try_into_message_param_with_tool_use() {
use crate::types::content::ContentBlock;
let response = MessagesCreateResponse {
id: "msg_456".into(),
kind: "message".into(),
role: MessageRole::Assistant,
content: vec![
ContentBlock::Text {
text: "Let me check the weather.".into(),
citations: None,
},
ContentBlock::ToolUse {
id: "toolu_abc123".into(),
name: "get_weather".into(),
input: serde_json::json!({"location": "Paris"}),
},
],
model: "claude-3-5-sonnet-20241022".into(),
stop_reason: Some("tool_use".into()),
usage: None,
};
let message_param = response.try_into_message_param().unwrap();
assert_eq!(message_param.role, MessageRole::Assistant);
match message_param.content {
MessageContentParam::Blocks(blocks) => {
assert_eq!(blocks.len(), 2);
assert!(matches!(&blocks[0], ContentBlockParam::Text { .. }));
match &blocks[1] {
ContentBlockParam::ToolUse {
id, name, input, ..
} => {
assert_eq!(id, "toolu_abc123");
assert_eq!(name, "get_weather");
assert_eq!(input["location"], "Paris");
}
_ => panic!("Expected ToolUse block"),
}
}
MessageContentParam::String(_) => panic!("Expected Blocks content"),
}
}
#[test]
fn try_into_message_param_fails_on_unknown_block() {
use crate::types::content::ContentBlock;
use crate::types::content::ContentBlockConversionError;
let response = MessagesCreateResponse {
id: "msg_789".into(),
kind: "message".into(),
role: MessageRole::Assistant,
content: vec![ContentBlock::Unknown],
model: "claude-3-5-sonnet-20241022".into(),
stop_reason: None,
usage: None,
};
let result = response.try_into_message_param();
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
ContentBlockConversionError::UnknownContentBlock
));
}
}