1use serde::{Deserialize, Serialize};
2use serde_json::Value;
3
4#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
6#[serde(rename_all = "lowercase")]
7pub enum Role {
8 User,
10 Assistant,
12 System,
14 Tool,
16}
17
18#[derive(Debug, Clone, Serialize, Deserialize)]
20#[serde(untagged)]
21pub enum MessageContent {
22 Text(String),
24 Blocks(Vec<ContentBlock>),
26}
27
28#[derive(Debug, Clone, Serialize, Deserialize)]
30#[serde(tag = "type", rename_all = "snake_case")]
31pub enum ContentBlock {
32 Text {
34 text: String,
36 },
37 Image {
39 source: ImageSource,
41 },
42 ToolUse {
44 id: String,
46 name: String,
48 input: Value,
50 },
51 ToolResult {
53 tool_use_id: String,
55 content: String,
57 #[serde(skip_serializing_if = "Option::is_none")]
59 is_error: Option<bool>,
60 },
61}
62
63#[derive(Debug, Clone, Serialize, Deserialize)]
65#[serde(tag = "type", rename_all = "snake_case")]
66pub enum ImageSource {
67 Base64 {
69 media_type: String,
71 data: String,
73 },
74}
75
76#[derive(Debug, Clone, Serialize, Deserialize)]
78pub struct Message {
79 pub role: Role,
81 pub content: MessageContent,
83 #[serde(skip_serializing_if = "Option::is_none")]
85 pub name: Option<String>,
86 #[serde(skip_serializing_if = "Option::is_none")]
88 pub metadata: Option<Value>,
89}
90
91impl Message {
92 pub fn user<S: Into<String>>(content: S) -> Self {
94 Self {
95 role: Role::User,
96 content: MessageContent::Text(content.into()),
97 name: None,
98 metadata: None,
99 }
100 }
101
102 pub fn assistant<S: Into<String>>(content: S) -> Self {
104 Self {
105 role: Role::Assistant,
106 content: MessageContent::Text(content.into()),
107 name: None,
108 metadata: None,
109 }
110 }
111
112 pub fn system<S: Into<String>>(content: S) -> Self {
114 Self {
115 role: Role::System,
116 content: MessageContent::Text(content.into()),
117 name: None,
118 metadata: None,
119 }
120 }
121
122 pub fn tool_result<S: Into<String>>(tool_use_id: S, content: S) -> Self {
124 Self {
125 role: Role::Tool,
126 content: MessageContent::Blocks(vec![ContentBlock::ToolResult {
127 tool_use_id: tool_use_id.into(),
128 content: content.into(),
129 is_error: None,
130 }]),
131 name: None,
132 metadata: None,
133 }
134 }
135
136 pub fn text(&self) -> Option<&str> {
138 match &self.content {
139 MessageContent::Text(text) => Some(text),
140 MessageContent::Blocks(_) => None,
141 }
142 }
143
144 pub fn text_or_summary(&self) -> String {
149 match &self.content {
150 MessageContent::Text(text) => text.clone(),
151 MessageContent::Blocks(blocks) => {
152 let mut parts = Vec::new();
153 for block in blocks {
154 match block {
155 ContentBlock::Text { text } => {
156 parts.push(text.clone());
157 }
158 ContentBlock::ToolUse { name, input, .. } => {
159 parts.push(format!("[Called tool: {} with args: {}]", name, input));
160 }
161 ContentBlock::ToolResult {
162 content, is_error, ..
163 } => {
164 if is_error == &Some(true) {
165 parts.push(format!("[Tool error: {}]", content));
166 } else {
167 parts.push(format!("[Tool result: {}]", content));
168 }
169 }
170 ContentBlock::Image { .. } => {
171 parts.push("[Image]".to_string());
172 }
173 }
174 }
175 parts.join("\n")
176 }
177 }
178 }
179
180 pub fn text_mut(&mut self) -> Option<&mut String> {
182 match &mut self.content {
183 MessageContent::Text(text) => Some(text),
184 MessageContent::Blocks(_) => None,
185 }
186 }
187}
188
189#[derive(Debug, Clone, Serialize, Deserialize, Default)]
191pub struct Usage {
192 pub prompt_tokens: u32,
194 pub completion_tokens: u32,
196 pub total_tokens: u32,
198 #[serde(default, skip_serializing_if = "is_zero_u32")]
204 pub cache_creation_input_tokens: u32,
205 #[serde(default, skip_serializing_if = "is_zero_u32")]
210 pub cache_read_input_tokens: u32,
211}
212
213fn is_zero_u32(v: &u32) -> bool {
214 *v == 0
215}
216
217impl Usage {
218 pub fn new(prompt_tokens: u32, completion_tokens: u32) -> Self {
220 Self {
221 prompt_tokens,
222 completion_tokens,
223 total_tokens: prompt_tokens + completion_tokens,
224 cache_creation_input_tokens: 0,
225 cache_read_input_tokens: 0,
226 }
227 }
228
229 pub fn with_cache(
231 prompt_tokens: u32,
232 completion_tokens: u32,
233 cache_creation_input_tokens: u32,
234 cache_read_input_tokens: u32,
235 ) -> Self {
236 Self {
237 prompt_tokens,
238 completion_tokens,
239 total_tokens: prompt_tokens + completion_tokens,
240 cache_creation_input_tokens,
241 cache_read_input_tokens,
242 }
243 }
244}
245
246#[derive(Debug, Clone)]
248pub struct ChatResponse {
249 pub message: Message,
251 pub usage: Usage,
253 pub finish_reason: Option<String>,
255}
256
257pub fn serialize_messages_to_stateless_history(messages: &[Message]) -> Vec<Value> {
266 let mut history = Vec::new();
267
268 for msg in messages {
269 if msg.role == Role::System {
271 continue;
272 }
273
274 let role_str = match msg.role {
275 Role::User => "user",
276 Role::Assistant => "assistant",
277 Role::Tool => "tool",
278 Role::System => continue, };
280
281 match &msg.content {
282 MessageContent::Text(text) => {
283 if msg.role == Role::Assistant && text.trim().is_empty() {
285 continue;
286 }
287 history.push(serde_json::json!({
288 "role": role_str,
289 "content": text,
290 }));
291 }
292 MessageContent::Blocks(blocks) => {
293 let mut text_parts = Vec::new();
295
296 for block in blocks {
297 match block {
298 ContentBlock::Text { text } => {
299 text_parts.push(text.clone());
300 }
301 ContentBlock::ToolUse { id, name, input } => {
302 if !text_parts.is_empty() {
304 let combined = text_parts.join("\n");
305 if !(msg.role == Role::Assistant && combined.trim().is_empty()) {
306 history.push(serde_json::json!({
307 "role": role_str,
308 "content": combined,
309 }));
310 }
311 text_parts.clear();
312 }
313 history.push(serde_json::json!({
314 "role": "function_call",
315 "call_id": id,
316 "name": name,
317 "arguments": input.to_string(),
318 }));
319 }
320 ContentBlock::ToolResult {
321 tool_use_id,
322 content,
323 ..
324 } => {
325 if !text_parts.is_empty() {
327 let combined = text_parts.join("\n");
328 if !(msg.role == Role::Assistant && combined.trim().is_empty()) {
329 history.push(serde_json::json!({
330 "role": role_str,
331 "content": combined,
332 }));
333 }
334 text_parts.clear();
335 }
336 history.push(serde_json::json!({
337 "role": "tool",
338 "call_id": tool_use_id,
339 "content": content,
340 }));
341 }
342 ContentBlock::Image { .. } => {
343 }
345 }
346 }
347
348 if !text_parts.is_empty() {
350 let combined = text_parts.join("\n");
351 if !(msg.role == Role::Assistant && combined.trim().is_empty()) {
352 history.push(serde_json::json!({
353 "role": role_str,
354 "content": combined,
355 }));
356 }
357 }
358 }
359 }
360 }
361
362 history
363}
364
365#[derive(Debug, Clone)]
367pub enum StreamChunk {
368 Text(String),
370 ToolUse {
372 id: String,
374 name: String,
376 },
377 ToolInputDelta {
379 id: String,
381 partial_json: String,
383 },
384 ToolCall {
386 call_id: String,
388 response_id: String,
390 chat_id: Option<String>,
392 tool_name: String,
394 server: String,
396 parameters: serde_json::Value,
398 },
399 Usage(Usage),
401 ContextCompacted {
407 summary: String,
409 tokens_freed: Option<u32>,
411 },
412 Done,
414}
415
416#[cfg(test)]
417mod tests {
418 use super::*;
419 use serde_json::json;
420
421 #[test]
422 fn test_message_user() {
423 let msg = Message::user("Hello");
424 assert_eq!(msg.role, Role::User);
425 assert_eq!(msg.text(), Some("Hello"));
426 }
427
428 #[test]
429 fn test_message_assistant() {
430 let msg = Message::assistant("Response");
431 assert_eq!(msg.role, Role::Assistant);
432 assert_eq!(msg.text(), Some("Response"));
433 }
434
435 #[test]
436 fn test_message_tool_result() {
437 let msg = Message::tool_result("tool-1", "Result");
438 assert_eq!(msg.role, Role::Tool);
439 }
440
441 #[test]
442 fn test_usage_new() {
443 let usage = Usage::new(100, 50);
444 assert_eq!(usage.total_tokens, 150);
445 }
446
447 #[test]
448 fn test_role_serialization() {
449 let role = Role::User;
450 let json = serde_json::to_string(&role).unwrap();
451 assert_eq!(json, "\"user\"");
452 }
453
454 #[test]
455 fn test_stateless_history_simple_text() {
456 let messages = vec![Message::user("Hello"), Message::assistant("Hi there")];
457 let history = serialize_messages_to_stateless_history(&messages);
458 assert_eq!(history.len(), 2);
459 assert_eq!(history[0]["role"], "user");
460 assert_eq!(history[1]["role"], "assistant");
461 }
462
463 #[test]
464 fn test_stateless_history_skips_system() {
465 let messages = vec![Message::system("You are helpful"), Message::user("Hello")];
466 let history = serialize_messages_to_stateless_history(&messages);
467 assert_eq!(history.len(), 1);
468 assert_eq!(history[0]["role"], "user");
469 }
470
471 #[test]
472 fn test_stateless_history_tool_round_trip() {
473 let messages = vec![
474 Message::user("Read the file"),
475 Message {
476 role: Role::Assistant,
477 content: MessageContent::Blocks(vec![
478 ContentBlock::Text {
479 text: "I'll check.".to_string(),
480 },
481 ContentBlock::ToolUse {
482 id: "call-1".to_string(),
483 name: "read_file".to_string(),
484 input: json!({"path": "main.rs"}),
485 },
486 ]),
487 name: None,
488 metadata: None,
489 },
490 Message::tool_result("call-1", "fn main() {}"),
491 Message::assistant("The file contains a main function."),
492 ];
493 let history = serialize_messages_to_stateless_history(&messages);
494 assert_eq!(history.len(), 5);
495 assert_eq!(history[0]["role"], "user");
496 assert_eq!(history[1]["role"], "assistant");
497 assert_eq!(history[2]["role"], "function_call");
498 assert_eq!(history[3]["role"], "tool");
499 assert_eq!(history[4]["role"], "assistant");
500 }
501}