1use serde::{Deserialize, Serialize};
8use uuid::Uuid;
9
10#[derive(Debug, Clone, Serialize, Deserialize)]
15#[serde(tag = "type")]
16pub enum Message {
17 #[serde(rename = "user")]
19 User(UserMessage),
20 #[serde(rename = "assistant")]
22 Assistant(AssistantMessage),
23 #[serde(rename = "system")]
25 System(SystemMessage),
26}
27
28impl Message {
29 pub fn uuid(&self) -> &Uuid {
30 match self {
31 Message::User(m) => &m.uuid,
32 Message::Assistant(m) => &m.uuid,
33 Message::System(m) => &m.uuid,
34 }
35 }
36}
37
38#[derive(Debug, Clone, Serialize, Deserialize)]
40pub struct UserMessage {
41 pub uuid: Uuid,
42 pub timestamp: String,
43 pub content: Vec<ContentBlock>,
44 #[serde(default)]
47 pub is_meta: bool,
48 #[serde(default)]
50 pub is_compact_summary: bool,
51}
52
53#[derive(Debug, Clone, Serialize, Deserialize)]
55pub struct AssistantMessage {
56 pub uuid: Uuid,
57 pub timestamp: String,
58 pub content: Vec<ContentBlock>,
59 #[serde(skip_serializing_if = "Option::is_none")]
61 pub model: Option<String>,
62 #[serde(skip_serializing_if = "Option::is_none")]
64 pub usage: Option<Usage>,
65 #[serde(skip_serializing_if = "Option::is_none")]
67 pub stop_reason: Option<StopReason>,
68 #[serde(skip_serializing_if = "Option::is_none")]
70 pub request_id: Option<String>,
71}
72
73#[derive(Debug, Clone, Serialize, Deserialize)]
75pub struct SystemMessage {
76 pub uuid: Uuid,
77 pub timestamp: String,
78 pub subtype: SystemMessageType,
79 pub content: String,
80 #[serde(default)]
81 pub level: MessageLevel,
82}
83
84#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
86#[serde(rename_all = "snake_case")]
87pub enum SystemMessageType {
88 Informational,
89 ApiError,
90 CompactBoundary,
91 TurnDuration,
92 MemorySaved,
93 ToolProgress,
94}
95
96#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
98#[serde(rename_all = "snake_case")]
99pub enum MessageLevel {
100 #[default]
101 Info,
102 Warning,
103 Error,
104}
105
106#[derive(Debug, Clone, Serialize, Deserialize)]
112#[serde(tag = "type")]
113pub enum ContentBlock {
114 #[serde(rename = "text")]
116 Text { text: String },
117
118 #[serde(rename = "tool_use")]
120 ToolUse {
121 id: String,
122 name: String,
123 input: serde_json::Value,
124 },
125
126 #[serde(rename = "tool_result")]
130 ToolResult {
131 tool_use_id: String,
132 content: String,
133 #[serde(default)]
134 is_error: bool,
135 #[serde(default, skip_serializing_if = "Vec::is_empty")]
137 extra_content: Vec<ToolResultBlock>,
138 },
139
140 #[serde(rename = "thinking")]
142 Thinking {
143 thinking: String,
144 #[serde(skip_serializing_if = "Option::is_none")]
145 signature: Option<String>,
146 },
147
148 #[serde(rename = "image")]
150 Image {
151 #[serde(rename = "media_type")]
152 media_type: String,
153 data: String,
154 },
155
156 #[serde(rename = "document")]
158 Document {
159 #[serde(rename = "media_type")]
160 media_type: String,
161 data: String,
162 #[serde(skip_serializing_if = "Option::is_none")]
163 title: Option<String>,
164 },
165}
166
167#[derive(Debug, Clone, Serialize, Deserialize)]
169#[serde(tag = "type")]
170pub enum ToolResultBlock {
171 #[serde(rename = "text")]
172 Text { text: String },
173 #[serde(rename = "image")]
174 Image {
175 #[serde(rename = "media_type")]
176 media_type: String,
177 data: String,
178 },
179}
180
181impl ContentBlock {
182 pub fn as_text(&self) -> Option<&str> {
184 match self {
185 ContentBlock::Text { text } => Some(text),
186 _ => None,
187 }
188 }
189
190 pub fn as_tool_use(&self) -> Option<(&str, &str, &serde_json::Value)> {
192 match self {
193 ContentBlock::ToolUse { id, name, input } => Some((id, name, input)),
194 _ => None,
195 }
196 }
197}
198
199#[derive(Debug, Clone, Default, Serialize, Deserialize)]
205pub struct Usage {
206 pub input_tokens: u64,
207 pub output_tokens: u64,
208 #[serde(default)]
209 pub cache_creation_input_tokens: u64,
210 #[serde(default)]
211 pub cache_read_input_tokens: u64,
212}
213
214impl Usage {
215 pub fn total(&self) -> u64 {
217 self.input_tokens
218 + self.output_tokens
219 + self.cache_creation_input_tokens
220 + self.cache_read_input_tokens
221 }
222
223 pub fn merge(&mut self, other: &Usage) {
225 self.input_tokens = other.input_tokens;
226 self.output_tokens += other.output_tokens;
227 self.cache_creation_input_tokens = other.cache_creation_input_tokens;
228 self.cache_read_input_tokens = other.cache_read_input_tokens;
229 }
230}
231
232#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
234#[serde(rename_all = "snake_case")]
235pub enum StopReason {
236 EndTurn,
237 MaxTokens,
238 ToolUse,
239 StopSequence,
240}
241
242pub fn user_message(text: impl Into<String>) -> Message {
244 Message::User(UserMessage {
245 uuid: Uuid::new_v4(),
246 timestamp: chrono::Utc::now().to_rfc3339(),
247 content: vec![ContentBlock::Text { text: text.into() }],
248 is_meta: false,
249 is_compact_summary: false,
250 })
251}
252
253pub fn image_block_from_file(path: &std::path::Path) -> Result<ContentBlock, String> {
258 let data = std::fs::read(path).map_err(|e| format!("Failed to read image: {e}"))?;
259
260 let media_type = match path.extension().and_then(|e| e.to_str()) {
261 Some("png") => "image/png",
262 Some("jpg" | "jpeg") => "image/jpeg",
263 Some("gif") => "image/gif",
264 Some("webp") => "image/webp",
265 Some("svg") => "image/svg+xml",
266 _ => "application/octet-stream",
267 };
268
269 use std::io::Write;
270 let mut encoded = String::new();
271 {
272 let mut encoder = base64_encode_writer(&mut encoded);
273 encoder
274 .write_all(&data)
275 .map_err(|e| format!("base64 error: {e}"))?;
276 }
277
278 Ok(ContentBlock::Image {
279 media_type: media_type.to_string(),
280 data: encoded,
281 })
282}
283
284fn base64_encode_writer(output: &mut String) -> Base64Writer<'_> {
286 Base64Writer {
287 output,
288 buffer: Vec::new(),
289 }
290}
291
292struct Base64Writer<'a> {
293 output: &'a mut String,
294 buffer: Vec<u8>,
295}
296
297impl<'a> std::io::Write for Base64Writer<'a> {
298 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
299 self.buffer.extend_from_slice(buf);
300 Ok(buf.len())
301 }
302 fn flush(&mut self) -> std::io::Result<()> {
303 const CHARS: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
304 let mut i = 0;
305 while i + 2 < self.buffer.len() {
306 let b0 = self.buffer[i] as usize;
307 let b1 = self.buffer[i + 1] as usize;
308 let b2 = self.buffer[i + 2] as usize;
309 self.output.push(CHARS[b0 >> 2] as char);
310 self.output.push(CHARS[((b0 & 3) << 4) | (b1 >> 4)] as char);
311 self.output
312 .push(CHARS[((b1 & 0xf) << 2) | (b2 >> 6)] as char);
313 self.output.push(CHARS[b2 & 0x3f] as char);
314 i += 3;
315 }
316 let remaining = self.buffer.len() - i;
317 if remaining == 1 {
318 let b0 = self.buffer[i] as usize;
319 self.output.push(CHARS[b0 >> 2] as char);
320 self.output.push(CHARS[(b0 & 3) << 4] as char);
321 self.output.push('=');
322 self.output.push('=');
323 } else if remaining == 2 {
324 let b0 = self.buffer[i] as usize;
325 let b1 = self.buffer[i + 1] as usize;
326 self.output.push(CHARS[b0 >> 2] as char);
327 self.output.push(CHARS[((b0 & 3) << 4) | (b1 >> 4)] as char);
328 self.output.push(CHARS[(b1 & 0xf) << 2] as char);
329 self.output.push('=');
330 }
331 Ok(())
332 }
333}
334
335pub fn image_message(path: &std::path::Path, caption: &str) -> Result<Message, String> {
337 let image = image_block_from_file(path)?;
338 Ok(Message::User(UserMessage {
339 uuid: Uuid::new_v4(),
340 timestamp: chrono::Utc::now().to_rfc3339(),
341 content: vec![
342 image,
343 ContentBlock::Text {
344 text: caption.to_string(),
345 },
346 ],
347 is_meta: false,
348 is_compact_summary: false,
349 }))
350}
351
352pub fn tool_result_message(tool_use_id: &str, content: &str, is_error: bool) -> Message {
354 Message::User(UserMessage {
355 uuid: Uuid::new_v4(),
356 timestamp: chrono::Utc::now().to_rfc3339(),
357 content: vec![ContentBlock::ToolResult {
358 tool_use_id: tool_use_id.to_string(),
359 content: content.to_string(),
360 is_error,
361 extra_content: vec![],
362 }],
363 is_meta: true,
364 is_compact_summary: false,
365 })
366}
367
368pub fn messages_to_api_params(messages: &[Message]) -> Vec<serde_json::Value> {
370 messages
371 .iter()
372 .filter_map(|msg| match msg {
373 Message::User(u) => Some(serde_json::json!({
374 "role": "user",
375 "content": content_blocks_to_api(&u.content),
376 })),
377 Message::Assistant(a) => Some(serde_json::json!({
378 "role": "assistant",
379 "content": content_blocks_to_api(&a.content),
380 })),
381 Message::System(_) => None,
383 })
384 .collect()
385}
386
387fn content_blocks_to_api(blocks: &[ContentBlock]) -> serde_json::Value {
388 let api_blocks: Vec<serde_json::Value> = blocks
389 .iter()
390 .map(|block| match block {
391 ContentBlock::Text { text } => serde_json::json!({
392 "type": "text",
393 "text": text,
394 }),
395 ContentBlock::ToolUse { id, name, input } => serde_json::json!({
396 "type": "tool_use",
397 "id": id,
398 "name": name,
399 "input": input,
400 }),
401 ContentBlock::ToolResult {
402 tool_use_id,
403 content,
404 is_error,
405 ..
406 } => serde_json::json!({
407 "type": "tool_result",
408 "tool_use_id": tool_use_id,
409 "content": content,
410 "is_error": is_error,
411 }),
412 ContentBlock::Thinking {
413 thinking,
414 signature,
415 } => serde_json::json!({
416 "type": "thinking",
417 "thinking": thinking,
418 "signature": signature,
419 }),
420 ContentBlock::Image { media_type, data } => serde_json::json!({
421 "type": "image",
422 "source": {
423 "type": "base64",
424 "media_type": media_type,
425 "data": data,
426 }
427 }),
428 ContentBlock::Document {
429 media_type,
430 data,
431 title,
432 } => {
433 let mut doc = serde_json::json!({
434 "type": "document",
435 "source": {
436 "type": "base64",
437 "media_type": media_type,
438 "data": data,
439 }
440 });
441 if let Some(t) = title {
442 doc["title"] = serde_json::json!(t);
443 }
444 doc
445 }
446 })
447 .collect();
448
449 if api_blocks.len() == 1
451 && let Some(text) = blocks[0].as_text()
452 {
453 return serde_json::Value::String(text.to_string());
454 }
455
456 serde_json::Value::Array(api_blocks)
457}
458
459pub fn messages_to_api_params_cached(messages: &[Message]) -> Vec<serde_json::Value> {
465 let user_indices: Vec<usize> = messages
467 .iter()
468 .enumerate()
469 .filter(|(_, m)| matches!(m, Message::User(u) if !u.is_meta))
470 .map(|(i, _)| i)
471 .collect();
472
473 let cache_index = if user_indices.len() >= 2 {
474 Some(user_indices[user_indices.len() - 2])
475 } else {
476 None
477 };
478
479 messages
480 .iter()
481 .enumerate()
482 .filter_map(|(i, msg)| match msg {
483 Message::User(u) => {
484 let mut content = content_blocks_to_api(&u.content);
485 if Some(i) == cache_index
487 && let serde_json::Value::Array(ref mut blocks) = content
488 && let Some(last) = blocks.last_mut()
489 {
490 last["cache_control"] = serde_json::json!({"type": "ephemeral"});
491 }
492 Some(serde_json::json!({
493 "role": "user",
494 "content": content,
495 }))
496 }
497 Message::Assistant(a) => Some(serde_json::json!({
498 "role": "assistant",
499 "content": content_blocks_to_api(&a.content),
500 })),
501 Message::System(_) => None,
502 })
503 .collect()
504}
505
506#[cfg(test)]
507mod tests {
508 use super::*;
509
510 #[test]
511 fn test_user_message_creates_text() {
512 let msg = user_message("hello");
513 if let Message::User(u) = &msg {
514 assert_eq!(u.content.len(), 1);
515 assert_eq!(u.content[0].as_text(), Some("hello"));
516 assert!(!u.is_meta);
517 } else {
518 panic!("Expected User");
519 }
520 }
521
522 #[test]
523 fn test_tool_result_message_success() {
524 let msg = tool_result_message("c1", "output", false);
525 if let Message::User(u) = &msg {
526 assert!(u.is_meta);
527 if let ContentBlock::ToolResult {
528 tool_use_id,
529 is_error,
530 ..
531 } = &u.content[0]
532 {
533 assert_eq!(tool_use_id, "c1");
534 assert!(!is_error);
535 }
536 }
537 }
538
539 #[test]
540 fn test_tool_result_message_error() {
541 let msg = tool_result_message("c2", "fail", true);
542 if let Message::User(u) = &msg
543 && let ContentBlock::ToolResult { is_error, .. } = &u.content[0]
544 {
545 assert!(is_error);
546 }
547 }
548
549 #[test]
550 fn test_as_text() {
551 assert_eq!(
552 ContentBlock::Text { text: "hi".into() }.as_text(),
553 Some("hi")
554 );
555 assert_eq!(
556 ContentBlock::ToolUse {
557 id: "1".into(),
558 name: "X".into(),
559 input: serde_json::json!({})
560 }
561 .as_text(),
562 None
563 );
564 }
565
566 #[test]
567 fn test_as_tool_use() {
568 let b = ContentBlock::ToolUse {
569 id: "a".into(),
570 name: "B".into(),
571 input: serde_json::json!(1),
572 };
573 let (id, name, _) = b.as_tool_use().unwrap();
574 assert_eq!(id, "a");
575 assert_eq!(name, "B");
576 assert!(
577 ContentBlock::Text { text: "x".into() }
578 .as_tool_use()
579 .is_none()
580 );
581 }
582
583 #[test]
584 fn test_usage_total() {
585 let u = Usage {
586 input_tokens: 10,
587 output_tokens: 20,
588 cache_creation_input_tokens: 3,
589 cache_read_input_tokens: 7,
590 };
591 assert_eq!(u.total(), 40);
592 }
593
594 #[test]
595 fn test_usage_merge() {
596 let mut u = Usage {
597 input_tokens: 100,
598 output_tokens: 50,
599 ..Default::default()
600 };
601 u.merge(&Usage {
602 input_tokens: 200,
603 output_tokens: 30,
604 cache_creation_input_tokens: 5,
605 cache_read_input_tokens: 10,
606 });
607 assert_eq!(u.input_tokens, 200);
608 assert_eq!(u.output_tokens, 80);
609 assert_eq!(u.cache_creation_input_tokens, 5);
610 }
611
612 #[test]
613 fn test_usage_default() {
614 assert_eq!(Usage::default().total(), 0);
615 }
616
617 #[test]
618 fn test_message_uuid_accessible() {
619 let _ = user_message("t").uuid();
620 }
621
622 #[test]
623 fn test_messages_to_api_params_filters_system() {
624 let messages = vec![user_message("hi")];
625 let params = messages_to_api_params(&messages);
626 assert_eq!(params.len(), 1);
627 assert_eq!(params[0]["role"], "user");
628 }
629}