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}
199
200impl Usage {
201 pub fn new(prompt_tokens: u32, completion_tokens: u32) -> Self {
203 Self {
204 prompt_tokens,
205 completion_tokens,
206 total_tokens: prompt_tokens + completion_tokens,
207 }
208 }
209}
210
211#[derive(Debug, Clone)]
213pub struct ChatResponse {
214 pub message: Message,
216 pub usage: Usage,
218 pub finish_reason: Option<String>,
220}
221
222pub fn serialize_messages_to_stateless_history(messages: &[Message]) -> Vec<Value> {
231 let mut history = Vec::new();
232
233 for msg in messages {
234 if msg.role == Role::System {
236 continue;
237 }
238
239 let role_str = match msg.role {
240 Role::User => "user",
241 Role::Assistant => "assistant",
242 Role::Tool => "tool",
243 Role::System => continue, };
245
246 match &msg.content {
247 MessageContent::Text(text) => {
248 if msg.role == Role::Assistant && text.trim().is_empty() {
250 continue;
251 }
252 history.push(serde_json::json!({
253 "role": role_str,
254 "content": text,
255 }));
256 }
257 MessageContent::Blocks(blocks) => {
258 let mut text_parts = Vec::new();
260
261 for block in blocks {
262 match block {
263 ContentBlock::Text { text } => {
264 text_parts.push(text.clone());
265 }
266 ContentBlock::ToolUse { id, name, input } => {
267 if !text_parts.is_empty() {
269 let combined = text_parts.join("\n");
270 if !(msg.role == Role::Assistant && combined.trim().is_empty()) {
271 history.push(serde_json::json!({
272 "role": role_str,
273 "content": combined,
274 }));
275 }
276 text_parts.clear();
277 }
278 history.push(serde_json::json!({
279 "role": "function_call",
280 "call_id": id,
281 "name": name,
282 "arguments": input.to_string(),
283 }));
284 }
285 ContentBlock::ToolResult {
286 tool_use_id,
287 content,
288 ..
289 } => {
290 if !text_parts.is_empty() {
292 let combined = text_parts.join("\n");
293 if !(msg.role == Role::Assistant && combined.trim().is_empty()) {
294 history.push(serde_json::json!({
295 "role": role_str,
296 "content": combined,
297 }));
298 }
299 text_parts.clear();
300 }
301 history.push(serde_json::json!({
302 "role": "tool",
303 "call_id": tool_use_id,
304 "content": content,
305 }));
306 }
307 ContentBlock::Image { .. } => {
308 }
310 }
311 }
312
313 if !text_parts.is_empty() {
315 let combined = text_parts.join("\n");
316 if !(msg.role == Role::Assistant && combined.trim().is_empty()) {
317 history.push(serde_json::json!({
318 "role": role_str,
319 "content": combined,
320 }));
321 }
322 }
323 }
324 }
325 }
326
327 history
328}
329
330#[derive(Debug, Clone)]
332pub enum StreamChunk {
333 Text(String),
335 ToolUse {
337 id: String,
339 name: String,
341 },
342 ToolInputDelta {
344 id: String,
346 partial_json: String,
348 },
349 ToolCall {
351 call_id: String,
353 response_id: String,
355 chat_id: Option<String>,
357 tool_name: String,
359 server: String,
361 parameters: serde_json::Value,
363 },
364 Usage(Usage),
366 Done,
368}
369
370#[cfg(test)]
371mod tests {
372 use super::*;
373 use serde_json::json;
374
375 #[test]
376 fn test_message_user() {
377 let msg = Message::user("Hello");
378 assert_eq!(msg.role, Role::User);
379 assert_eq!(msg.text(), Some("Hello"));
380 }
381
382 #[test]
383 fn test_message_assistant() {
384 let msg = Message::assistant("Response");
385 assert_eq!(msg.role, Role::Assistant);
386 assert_eq!(msg.text(), Some("Response"));
387 }
388
389 #[test]
390 fn test_message_tool_result() {
391 let msg = Message::tool_result("tool-1", "Result");
392 assert_eq!(msg.role, Role::Tool);
393 }
394
395 #[test]
396 fn test_usage_new() {
397 let usage = Usage::new(100, 50);
398 assert_eq!(usage.total_tokens, 150);
399 }
400
401 #[test]
402 fn test_role_serialization() {
403 let role = Role::User;
404 let json = serde_json::to_string(&role).unwrap();
405 assert_eq!(json, "\"user\"");
406 }
407
408 #[test]
409 fn test_stateless_history_simple_text() {
410 let messages = vec![Message::user("Hello"), Message::assistant("Hi there")];
411 let history = serialize_messages_to_stateless_history(&messages);
412 assert_eq!(history.len(), 2);
413 assert_eq!(history[0]["role"], "user");
414 assert_eq!(history[1]["role"], "assistant");
415 }
416
417 #[test]
418 fn test_stateless_history_skips_system() {
419 let messages = vec![Message::system("You are helpful"), Message::user("Hello")];
420 let history = serialize_messages_to_stateless_history(&messages);
421 assert_eq!(history.len(), 1);
422 assert_eq!(history[0]["role"], "user");
423 }
424
425 #[test]
426 fn test_stateless_history_tool_round_trip() {
427 let messages = vec![
428 Message::user("Read the file"),
429 Message {
430 role: Role::Assistant,
431 content: MessageContent::Blocks(vec![
432 ContentBlock::Text {
433 text: "I'll check.".to_string(),
434 },
435 ContentBlock::ToolUse {
436 id: "call-1".to_string(),
437 name: "read_file".to_string(),
438 input: json!({"path": "main.rs"}),
439 },
440 ]),
441 name: None,
442 metadata: None,
443 },
444 Message::tool_result("call-1", "fn main() {}"),
445 Message::assistant("The file contains a main function."),
446 ];
447 let history = serialize_messages_to_stateless_history(&messages);
448 assert_eq!(history.len(), 5);
449 assert_eq!(history[0]["role"], "user");
450 assert_eq!(history[1]["role"], "assistant");
451 assert_eq!(history[2]["role"], "function_call");
452 assert_eq!(history[3]["role"], "tool");
453 assert_eq!(history[4]["role"], "assistant");
454 }
455}