use crate::common::Usage;
use crate::messages::request::content::ContentBlock;
use crate::messages::request::model::Model;
use crate::messages::request::role::Role;
use serde::{Deserialize, Serialize};
use strum::{Display, EnumString};
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Response {
pub id: String,
#[serde(rename = "type")]
pub type_name: String,
pub role: Role,
pub content: Vec<ContentBlock>,
pub model: Model,
#[serde(skip_serializing_if = "Option::is_none")]
pub stop_reason: Option<StopReason>,
#[serde(skip_serializing_if = "Option::is_none")]
pub stop_sequence: Option<String>,
pub usage: Usage,
}
#[derive(Serialize, Deserialize, Debug, Clone, Display, EnumString, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum StopReason {
EndTurn,
MaxTokens,
StopSequence,
ToolUse,
Refusal,
}
impl Response {
pub fn text(&self) -> Option<String> {
self.content
.iter()
.filter_map(|block| match block {
ContentBlock::Text { text, .. } => Some(text.clone()),
_ => None,
})
.collect::<Vec<_>>()
.join("")
.into()
}
pub fn get_text(&self) -> String {
self.content
.iter()
.filter_map(|block| match block {
ContentBlock::Text { text, .. } => Some(text.clone()),
_ => None,
})
.collect::<Vec<_>>()
.join("")
}
pub fn has_tool_use(&self) -> bool {
self.content
.iter()
.any(|block| matches!(block, ContentBlock::ToolUse { .. }))
}
pub fn get_tool_uses(&self) -> Vec<&ContentBlock> {
self.content
.iter()
.filter(|block| matches!(block, ContentBlock::ToolUse { .. }))
.collect()
}
pub fn get_tool_use_by_id(&self, id: &str) -> Option<&ContentBlock> {
self.content.iter().find(|block| match block {
ContentBlock::ToolUse { id: tool_id, .. } => tool_id == id,
_ => false,
})
}
pub fn has_thinking(&self) -> bool {
self.content
.iter()
.any(|block| matches!(block, ContentBlock::Thinking { .. }))
}
pub fn get_thinking(&self) -> Option<String> {
self.content
.iter()
.filter_map(|block| match block {
ContentBlock::Thinking { thinking, .. } => Some(thinking.clone()),
_ => None,
})
.collect::<Vec<_>>()
.join("")
.into()
}
pub fn stopped_for_tool_use(&self) -> bool {
self.stop_reason == Some(StopReason::ToolUse)
}
pub fn stopped_naturally(&self) -> bool {
self.stop_reason == Some(StopReason::EndTurn)
}
pub fn hit_max_tokens(&self) -> bool {
self.stop_reason == Some(StopReason::MaxTokens)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn sample_response() -> Response {
Response {
id: "msg_123".to_string(),
type_name: "message".to_string(),
role: Role::Assistant,
content: vec![ContentBlock::Text {
text: "Hello, world!".to_string(),
cache_control: None,
}],
model: Model::Sonnet4,
stop_reason: Some(StopReason::EndTurn),
stop_sequence: None,
usage: Usage::new(10, 5),
}
}
#[test]
fn test_response_text() {
let response = sample_response();
assert_eq!(response.get_text(), "Hello, world!");
}
#[test]
fn test_response_stop_reason() {
let response = sample_response();
assert!(response.stopped_naturally());
assert!(!response.stopped_for_tool_use());
assert!(!response.hit_max_tokens());
}
#[test]
fn test_response_with_tool_use() {
let response = Response {
id: "msg_123".to_string(),
type_name: "message".to_string(),
role: Role::Assistant,
content: vec![
ContentBlock::Text {
text: "Let me search for that.".to_string(),
cache_control: None,
},
ContentBlock::ToolUse {
id: "tool_123".to_string(),
name: "search".to_string(),
input: serde_json::json!({"query": "test"}),
},
],
model: Model::Sonnet4,
stop_reason: Some(StopReason::ToolUse),
stop_sequence: None,
usage: Usage::new(20, 15),
};
assert!(response.has_tool_use());
assert!(response.stopped_for_tool_use());
assert_eq!(response.get_tool_uses().len(), 1);
}
#[test]
fn test_deserialize_response() {
let json = r#"{
"id": "msg_01XYZ",
"type": "message",
"role": "assistant",
"content": [
{
"type": "text",
"text": "Hello!"
}
],
"model": "claude-sonnet-4-20250514",
"stop_reason": "end_turn",
"usage": {
"input_tokens": 10,
"output_tokens": 5
}
}"#;
let response: Response = serde_json::from_str(json).unwrap();
assert_eq!(response.id, "msg_01XYZ");
assert_eq!(response.get_text(), "Hello!");
assert_eq!(response.model, Model::Sonnet4);
assert_eq!(response.stop_reason, Some(StopReason::EndTurn));
}
#[test]
fn test_deserialize_response_unknown_model() {
let json = r#"{
"id": "msg_01XYZ",
"type": "message",
"role": "assistant",
"content": [],
"model": "claude-future-model-2026",
"stop_reason": "end_turn",
"usage": {
"input_tokens": 10,
"output_tokens": 5
}
}"#;
let response: Response = serde_json::from_str(json).unwrap();
assert_eq!(
response.model,
Model::Other("claude-future-model-2026".to_string())
);
}
#[test]
fn test_serialize_stop_reason() {
let reason = StopReason::ToolUse;
let json = serde_json::to_string(&reason).unwrap();
assert_eq!(json, "\"tool_use\"");
let reason = StopReason::EndTurn;
let json = serde_json::to_string(&reason).unwrap();
assert_eq!(json, "\"end_turn\"");
}
}