use serde::{Deserialize, Serialize};
use crate::types::content::{ContentBlock, ContentBlockParam};
use crate::types::string_enum;
string_enum! {
pub enum Role {
User = "user",
Assistant = "assistant",
}
}
string_enum! {
pub enum StopReason {
EndTurn = "end_turn",
MaxTokens = "max_tokens",
StopSequence = "stop_sequence",
ToolUse = "tool_use",
PauseTurn = "pause_turn",
Refusal = "refusal",
ModelContextWindowExceeded = "model_context_window_exceeded",
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct StopDetails {
#[serde(rename = "type")]
pub detail_type: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub category: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub explanation: Option<String>,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
pub struct Usage {
pub input_tokens: u64,
pub output_tokens: u64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cache_creation_input_tokens: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cache_read_input_tokens: Option<u64>,
#[serde(flatten)]
pub extra: serde_json::Map<String, serde_json::Value>,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct Container {
pub id: String,
#[serde(flatten)]
pub extra: serde_json::Map<String, serde_json::Value>,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct Message {
pub id: String,
#[serde(rename = "type")]
pub message_type: String,
pub role: Role,
pub model: String,
pub content: Vec<ContentBlock>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub stop_reason: Option<StopReason>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub stop_sequence: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub stop_details: Option<StopDetails>,
pub usage: Usage,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub container: Option<Container>,
}
impl Message {
pub fn text(&self) -> String {
self.content
.iter()
.filter_map(ContentBlock::as_text)
.collect()
}
pub fn into_param(self) -> MessageParam {
MessageParam {
role: self.role,
content: MessageContent::Blocks(
self.content
.into_iter()
.map(ContentBlockParam::from)
.collect(),
),
}
}
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
pub struct Metadata {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub user_id: Option<String>,
#[serde(flatten)]
pub extra: serde_json::Map<String, serde_json::Value>,
}
impl Metadata {
pub fn with_user_id(user_id: impl Into<String>) -> Self {
Self {
user_id: Some(user_id.into()),
extra: serde_json::Map::new(),
}
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum SystemPrompt {
Text(String),
Blocks(Vec<ContentBlockParam>),
}
impl From<String> for SystemPrompt {
fn from(s: String) -> Self {
SystemPrompt::Text(s)
}
}
impl From<&str> for SystemPrompt {
fn from(s: &str) -> Self {
SystemPrompt::Text(s.to_string())
}
}
impl From<Vec<ContentBlockParam>> for SystemPrompt {
fn from(blocks: Vec<ContentBlockParam>) -> Self {
SystemPrompt::Blocks(blocks)
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum MessageContent {
Text(String),
Blocks(Vec<ContentBlockParam>),
}
impl From<String> for MessageContent {
fn from(s: String) -> Self {
MessageContent::Text(s)
}
}
impl From<&str> for MessageContent {
fn from(s: &str) -> Self {
MessageContent::Text(s.to_string())
}
}
impl From<Vec<ContentBlockParam>> for MessageContent {
fn from(blocks: Vec<ContentBlockParam>) -> Self {
MessageContent::Blocks(blocks)
}
}
impl From<ContentBlockParam> for MessageContent {
fn from(block: ContentBlockParam) -> Self {
MessageContent::Blocks(vec![block])
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct MessageParam {
pub role: Role,
pub content: MessageContent,
}
impl MessageParam {
pub fn user(content: impl Into<MessageContent>) -> Self {
Self {
role: Role::User,
content: content.into(),
}
}
pub fn assistant(content: impl Into<MessageContent>) -> Self {
Self {
role: Role::Assistant,
content: content.into(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn rt<T>(json: serde_json::Value) -> serde_json::Value
where
T: serde::Serialize + serde::de::DeserializeOwned + PartialEq + std::fmt::Debug,
{
let parsed: T = serde_json::from_value(json).expect("deserialize");
let out = serde_json::to_value(&parsed).expect("serialize");
let reparsed: T = serde_json::from_value(out.clone()).expect("re-deserialize");
assert_eq!(parsed, reparsed, "struct round-trip mismatch");
out
}
#[test]
fn message_response_round_trips() {
let j = serde_json::json!({
"id": "msg_01ABC",
"type": "message",
"role": "assistant",
"model": "claude-opus-4-8",
"content": [{"type": "text", "text": "Hi there."}],
"stop_reason": "end_turn",
"stop_sequence": null,
"stop_details": null,
"usage": {
"input_tokens": 10,
"output_tokens": 25,
"cache_creation_input_tokens": 0,
"cache_read_input_tokens": 0
},
"container": null
});
rt::<Message>(j);
}
#[test]
fn refusal_message_with_stop_details() {
let j = serde_json::json!({
"id": "msg_02",
"type": "message",
"role": "assistant",
"model": "claude-opus-4-8",
"content": [],
"stop_reason": "refusal",
"stop_sequence": null,
"stop_details": {"type": "refusal", "category": "cyber", "explanation": "no"},
"usage": {"input_tokens": 3, "output_tokens": 0}
});
let msg: Message = serde_json::from_value(j).unwrap();
assert_eq!(msg.stop_reason, Some(StopReason::Refusal));
assert_eq!(
msg.stop_details.as_ref().unwrap().category.as_deref(),
Some("cyber")
);
}
#[test]
fn streaming_message_start_shape() {
let j = serde_json::json!({
"id": "msg_stream",
"type": "message",
"role": "assistant",
"model": "claude-opus-4-8",
"content": [],
"stop_reason": null,
"stop_sequence": null,
"usage": {"input_tokens": 25, "output_tokens": 1}
});
let msg: Message = serde_json::from_value(j).unwrap();
assert_eq!(msg.stop_reason, None);
assert_eq!(msg.usage.cache_read_input_tokens, None);
}
#[test]
fn usage_extra_fields_are_captured() {
let j = serde_json::json!({
"input_tokens": 10,
"output_tokens": 25,
"cache_creation_input_tokens": 0,
"cache_read_input_tokens": 0,
"service_tier": "standard",
"iterations": 2
});
assert_eq!(rt::<Usage>(j.clone()), j);
let usage: Usage = serde_json::from_value(j).unwrap();
assert_eq!(usage.extra["service_tier"], serde_json::json!("standard"));
assert_eq!(usage.extra["iterations"], serde_json::json!(2));
}
#[test]
fn unknown_role_does_not_fail_message_deserialize() {
let j = serde_json::json!({
"id": "msg_role",
"type": "message",
"role": "assistant_v2",
"model": "claude-opus-4-8",
"content": [{"type": "text", "text": "hi"}],
"usage": {"input_tokens": 1, "output_tokens": 1}
});
let msg: Message = serde_json::from_value(j).expect("unknown role should still parse");
assert_eq!(msg.role, Role::Unknown("assistant_v2".to_string()));
assert_eq!(
serde_json::to_value(&msg.role).unwrap(),
serde_json::json!("assistant_v2")
);
}
#[test]
fn unknown_stop_reason_is_preserved() {
let parsed: StopReason =
serde_json::from_value(serde_json::json!("some_future_reason")).unwrap();
assert_eq!(
parsed,
StopReason::Unknown("some_future_reason".to_string())
);
assert_eq!(
serde_json::to_value(&parsed).unwrap(),
serde_json::json!("some_future_reason")
);
}
#[test]
fn system_prompt_forms() {
let s = serde_json::json!("You are terse.");
assert_eq!(rt::<SystemPrompt>(s.clone()), s);
let blocks = serde_json::json!([
{"type": "text", "text": "You are terse.", "cache_control": {"type": "ephemeral"}}
]);
assert_eq!(rt::<SystemPrompt>(blocks.clone()), blocks);
}
#[test]
fn message_param_forms() {
let string_form = serde_json::json!({"role": "user", "content": "hi"});
assert_eq!(rt::<MessageParam>(string_form.clone()), string_form);
let block_form = serde_json::json!({
"role": "assistant",
"content": [{"type": "text", "text": "..."}]
});
assert_eq!(rt::<MessageParam>(block_form.clone()), block_form);
}
#[derive(Debug, PartialEq, serde::Serialize, serde::Deserialize)]
struct ErrorEnvelope {
#[serde(rename = "type")]
envelope_type: String,
error: ApiErrorBody,
request_id: Option<String>,
}
#[derive(Debug, PartialEq, serde::Serialize, serde::Deserialize)]
struct ApiErrorBody {
#[serde(rename = "type")]
error_type: String,
message: String,
}
#[test]
fn error_envelope_round_trips() {
let j = serde_json::json!({
"type": "error",
"error": {"type": "invalid_request_error", "message": "bad thing"},
"request_id": "req_123"
});
let parsed: ErrorEnvelope = serde_json::from_value(j.clone()).unwrap();
assert_eq!(parsed.error.error_type, "invalid_request_error");
assert_eq!(serde_json::to_value(&parsed).unwrap(), j);
}
}