1use serde::{Deserialize, Serialize};
2use std::fmt;
3
4use super::cache::CacheControl;
5use super::tool::ToolCall;
6
7#[derive(Debug, Clone, Serialize, Deserialize)]
16#[serde(tag = "role")]
17pub enum Message {
18 #[serde(rename = "system")]
19 System {
20 content: MessageContent,
21 #[serde(skip_serializing_if = "Option::is_none")]
22 cache_control: Option<CacheControl>,
23 },
24
25 #[serde(rename = "developer")]
26 Developer {
27 content: MessageContent,
28 #[serde(skip_serializing_if = "Option::is_none")]
29 cache_control: Option<CacheControl>,
30 },
31
32 #[serde(rename = "user")]
33 User { content: MessageContent },
34
35 #[serde(rename = "assistant")]
36 Assistant {
37 content: MessageContent,
39 #[serde(skip_serializing_if = "Option::is_none")]
41 tool_calls: Option<Vec<ToolCall>>,
42 #[serde(skip_serializing_if = "Option::is_none")]
44 cache_control: Option<CacheControl>,
45 #[serde(rename = "reasoning_content", skip_serializing_if = "Option::is_none")]
47 reasoning_content: Option<String>,
48 },
49
50 #[serde(rename = "tool")]
51 Tool {
52 content: MessageContent,
53 tool_call_id: String,
54 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
55 is_error: bool,
56 },
57}
58
59impl Message {
60 pub fn system(text: &str) -> Self {
61 Message::System { content: MessageContent::Text(text.into()), cache_control: None }
62 }
63
64 pub fn developer(text: &str) -> Self {
65 Message::Developer { content: MessageContent::Text(text.into()), cache_control: None }
66 }
67
68 pub fn user(text: &str) -> Self {
69 Message::User { content: MessageContent::Text(text.into()) }
70 }
71
72 pub fn assistant(text: &str) -> Self {
73 Message::Assistant {
74 content: MessageContent::Text(text.into()),
75 tool_calls: None,
76 cache_control: None,
77 reasoning_content: None,
78 }
79 }
80
81 pub fn tool_result(tool_call_id: &str, content: &str) -> Self {
82 Message::Tool {
83 content: MessageContent::Text(content.into()),
84 tool_call_id: tool_call_id.into(),
85 is_error: false,
86 }
87 }
88
89 pub fn tool_error(tool_call_id: &str, error: &str) -> Self {
90 Message::Tool {
91 content: MessageContent::Text(error.into()),
92 tool_call_id: tool_call_id.into(),
93 is_error: true,
94 }
95 }
96
97 pub fn role_str(&self) -> &'static str {
99 match self {
100 Message::System { .. } => "system",
101 Message::Developer { .. } => "developer",
102 Message::User { .. } => "user",
103 Message::Assistant { .. } => "assistant",
104 Message::Tool { .. } => "tool",
105 }
106 }
107}
108
109impl fmt::Display for Message {
110 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
111 match self {
112 Message::System { content, .. }
113 | Message::Developer { content, .. }
114 | Message::User { content, .. }
115 | Message::Assistant { content, .. }
116 | Message::Tool { content, .. } => match content {
117 MessageContent::Text(t) => write!(f, "{}", t),
118 MessageContent::MultiPart(parts) => {
119 for part in parts {
120 match part {
121 ContentPart::Text { text, .. } => write!(f, "{}", text)?,
122 ContentPart::ImageUrl { .. } => write!(f, "[Image]")?,
123 ContentPart::ImageBase64 { .. } => write!(f, "[Image(base64)]")?,
124 ContentPart::AudioBase64 { .. } => write!(f, "[Audio]")?,
125 ContentPart::File { filename, .. } => {
126 if let Some(name) = filename {
127 write!(f, "[File: {}]", name)?
128 } else {
129 write!(f, "[File]")?
130 }
131 }
132 ContentPart::Document { title, .. } => {
133 if let Some(t) = title {
134 write!(f, "[Document: {}]", t)?
135 } else {
136 write!(f, "[Document]")?
137 }
138 }
139 ContentPart::RedactedThinking { .. } => write!(f, "[Thinking]")?,
140 ContentPart::ToolReference { tool_name, .. } => {
141 write!(f, "[Tool: {}]", tool_name)?
142 }
143 }
144 }
145 Ok(())
146 }
147 MessageContent::None => Ok(()),
148 },
149 }
150 }
151}
152
153#[derive(Debug, Clone, Serialize, Deserialize)]
157#[serde(untagged)]
158pub enum MessageContent {
159 Text(String),
161 MultiPart(Vec<ContentPart>),
163 None,
165}
166
167impl From<String> for MessageContent {
168 fn from(s: String) -> Self {
169 MessageContent::Text(s)
170 }
171}
172
173impl From<&str> for MessageContent {
174 fn from(s: &str) -> Self {
175 MessageContent::Text(s.to_owned())
176 }
177}
178
179impl From<Vec<ContentPart>> for MessageContent {
180 fn from(parts: Vec<ContentPart>) -> Self {
181 MessageContent::MultiPart(parts)
182 }
183}
184
185#[derive(Debug, Clone, Serialize, Deserialize)]
189#[serde(tag = "type")]
190pub enum DocumentSource {
191 #[serde(rename = "base64")]
192 Base64 { media_type: String, data: String },
193 #[serde(rename = "url")]
194 Url { url: String },
195}
196
197#[derive(Debug, Clone, Serialize, Deserialize)]
201#[serde(tag = "type")]
202pub enum ContentPart {
203 #[serde(rename = "text")]
204 Text { text: String },
205
206 #[serde(rename = "image_url")]
207 ImageUrl {
208 url: String,
209 #[serde(skip_serializing_if = "Option::is_none")]
210 detail: Option<ImageDetail>,
211 },
212
213 #[serde(rename = "image_base64")]
214 ImageBase64 { media_type: String, data: String },
215
216 #[serde(rename = "audio_base64")]
218 AudioBase64 { media_type: String, data: String },
219
220 #[serde(rename = "file")]
223 File {
224 file_id: String,
225 #[serde(skip_serializing_if = "Option::is_none")]
226 filename: Option<String>,
227 },
228
229 #[serde(rename = "document")]
231 Document {
232 source: DocumentSource,
233 #[serde(skip_serializing_if = "Option::is_none")]
234 title: Option<String>,
235 #[serde(skip_serializing_if = "Option::is_none")]
236 cache_control: Option<CacheControl>,
237 },
238
239 #[serde(rename = "redacted_thinking")]
241 RedactedThinking { data: String },
242
243 #[serde(rename = "tool_reference")]
245 ToolReference {
246 tool_name: String,
247 #[serde(skip_serializing_if = "Option::is_none")]
248 cache_control: Option<CacheControl>,
249 },
250}
251
252#[derive(Debug, Clone, Serialize, Deserialize)]
256pub enum ImageDetail {
257 #[serde(rename = "auto")]
258 Auto,
259 #[serde(rename = "low")]
260 Low,
261 #[serde(rename = "high")]
262 High,
263}
264
265#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
269#[serde(rename_all = "snake_case")]
270pub enum Role {
271 System,
272 User,
273 Assistant,
274 Tool,
275}
276
277#[cfg(test)]
278mod tests {
279 use super::*;
280
281 #[test]
282 fn test_system_message() {
283 let msg = Message::system("You are a helpful assistant.");
284 assert_eq!(msg.role_str(), "system");
285 match msg {
286 Message::System { content, cache_control } => {
287 assert!(matches!(content, MessageContent::Text(_)));
288 assert!(cache_control.is_none());
289 }
290 _ => panic!("Expected System variant"),
291 }
292 }
293
294 #[test]
295 fn test_user_message() {
296 let msg = Message::user("Hello!");
297 assert_eq!(msg.role_str(), "user");
298 match msg {
299 Message::User { content } => {
300 assert!(matches!(content, MessageContent::Text(_)));
301 }
302 _ => panic!("Expected User variant"),
303 }
304 }
305
306 #[test]
307 fn test_assistant_message() {
308 let msg = Message::assistant("Hi!");
309 assert_eq!(msg.role_str(), "assistant");
310 match msg {
311 Message::Assistant { content, tool_calls, cache_control, .. } => {
312 assert!(matches!(content, MessageContent::Text(_)));
313 assert!(tool_calls.is_none());
314 assert!(cache_control.is_none());
315 }
316 _ => panic!("Expected Assistant variant"),
317 }
318 }
319
320 #[test]
321 fn test_tool_result() {
322 let msg = Message::tool_result("call_1", "result data");
323 assert_eq!(msg.role_str(), "tool");
324 match msg {
325 Message::Tool { content, tool_call_id, is_error } => {
326 assert!(matches!(content, MessageContent::Text(_)));
327 assert_eq!(tool_call_id, "call_1");
328 assert!(!is_error);
329 }
330 _ => panic!("Expected Tool variant"),
331 }
332 }
333
334 #[test]
335 fn test_tool_error() {
336 let msg = Message::tool_error("call_1", "error!");
337 match msg {
338 Message::Tool { ref tool_call_id, is_error, .. } => {
339 assert!(is_error);
340 assert_eq!(tool_call_id, "call_1");
341 }
342 _ => panic!("Expected Tool variant"),
343 }
344 }
345
346 #[test]
347 fn test_role_str_all_variants() {
348 assert_eq!(Message::system("a").role_str(), "system");
349 assert_eq!(Message::user("a").role_str(), "user");
350 assert_eq!(Message::assistant("a").role_str(), "assistant");
351 assert_eq!(Message::tool_result("1", "a").role_str(), "tool");
352 }
353
354 #[test]
355 fn test_message_content_from_string() {
356 let content: MessageContent = "hello".to_string().into();
357 assert!(matches!(content, MessageContent::Text(_)));
358 }
359
360 #[test]
361 fn test_message_content_from_str() {
362 let content: MessageContent = "hello".into();
363 assert!(matches!(content, MessageContent::Text(_)));
364 }
365
366 #[test]
367 fn test_message_content_from_vec_parts() {
368 let parts = vec![ContentPart::Text { text: "hello".into() }];
369 let content: MessageContent = parts.into();
370 assert!(matches!(content, MessageContent::MultiPart(_)));
371 }
372
373 #[test]
374 fn test_message_display_text() {
375 let msg = Message::user("Hello!");
376 assert_eq!(format!("{}", msg), "Hello!");
377 }
378
379 #[test]
380 fn test_message_display_multipart() {
381 let parts = vec![
382 ContentPart::Text { text: "Check ".into() },
383 ContentPart::ImageUrl { url: "https://example.com/img.png".into(), detail: None },
384 ];
385 let content = MessageContent::MultiPart(parts);
386 let msg = Message::User { content };
387 assert_eq!(format!("{}", msg), "Check [Image]");
388 }
389
390 #[test]
391 fn test_message_display_none() {
392 let msg = Message::Assistant {
393 content: MessageContent::None,
394 tool_calls: None,
395 cache_control: None,
396 reasoning_content: None,
397 };
398 assert_eq!(format!("{}", msg), "");
399 }
400
401 #[test]
402 fn test_content_part_image_base64_display() {
403 let part =
404 ContentPart::ImageBase64 { media_type: "image/png".into(), data: "base64data".into() };
405 let msg = Message::User { content: MessageContent::MultiPart(vec![part]) };
406 assert_eq!(format!("{}", msg), "[Image(base64)]");
407 }
408
409 #[test]
410 fn test_message_serde_roundtrip() {
411 let msg = Message::assistant("Hello");
412 let json = serde_json::to_string(&msg).unwrap();
413 let deserialized: Message = serde_json::from_str(&json).unwrap();
414 assert_eq!(msg.role_str(), deserialized.role_str());
415 }
416
417 #[test]
418 fn test_image_detail_serde() {
419 let json = r#""auto""#;
420 let detail: ImageDetail = serde_json::from_str(json).unwrap();
421 assert!(matches!(detail, ImageDetail::Auto));
422 }
423
424 #[test]
425 fn test_content_part_serde() {
426 let part = ContentPart::Text { text: "hello".into() };
427 let json = serde_json::to_string(&part).unwrap();
428 let deserialized: ContentPart = serde_json::from_str(&json).unwrap();
429 match deserialized {
430 ContentPart::Text { text } => assert_eq!(text, "hello"),
431 _ => panic!("Expected Text variant"),
432 }
433 }
434
435 #[test]
438 fn test_developer_message() {
439 let msg = Message::developer("You are a code expert.");
440 assert_eq!(msg.role_str(), "developer");
441 match msg {
442 Message::Developer { content, cache_control } => {
443 assert!(matches!(content, MessageContent::Text(_)));
444 assert!(cache_control.is_none());
445 }
446 _ => panic!("Expected Developer variant"),
447 }
448 }
449
450 #[test]
451 fn test_developer_serde_roundtrip() {
452 let msg = Message::developer("You are a Python expert.");
453 let json = serde_json::to_string(&msg).unwrap();
454 assert!(json.contains(r#""role":"developer""#));
455 let deserialized: Message = serde_json::from_str(&json).unwrap();
456 assert!(matches!(deserialized, Message::Developer { .. }));
457 }
458
459 #[test]
462 fn test_document_source_base64_serde() {
463 let src = DocumentSource::Base64 {
464 media_type: "application/pdf".into(),
465 data: "base64data".into(),
466 };
467 let json = serde_json::to_string(&src).unwrap();
468 assert!(json.contains(r#""type":"base64""#));
469 let deserialized: DocumentSource = serde_json::from_str(&json).unwrap();
470 match deserialized {
471 DocumentSource::Base64 { media_type, data } => {
472 assert_eq!(media_type, "application/pdf");
473 assert_eq!(data, "base64data");
474 }
475 _ => panic!("Expected Base64 variant"),
476 }
477 }
478
479 #[test]
480 fn test_document_source_url_serde() {
481 let src = DocumentSource::Url { url: "https://example.com/doc.pdf".into() };
482 let json = serde_json::to_string(&src).unwrap();
483 assert!(json.contains(r#""type":"url""#));
484 let deserialized: DocumentSource = serde_json::from_str(&json).unwrap();
485 match deserialized {
486 DocumentSource::Url { url } => assert_eq!(url, "https://example.com/doc.pdf"),
487 _ => panic!("Expected Url variant"),
488 }
489 }
490
491 #[test]
494 fn test_content_part_document_serde() {
495 let part = ContentPart::Document {
496 source: DocumentSource::Base64 {
497 media_type: "text/plain".into(),
498 data: "hello".into(),
499 },
500 title: Some("readme".into()),
501 cache_control: None,
502 };
503 let json = serde_json::to_string(&part).unwrap();
504 assert!(json.contains(r#""type":"document""#));
505 let deserialized: ContentPart = serde_json::from_str(&json).unwrap();
506 match deserialized {
507 ContentPart::Document { source, title, .. } => {
508 assert!(matches!(source, DocumentSource::Base64 { .. }));
509 assert_eq!(title, Some("readme".into()));
510 }
511 _ => panic!("Expected Document variant"),
512 }
513 }
514
515 #[test]
516 fn test_content_part_redacted_thinking_serde() {
517 let part = ContentPart::RedactedThinking { data: "redacted_token_count".into() };
518 let json = serde_json::to_string(&part).unwrap();
519 assert!(json.contains(r#""type":"redacted_thinking""#));
520 let deserialized: ContentPart = serde_json::from_str(&json).unwrap();
521 match deserialized {
522 ContentPart::RedactedThinking { data } => assert_eq!(data, "redacted_token_count"),
523 _ => panic!("Expected RedactedThinking variant"),
524 }
525 }
526
527 #[test]
528 fn test_content_part_tool_reference_serde() {
529 let part =
530 ContentPart::ToolReference { tool_name: "get_weather".into(), cache_control: None };
531 let json = serde_json::to_string(&part).unwrap();
532 assert!(json.contains(r#""type":"tool_reference""#));
533 let deserialized: ContentPart = serde_json::from_str(&json).unwrap();
534 match deserialized {
535 ContentPart::ToolReference { tool_name, .. } => assert_eq!(tool_name, "get_weather"),
536 _ => panic!("Expected ToolReference variant"),
537 }
538 }
539}