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