Skip to main content

claude_api/messages/
response.rs

1//! Response types: [`Message`], [`CountTokensResponse`], [`ContainerInfo`].
2
3use serde::{Deserialize, Serialize};
4
5use crate::messages::content::ContentBlock;
6use crate::types::{ModelId, Role, StopReason, Usage};
7
8/// A complete (non-streaming) Messages-API response.
9///
10/// Usually produced by the SDK from a wire payload rather than built by
11/// hand. Tests that need a fixture should round-trip a JSON literal through
12/// [`serde_json::from_value`].
13#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
14#[non_exhaustive]
15pub struct Message {
16    /// Unique message identifier (e.g. `msg_01ABC...`).
17    pub id: String,
18    /// Wire `type` discriminant. Always `"message"` for non-streaming responses;
19    /// retained on the struct for full round-trip fidelity.
20    #[serde(rename = "type", default = "default_message_kind")]
21    pub kind: String,
22    /// Author of the message. Always [`Role::Assistant`] for responses.
23    #[serde(default = "default_assistant_role")]
24    pub role: Role,
25    /// Ordered list of content blocks the model produced.
26    #[serde(default)]
27    pub content: Vec<ContentBlock>,
28    /// The model that produced this response.
29    pub model: ModelId,
30    /// Why the model stopped, when known.
31    #[serde(default, skip_serializing_if = "Option::is_none")]
32    pub stop_reason: Option<StopReason>,
33    /// The stop sequence that triggered termination, if applicable.
34    #[serde(default, skip_serializing_if = "Option::is_none")]
35    pub stop_sequence: Option<String>,
36    /// Structured information about *why* the model stopped (e.g. for
37    /// `refusal` stops, the policy category and an explanation). `None`
38    /// when no extra detail is reported.
39    ///
40    /// TODO: type as a closed enum once more `stop_details` shapes are
41    /// public. Currently preserved as raw JSON for forward-compat.
42    #[serde(default, skip_serializing_if = "Option::is_none")]
43    pub stop_details: Option<serde_json::Value>,
44    /// Token usage and related counters.
45    #[serde(default)]
46    pub usage: Usage,
47    /// Information about context-management edits applied to the
48    /// request (e.g. trimmed history). Present only when
49    /// `context-management-2025-06-27` is in play.
50    ///
51    /// TODO: type as `BetaResponseContextManagement` once the shape
52    /// stabilizes. Currently preserved as raw JSON.
53    #[serde(default, skip_serializing_if = "Option::is_none")]
54    pub context_management: Option<serde_json::Value>,
55    /// Container metadata, present when the request used the code-execution
56    /// container tool.
57    #[serde(default, skip_serializing_if = "Option::is_none")]
58    pub container: Option<ContainerInfo>,
59}
60
61fn default_message_kind() -> String {
62    "message".to_owned()
63}
64
65fn default_assistant_role() -> Role {
66    Role::Assistant
67}
68
69/// Container metadata returned when a request used the code-execution
70/// container tool.
71#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
72#[non_exhaustive]
73pub struct ContainerInfo {
74    /// Container identifier.
75    pub id: String,
76    /// Container expiration timestamp (ISO-8601), if reported.
77    #[serde(default, skip_serializing_if = "Option::is_none")]
78    pub expires_at: Option<String>,
79}
80
81/// Response payload for `POST /v1/messages/count_tokens`.
82#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
83#[non_exhaustive]
84pub struct CountTokensResponse {
85    /// Number of input tokens the request would consume.
86    pub input_tokens: u32,
87}
88
89#[cfg(test)]
90mod tests {
91    use super::*;
92    use crate::messages::content::KnownBlock;
93    use pretty_assertions::assert_eq;
94    use serde_json::json;
95
96    #[test]
97    fn realistic_message_response_round_trips() {
98        let raw = json!({
99            "id": "msg_01ABCDEF",
100            "type": "message",
101            "role": "assistant",
102            "content": [
103                {"type": "text", "text": "Hello!"}
104            ],
105            "model": "claude-sonnet-4-6",
106            "stop_reason": "end_turn",
107            "stop_sequence": null,
108            "usage": {
109                "input_tokens": 10,
110                "output_tokens": 5
111            }
112        });
113
114        let msg: Message = serde_json::from_value(raw).expect("deserialize");
115        assert_eq!(msg.id, "msg_01ABCDEF");
116        assert_eq!(msg.kind, "message");
117        assert_eq!(msg.role, Role::Assistant);
118        assert_eq!(msg.model, ModelId::SONNET_4_6);
119        assert_eq!(msg.stop_reason, Some(StopReason::EndTurn));
120        assert_eq!(msg.usage.input_tokens, 10);
121        assert_eq!(msg.usage.output_tokens, 5);
122        assert_eq!(msg.content.len(), 1);
123        assert_eq!(msg.content[0].type_tag(), Some("text"));
124
125        let reserialized = serde_json::to_value(&msg).expect("serialize");
126        let parsed_again: Message = serde_json::from_value(reserialized).expect("re-deserialize");
127        assert_eq!(parsed_again, msg, "round-trip mismatch");
128    }
129
130    #[test]
131    fn message_with_unknown_content_block_round_trips() {
132        let raw = json!({
133            "id": "msg_X",
134            "type": "message",
135            "role": "assistant",
136            "content": [
137                {"type": "text", "text": "hi"},
138                {"type": "future_block", "payload": 42}
139            ],
140            "model": "claude-opus-4-7",
141            "usage": {"input_tokens": 1, "output_tokens": 1}
142        });
143
144        let msg: Message = serde_json::from_value(raw.clone()).expect("deserialize");
145        assert_eq!(msg.content.len(), 2);
146        assert_eq!(msg.content[0].type_tag(), Some("text"));
147        assert_eq!(msg.content[1].type_tag(), Some("future_block"));
148        assert!(msg.content[1].other().is_some());
149
150        // Reserializing must put the unknown block back byte-for-byte.
151        let reserialized = serde_json::to_value(&msg).expect("serialize");
152        let blocks = reserialized.get("content").unwrap().as_array().unwrap();
153        assert_eq!(blocks[1], json!({"type": "future_block", "payload": 42}));
154    }
155
156    #[test]
157    fn message_kind_defaults_when_missing() {
158        // A wire payload missing the `type` field still parses, with kind defaulting to "message".
159        let raw = json!({
160            "id": "msg_1",
161            "role": "assistant",
162            "content": [],
163            "model": "claude-sonnet-4-6",
164            "usage": {"input_tokens": 0, "output_tokens": 0}
165        });
166        let msg: Message = serde_json::from_value(raw).expect("deserialize");
167        assert_eq!(msg.kind, "message");
168    }
169
170    #[test]
171    fn message_with_tool_use_block_round_trips() {
172        let msg = Message {
173            id: "msg_tool".into(),
174            kind: "message".into(),
175            role: Role::Assistant,
176            content: vec![ContentBlock::Known(KnownBlock::ToolUse {
177                id: "toolu_1".into(),
178                name: "lookup".into(),
179                input: json!({"q": "rust"}),
180            })],
181            model: ModelId::HAIKU_4_5,
182            stop_reason: Some(StopReason::ToolUse),
183            stop_sequence: None,
184            stop_details: None,
185            usage: Usage {
186                input_tokens: 7,
187                output_tokens: 3,
188                ..Usage::default()
189            },
190            context_management: None,
191            container: None,
192        };
193
194        let v = serde_json::to_value(&msg).expect("serialize");
195        let parsed: Message = serde_json::from_value(v).expect("deserialize");
196        assert_eq!(parsed, msg);
197    }
198
199    #[test]
200    fn count_tokens_response_round_trips() {
201        let r = CountTokensResponse { input_tokens: 42 };
202        let v = serde_json::to_value(&r).expect("serialize");
203        assert_eq!(v, json!({"input_tokens": 42}));
204        let parsed: CountTokensResponse = serde_json::from_value(v).expect("deserialize");
205        assert_eq!(parsed, r);
206    }
207
208    #[test]
209    fn container_info_round_trips() {
210        let c = ContainerInfo {
211            id: "cnt_01".into(),
212            expires_at: Some("2026-01-01T00:00:00Z".into()),
213        };
214        let v = serde_json::to_value(&c).expect("serialize");
215        assert_eq!(
216            v,
217            json!({"id": "cnt_01", "expires_at": "2026-01-01T00:00:00Z"})
218        );
219        let parsed: ContainerInfo = serde_json::from_value(v).expect("deserialize");
220        assert_eq!(parsed, c);
221    }
222
223    #[test]
224    fn message_with_container_round_trips() {
225        let raw = json!({
226            "id": "msg_with_container",
227            "type": "message",
228            "role": "assistant",
229            "content": [],
230            "model": "claude-opus-4-7",
231            "usage": {"input_tokens": 0, "output_tokens": 0},
232            "container": {"id": "cnt_42"}
233        });
234        let msg: Message = serde_json::from_value(raw).expect("deserialize");
235        assert_eq!(msg.container.as_ref().unwrap().id, "cnt_42");
236    }
237}