use serde::de::Error as _;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use serde_json::Value;
use crate::error::{ApiError, Error};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Role {
User,
Assistant,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum Content {
Text(String),
Blocks(Vec<ContentBlock>),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Message {
pub role: Role,
pub content: Content,
}
impl Message {
pub fn user(text: impl Into<String>) -> Self {
Self {
role: Role::User,
content: Content::Text(text.into()),
}
}
pub fn assistant(text: impl Into<String>) -> Self {
Self {
role: Role::Assistant,
content: Content::Text(text.into()),
}
}
pub fn user_blocks(blocks: Vec<ContentBlock>) -> Self {
Self {
role: Role::User,
content: Content::Blocks(blocks),
}
}
pub fn assistant_blocks(blocks: Vec<ContentBlock>) -> Self {
Self {
role: Role::Assistant,
content: Content::Blocks(blocks),
}
}
pub fn tool_result(tool_use_id: impl Into<String>, content: impl Into<String>) -> Self {
Self::user_blocks(vec![ContentBlock::tool_result(tool_use_id, content)])
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum ToolResultContent {
Text(String),
Blocks(Vec<ContentBlock>),
}
impl Default for ToolResultContent {
fn default() -> Self {
ToolResultContent::Text(String::new())
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum Source {
Base64 {
media_type: String,
data: String,
},
Url {
url: String,
},
Unknown(Value),
}
impl Serialize for Source {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
use serde::ser::SerializeMap;
match self {
Source::Base64 { media_type, data } => {
let mut map = serializer.serialize_map(Some(3))?;
map.serialize_entry("type", "base64")?;
map.serialize_entry("media_type", media_type)?;
map.serialize_entry("data", data)?;
map.end()
}
Source::Url { url } => {
let mut map = serializer.serialize_map(Some(2))?;
map.serialize_entry("type", "url")?;
map.serialize_entry("url", url)?;
map.end()
}
Source::Unknown(value) => value.serialize(serializer),
}
}
}
impl<'de> Deserialize<'de> for Source {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let value = Value::deserialize(deserializer)?;
match value.get("type").and_then(Value::as_str) {
Some("base64") => {
let raw: RawBase64Source =
serde_json::from_value(value).map_err(D::Error::custom)?;
Ok(Source::Base64 {
media_type: raw.media_type,
data: raw.data,
})
}
Some("url") => {
let raw: RawUrlSource = serde_json::from_value(value).map_err(D::Error::custom)?;
Ok(Source::Url { url: raw.url })
}
_ => Ok(Source::Unknown(value)),
}
}
}
#[derive(Deserialize)]
struct RawBase64Source {
#[serde(default)]
media_type: String,
#[serde(default)]
data: String,
}
#[derive(Deserialize)]
struct RawUrlSource {
#[serde(default)]
url: String,
}
#[derive(Debug, Clone, PartialEq)]
pub enum ContentBlock {
Text {
text: String,
},
ToolUse {
id: String,
name: String,
input: Value,
},
ToolResult {
tool_use_id: String,
content: ToolResultContent,
is_error: Option<bool>,
},
Thinking {
thinking: String,
signature: Option<String>,
},
Image {
source: Source,
},
Document {
source: Source,
},
Unknown(Value),
}
impl ContentBlock {
pub fn text(text: impl Into<String>) -> Self {
ContentBlock::Text { text: text.into() }
}
pub fn tool_result(tool_use_id: impl Into<String>, content: impl Into<String>) -> Self {
ContentBlock::ToolResult {
tool_use_id: tool_use_id.into(),
content: ToolResultContent::Text(content.into()),
is_error: None,
}
}
pub fn tool_error(tool_use_id: impl Into<String>, content: impl Into<String>) -> Self {
ContentBlock::ToolResult {
tool_use_id: tool_use_id.into(),
content: ToolResultContent::Text(content.into()),
is_error: Some(true),
}
}
pub fn image_base64(media_type: impl Into<String>, data: impl Into<String>) -> Self {
ContentBlock::Image {
source: Source::Base64 {
media_type: media_type.into(),
data: data.into(),
},
}
}
pub fn image_url(url: impl Into<String>) -> Self {
ContentBlock::Image {
source: Source::Url { url: url.into() },
}
}
pub fn document_base64(media_type: impl Into<String>, data: impl Into<String>) -> Self {
ContentBlock::Document {
source: Source::Base64 {
media_type: media_type.into(),
data: data.into(),
},
}
}
pub fn document_pdf(data: impl Into<String>) -> Self {
ContentBlock::document_base64("application/pdf", data)
}
pub fn document_url(url: impl Into<String>) -> Self {
ContentBlock::Document {
source: Source::Url { url: url.into() },
}
}
#[must_use]
pub fn as_text(&self) -> Option<&str> {
match self {
ContentBlock::Text { text } => Some(text),
_ => None,
}
}
#[must_use]
pub fn as_tool_use(&self) -> Option<(&str, &str, &Value)> {
match self {
ContentBlock::ToolUse { id, name, input } => Some((id, name, input)),
_ => None,
}
}
}
#[derive(Deserialize)]
struct RawText {
#[serde(default)]
text: String,
}
#[derive(Deserialize)]
struct RawToolUse {
#[serde(default)]
id: String,
#[serde(default)]
name: String,
#[serde(default)]
input: Value,
}
#[derive(Deserialize)]
struct RawToolResult {
#[serde(default)]
tool_use_id: String,
#[serde(default)]
content: ToolResultContent,
#[serde(default)]
is_error: Option<bool>,
}
#[derive(Deserialize)]
struct RawThinking {
#[serde(default)]
thinking: String,
#[serde(default)]
signature: Option<String>,
}
#[derive(Deserialize)]
struct RawSourced {
source: Source,
}
impl Serialize for ContentBlock {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
use serde::ser::SerializeMap;
match self {
ContentBlock::Text { text } => {
let mut map = serializer.serialize_map(Some(2))?;
map.serialize_entry("type", "text")?;
map.serialize_entry("text", text)?;
map.end()
}
ContentBlock::ToolUse { id, name, input } => {
let mut map = serializer.serialize_map(Some(4))?;
map.serialize_entry("type", "tool_use")?;
map.serialize_entry("id", id)?;
map.serialize_entry("name", name)?;
map.serialize_entry("input", input)?;
map.end()
}
ContentBlock::ToolResult {
tool_use_id,
content,
is_error,
} => {
let len = if is_error.is_some() { 4 } else { 3 };
let mut map = serializer.serialize_map(Some(len))?;
map.serialize_entry("type", "tool_result")?;
map.serialize_entry("tool_use_id", tool_use_id)?;
map.serialize_entry("content", content)?;
if let Some(is_error) = is_error {
map.serialize_entry("is_error", is_error)?;
}
map.end()
}
ContentBlock::Thinking {
thinking,
signature,
} => {
let len = if signature.is_some() { 3 } else { 2 };
let mut map = serializer.serialize_map(Some(len))?;
map.serialize_entry("type", "thinking")?;
map.serialize_entry("thinking", thinking)?;
if let Some(signature) = signature {
map.serialize_entry("signature", signature)?;
}
map.end()
}
ContentBlock::Image { source } => {
let mut map = serializer.serialize_map(Some(2))?;
map.serialize_entry("type", "image")?;
map.serialize_entry("source", source)?;
map.end()
}
ContentBlock::Document { source } => {
let mut map = serializer.serialize_map(Some(2))?;
map.serialize_entry("type", "document")?;
map.serialize_entry("source", source)?;
map.end()
}
ContentBlock::Unknown(value) => value.serialize(serializer),
}
}
}
impl<'de> Deserialize<'de> for ContentBlock {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let value = Value::deserialize(deserializer)?;
match value.get("type").and_then(Value::as_str) {
Some("text") => {
let raw: RawText = serde_json::from_value(value).map_err(D::Error::custom)?;
Ok(ContentBlock::Text { text: raw.text })
}
Some("tool_use") => {
let raw: RawToolUse = serde_json::from_value(value).map_err(D::Error::custom)?;
Ok(ContentBlock::ToolUse {
id: raw.id,
name: raw.name,
input: raw.input,
})
}
Some("tool_result") => {
let raw: RawToolResult = serde_json::from_value(value).map_err(D::Error::custom)?;
Ok(ContentBlock::ToolResult {
tool_use_id: raw.tool_use_id,
content: raw.content,
is_error: raw.is_error,
})
}
Some("thinking") => {
let raw: RawThinking = serde_json::from_value(value).map_err(D::Error::custom)?;
Ok(ContentBlock::Thinking {
thinking: raw.thinking,
signature: raw.signature,
})
}
Some("image") => match serde_json::from_value::<RawSourced>(value.clone()) {
Ok(raw) => Ok(ContentBlock::Image { source: raw.source }),
Err(_) => Ok(ContentBlock::Unknown(value)),
},
Some("document") => match serde_json::from_value::<RawSourced>(value.clone()) {
Ok(raw) => Ok(ContentBlock::Document { source: raw.source }),
Err(_) => Ok(ContentBlock::Unknown(value)),
},
_ => Ok(ContentBlock::Unknown(value)),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Tool {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
pub input_schema: Value,
}
impl Tool {
pub fn new(
name: impl Into<String>,
description: impl Into<String>,
input_schema: Value,
) -> Self {
Self {
name: name.into(),
description: Some(description.into()),
input_schema,
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ToolChoice {
Auto {
#[serde(skip_serializing_if = "Option::is_none", default)]
disable_parallel_tool_use: Option<bool>,
},
Any {
#[serde(skip_serializing_if = "Option::is_none", default)]
disable_parallel_tool_use: Option<bool>,
},
Tool {
name: String,
#[serde(skip_serializing_if = "Option::is_none", default)]
disable_parallel_tool_use: Option<bool>,
},
}
impl ToolChoice {
#[must_use]
pub fn auto() -> Self {
ToolChoice::Auto {
disable_parallel_tool_use: None,
}
}
#[must_use]
pub fn any() -> Self {
ToolChoice::Any {
disable_parallel_tool_use: None,
}
}
pub fn tool(name: impl Into<String>) -> Self {
ToolChoice::Tool {
name: name.into(),
disable_parallel_tool_use: None,
}
}
#[must_use]
pub fn with_disable_parallel_tool_use(mut self, disable: bool) -> Self {
match &mut self {
ToolChoice::Auto {
disable_parallel_tool_use,
}
| ToolChoice::Any {
disable_parallel_tool_use,
}
| ToolChoice::Tool {
disable_parallel_tool_use,
..
} => {
*disable_parallel_tool_use = Some(disable);
}
}
self
}
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
pub struct CacheControl {
#[serde(default)]
pub ttl: Option<String>,
}
impl CacheControl {
#[must_use]
pub fn ephemeral() -> Self {
Self { ttl: None }
}
pub fn ephemeral_ttl(ttl: impl Into<String>) -> Self {
Self {
ttl: Some(ttl.into()),
}
}
}
impl Serialize for CacheControl {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
use serde::ser::SerializeMap;
let len = if self.ttl.is_some() { 2 } else { 1 };
let mut map = serializer.serialize_map(Some(len))?;
map.serialize_entry("type", "ephemeral")?;
if let Some(ttl) = &self.ttl {
map.serialize_entry("ttl", ttl)?;
}
map.end()
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct SystemBlock {
pub text: String,
pub cache_control: Option<CacheControl>,
}
impl SystemBlock {
pub fn text(text: impl Into<String>) -> Self {
Self {
text: text.into(),
cache_control: None,
}
}
#[must_use]
pub fn with_cache_control(mut self, cache_control: CacheControl) -> Self {
self.cache_control = Some(cache_control);
self
}
}
#[derive(Deserialize)]
struct RawSystemBlock {
#[serde(default)]
text: String,
#[serde(default)]
cache_control: Option<CacheControl>,
}
impl Serialize for SystemBlock {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
use serde::ser::SerializeMap;
let len = if self.cache_control.is_some() { 3 } else { 2 };
let mut map = serializer.serialize_map(Some(len))?;
map.serialize_entry("type", "text")?;
map.serialize_entry("text", &self.text)?;
if let Some(cache_control) = &self.cache_control {
map.serialize_entry("cache_control", cache_control)?;
}
map.end()
}
}
impl<'de> Deserialize<'de> for SystemBlock {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let raw = RawSystemBlock::deserialize(deserializer)?;
Ok(SystemBlock {
text: raw.text,
cache_control: raw.cache_control,
})
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum System {
Text(String),
Blocks(Vec<SystemBlock>),
}
impl System {
pub fn blocks(blocks: impl IntoIterator<Item = SystemBlock>) -> Self {
System::Blocks(blocks.into_iter().collect())
}
pub fn leading(identity: SystemBlock, rest: impl IntoIterator<Item = SystemBlock>) -> Self {
let mut blocks = vec![identity];
blocks.extend(rest);
System::Blocks(blocks)
}
}
impl From<String> for System {
fn from(text: String) -> Self {
System::Text(text)
}
}
impl From<&str> for System {
fn from(text: &str) -> Self {
System::Text(text.to_owned())
}
}
#[derive(Debug, Clone, Serialize)]
pub struct MessageRequest {
pub model: String,
pub max_tokens: u32,
#[serde(skip_serializing_if = "Option::is_none")]
pub system: Option<System>,
pub messages: Vec<Message>,
#[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 stop_sequences: Option<Vec<String>>,
#[serde(skip_serializing_if = "is_false")]
pub stream: bool,
}
fn is_false(value: &bool) -> bool {
!*value
}
impl MessageRequest {
pub fn new(model: impl Into<String>, max_tokens: u32) -> Self {
Self {
model: model.into(),
max_tokens,
system: None,
messages: Vec::new(),
tools: None,
tool_choice: None,
stop_sequences: None,
stream: false,
}
}
#[must_use]
pub fn with_system(mut self, system: impl Into<System>) -> Self {
self.system = Some(system.into());
self
}
#[must_use]
pub fn with_messages(mut self, messages: Vec<Message>) -> Self {
self.messages = messages;
self
}
#[must_use]
pub fn push_message(mut self, message: Message) -> Self {
self.messages.push(message);
self
}
#[must_use]
pub fn with_tools(mut self, tools: Vec<Tool>) -> Self {
self.tools = Some(tools);
self
}
#[must_use]
pub fn with_tool_choice(mut self, tool_choice: ToolChoice) -> Self {
self.tool_choice = Some(tool_choice);
self
}
#[must_use]
pub fn with_stop_sequences(mut self, stop_sequences: Vec<String>) -> Self {
self.stop_sequences = Some(stop_sequences);
self
}
#[must_use]
pub fn with_stream(mut self, stream: bool) -> Self {
self.stream = stream;
self
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum StopReason {
EndTurn,
MaxTokens,
StopSequence,
ToolUse,
PauseTurn,
Refusal,
Other(String),
}
impl StopReason {
#[must_use]
pub fn as_str(&self) -> &str {
match self {
StopReason::EndTurn => "end_turn",
StopReason::MaxTokens => "max_tokens",
StopReason::StopSequence => "stop_sequence",
StopReason::ToolUse => "tool_use",
StopReason::PauseTurn => "pause_turn",
StopReason::Refusal => "refusal",
StopReason::Other(other) => other,
}
}
}
impl Serialize for StopReason {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(self.as_str())
}
}
impl<'de> Deserialize<'de> for StopReason {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let value = String::deserialize(deserializer)?;
Ok(match value.as_str() {
"end_turn" => StopReason::EndTurn,
"max_tokens" => StopReason::MaxTokens,
"stop_sequence" => StopReason::StopSequence,
"tool_use" => StopReason::ToolUse,
"pause_turn" => StopReason::PauseTurn,
"refusal" => StopReason::Refusal,
_ => StopReason::Other(value),
})
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Deserialize)]
pub struct Usage {
pub input_tokens: u64,
pub output_tokens: u64,
#[serde(default)]
pub cache_creation_input_tokens: Option<u64>,
#[serde(default)]
pub cache_read_input_tokens: Option<u64>,
}
#[derive(Debug, Clone, PartialEq, Deserialize)]
pub struct MessageResponse {
pub id: String,
pub model: String,
pub role: Role,
pub content: Vec<ContentBlock>,
#[serde(default)]
pub stop_reason: Option<StopReason>,
#[serde(default)]
pub stop_sequence: Option<String>,
pub usage: Usage,
}
impl MessageResponse {
#[must_use]
pub fn text(&self) -> String {
self.content
.iter()
.filter_map(ContentBlock::as_text)
.collect()
}
#[must_use]
pub fn tool_uses(&self) -> Vec<(&str, &str, &Value)> {
self.content
.iter()
.filter_map(ContentBlock::as_tool_use)
.collect()
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Deserialize)]
pub struct MessageDeltaUsage {
#[serde(default)]
pub input_tokens: Option<u64>,
#[serde(default)]
pub output_tokens: Option<u64>,
#[serde(default)]
pub cache_creation_input_tokens: Option<u64>,
#[serde(default)]
pub cache_read_input_tokens: Option<u64>,
}
#[derive(Debug, Clone, PartialEq)]
pub enum ContentDelta {
Text {
text: String,
},
InputJson {
partial_json: String,
},
Thinking {
thinking: String,
},
Signature {
signature: String,
},
Unknown(Value),
}
impl ContentDelta {
fn from_value(value: Value) -> Self {
match value.get("type").and_then(Value::as_str) {
Some("text_delta") => ContentDelta::Text {
text: string_field(&value, "text"),
},
Some("input_json_delta") => ContentDelta::InputJson {
partial_json: string_field(&value, "partial_json"),
},
Some("thinking_delta") => ContentDelta::Thinking {
thinking: string_field(&value, "thinking"),
},
Some("signature_delta") => ContentDelta::Signature {
signature: string_field(&value, "signature"),
},
_ => ContentDelta::Unknown(value),
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum StreamEvent {
MessageStart(MessageResponse),
ContentBlockStart {
index: usize,
content_block: ContentBlock,
},
ContentBlockDelta {
index: usize,
delta: ContentDelta,
},
ContentBlockStop {
index: usize,
},
MessageDelta {
stop_reason: Option<StopReason>,
stop_sequence: Option<String>,
usage: MessageDeltaUsage,
},
MessageStop,
Ping,
Unknown(Value),
}
impl StreamEvent {
pub(crate) fn from_value(value: Value) -> Result<Self, Error> {
match value.get("type").and_then(Value::as_str) {
Some("message_start") => {
let message = value.get("message").cloned().unwrap_or(Value::Null);
let message: MessageResponse =
serde_json::from_value(message).map_err(Error::Decode)?;
Ok(StreamEvent::MessageStart(message))
}
Some("content_block_start") => {
let block = value.get("content_block").cloned().unwrap_or(Value::Null);
let content_block: ContentBlock =
serde_json::from_value(block).map_err(Error::Decode)?;
Ok(StreamEvent::ContentBlockStart {
index: index_of(&value),
content_block,
})
}
Some("content_block_delta") => Ok(StreamEvent::ContentBlockDelta {
index: index_of(&value),
delta: ContentDelta::from_value(value.get("delta").cloned().unwrap_or(Value::Null)),
}),
Some("content_block_stop") => Ok(StreamEvent::ContentBlockStop {
index: index_of(&value),
}),
Some("message_delta") => {
let delta = value.get("delta").cloned().unwrap_or(Value::Null);
let stop_reason = delta
.get("stop_reason")
.filter(|reason| !reason.is_null())
.cloned()
.map(serde_json::from_value)
.transpose()
.map_err(Error::Decode)?;
let stop_sequence = delta
.get("stop_sequence")
.and_then(Value::as_str)
.map(str::to_owned);
let usage = value
.get("usage")
.cloned()
.map(serde_json::from_value)
.transpose()
.map_err(Error::Decode)?
.unwrap_or_default();
Ok(StreamEvent::MessageDelta {
stop_reason,
stop_sequence,
usage,
})
}
Some("message_stop") => Ok(StreamEvent::MessageStop),
Some("ping") => Ok(StreamEvent::Ping),
Some("error") => Err(stream_error(&value)),
_ => Ok(StreamEvent::Unknown(value)),
}
}
}
fn string_field(value: &Value, key: &str) -> String {
value
.get(key)
.and_then(Value::as_str)
.unwrap_or_default()
.to_owned()
}
fn index_of(value: &Value) -> usize {
value
.get("index")
.and_then(Value::as_u64)
.unwrap_or(0)
.try_into()
.unwrap_or(0)
}
fn stream_error(value: &Value) -> Error {
let error = value.get("error");
let kind = error
.and_then(|error| error.get("type"))
.and_then(Value::as_str)
.unwrap_or("error")
.to_owned();
let message = error
.and_then(|error| error.get("message"))
.and_then(Value::as_str)
.unwrap_or_default()
.to_owned();
Error::Api(ApiError {
status: 200,
kind,
message,
request_id: None,
retry_after: None,
})
}
#[cfg(test)]
mod system_tests {
use super::{CacheControl, Message, MessageRequest, System, SystemBlock};
use serde_json::json;
#[test]
fn string_system_serializes_byte_identically() {
let request = MessageRequest::new("claude-opus-4-8", 16).with_system("You are terse.");
let wire = serde_json::to_string(&request).expect("serializes");
assert_eq!(
wire,
r#"{"model":"claude-opus-4-8","max_tokens":16,"system":"You are terse.","messages":[]}"#
);
}
#[test]
fn string_system_is_a_bare_json_string_value() {
let request = MessageRequest::new("claude-opus-4-8", 16)
.with_system("hi")
.push_message(Message::user("Hello"));
let value = serde_json::to_value(&request).expect("to_value");
assert_eq!(value["system"], json!("hi"));
}
#[test]
fn owned_string_call_site_is_bare_string() {
let request = MessageRequest::new("claude-opus-4-8", 16).with_system(String::from("owned"));
let value = serde_json::to_value(&request).expect("to_value");
assert_eq!(value["system"], json!("owned"));
}
#[test]
fn blocks_system_matches_api_shape_and_round_trips() {
let system = System::blocks([SystemBlock::text("first"), SystemBlock::text("second")]);
let value = serde_json::to_value(&system).expect("to_value");
assert_eq!(
value,
json!([
{ "type": "text", "text": "first" },
{ "type": "text", "text": "second" },
])
);
let back: System = serde_json::from_value(value).expect("round-trips");
assert_eq!(back, system);
}
#[test]
fn leading_block_is_the_distinct_first_block() {
let system = System::leading(
SystemBlock::text("You are Claude Code."),
[SystemBlock::text("Follow the user's instructions.")],
);
let value = serde_json::to_value(&system).expect("to_value");
assert_eq!(value[0]["text"], json!("You are Claude Code."));
assert_eq!(value[1]["text"], json!("Follow the user's instructions."));
}
#[test]
fn cache_control_serializes_and_is_optional() {
let plain = serde_json::to_value(SystemBlock::text("x")).expect("to_value");
assert_eq!(plain, json!({ "type": "text", "text": "x" }));
let cached = SystemBlock::text("x").with_cache_control(CacheControl::ephemeral());
assert_eq!(
serde_json::to_value(&cached).expect("to_value"),
json!({ "type": "text", "text": "x", "cache_control": { "type": "ephemeral" } })
);
let ttl = SystemBlock::text("x").with_cache_control(CacheControl::ephemeral_ttl("1h"));
assert_eq!(
serde_json::to_value(&ttl).expect("to_value"),
json!({
"type": "text",
"text": "x",
"cache_control": { "type": "ephemeral", "ttl": "1h" }
})
);
}
#[test]
fn unknown_field_in_block_is_ignored() {
let block: SystemBlock =
serde_json::from_value(json!({ "type": "text", "text": "x", "future": 42 }))
.expect("tolerates unknown field");
assert_eq!(block.text, "x");
assert_eq!(block.cache_control, None);
}
}
#[cfg(test)]
mod tool_choice_tests {
use super::{Message, MessageRequest, Tool, ToolChoice};
use serde_json::json;
#[test]
fn no_tool_choice_serializes_byte_identically_to_before() {
let request = MessageRequest::new("claude-opus-4-8", 16)
.with_tools(vec![Tool::new(
"get_weather",
"Get the weather",
json!({ "type": "object" }),
)])
.push_message(Message::user("Hi"));
let wire = serde_json::to_string(&request).expect("serializes");
assert_eq!(
wire,
r#"{"model":"claude-opus-4-8","max_tokens":16,"messages":[{"role":"user","content":"Hi"}],"tools":[{"name":"get_weather","description":"Get the weather","input_schema":{"type":"object"}}]}"#
);
}
#[test]
fn auto_matches_pinned_wire_shape() {
assert_eq!(
serde_json::to_value(ToolChoice::auto()).expect("to_value"),
json!({ "type": "auto" })
);
}
#[test]
fn any_matches_pinned_wire_shape() {
assert_eq!(
serde_json::to_value(ToolChoice::any()).expect("to_value"),
json!({ "type": "any" })
);
}
#[test]
fn tool_matches_pinned_wire_shape() {
assert_eq!(
serde_json::to_value(ToolChoice::tool("get_weather")).expect("to_value"),
json!({ "type": "tool", "name": "get_weather" })
);
}
#[test]
fn disable_parallel_tool_use_is_optional_and_appears_when_set() {
assert_eq!(
serde_json::to_value(ToolChoice::any()).expect("to_value"),
json!({ "type": "any" }),
"unset stays absent from the wire"
);
assert_eq!(
serde_json::to_value(ToolChoice::any().with_disable_parallel_tool_use(true))
.expect("to_value"),
json!({ "type": "any", "disable_parallel_tool_use": true })
);
assert_eq!(
serde_json::to_value(
ToolChoice::tool("get_weather").with_disable_parallel_tool_use(false)
)
.expect("to_value"),
json!({ "type": "tool", "name": "get_weather", "disable_parallel_tool_use": false })
);
}
#[test]
fn request_with_tool_choice_matches_pinned_wire_shape() {
let request = MessageRequest::new("claude-opus-4-8", 16)
.with_tools(vec![Tool::new(
"get_weather",
"Get the weather",
json!({ "type": "object" }),
)])
.with_tool_choice(ToolChoice::tool("get_weather"))
.push_message(Message::user("What's the weather?"));
let wire = serde_json::to_string(&request).expect("serializes");
assert_eq!(
wire,
r#"{"model":"claude-opus-4-8","max_tokens":16,"messages":[{"role":"user","content":"What's the weather?"}],"tools":[{"name":"get_weather","description":"Get the weather","input_schema":{"type":"object"}}],"tool_choice":{"type":"tool","name":"get_weather"}}"#
);
}
#[test]
fn variants_round_trip() {
let variants = [
ToolChoice::auto(),
ToolChoice::auto().with_disable_parallel_tool_use(true),
ToolChoice::any(),
ToolChoice::any().with_disable_parallel_tool_use(true),
ToolChoice::tool("get_weather"),
ToolChoice::tool("get_weather").with_disable_parallel_tool_use(true),
];
for variant in variants {
let value = serde_json::to_value(&variant).expect("to_value");
let back: ToolChoice = serde_json::from_value(value).expect("round-trips");
assert_eq!(back, variant);
}
}
}