1use serde::{Deserialize, Serialize};
4
5use crate::messages::content::ContentBlock;
6use crate::types::{ModelId, Role, StopReason, Usage};
7
8#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
14#[non_exhaustive]
15pub struct Message {
16 pub id: String,
18 #[serde(rename = "type", default = "default_message_kind")]
21 pub kind: String,
22 #[serde(default = "default_assistant_role")]
24 pub role: Role,
25 #[serde(default)]
27 pub content: Vec<ContentBlock>,
28 pub model: ModelId,
30 #[serde(default, skip_serializing_if = "Option::is_none")]
32 pub stop_reason: Option<StopReason>,
33 #[serde(default, skip_serializing_if = "Option::is_none")]
35 pub stop_sequence: Option<String>,
36 #[serde(default, skip_serializing_if = "Option::is_none")]
43 pub stop_details: Option<serde_json::Value>,
44 #[serde(default)]
46 pub usage: Usage,
47 #[serde(default, skip_serializing_if = "Option::is_none")]
54 pub context_management: Option<serde_json::Value>,
55 #[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#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
72#[non_exhaustive]
73pub struct ContainerInfo {
74 pub id: String,
76 #[serde(default, skip_serializing_if = "Option::is_none")]
78 pub expires_at: Option<String>,
79}
80
81#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
83#[non_exhaustive]
84pub struct CountTokensResponse {
85 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 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 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}