1use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
7#[serde(rename_all = "lowercase")]
8pub enum Role {
9 System,
11 Developer,
13 User,
15 Assistant,
17 Tool,
19}
20
21#[derive(Debug, Clone, Serialize, Deserialize)]
23#[serde(untagged)]
24pub enum MessageContent {
25 Text(String),
27 Parts(Vec<ContentPart>),
29}
30
31impl MessageContent {
32 pub fn text(text: impl Into<String>) -> Self {
34 MessageContent::Text(text.into())
35 }
36
37 pub fn parts(parts: Vec<ContentPart>) -> Self {
39 MessageContent::Parts(parts)
40 }
41}
42
43impl From<&str> for MessageContent {
44 fn from(s: &str) -> Self {
45 MessageContent::Text(s.to_string())
46 }
47}
48
49impl From<String> for MessageContent {
50 fn from(s: String) -> Self {
51 MessageContent::Text(s)
52 }
53}
54
55#[derive(Debug, Clone, Serialize, Deserialize)]
57pub struct ContentPart {
58 #[serde(rename = "type")]
60 pub content_type: ContentType,
61 #[serde(skip_serializing_if = "Option::is_none")]
63 pub text: Option<String>,
64 #[serde(skip_serializing_if = "Option::is_none")]
66 pub image_url: Option<ImageUrl>,
67 #[serde(skip_serializing_if = "Option::is_none")]
69 pub input_audio: Option<InputAudio>,
70 #[serde(skip_serializing_if = "Option::is_none")]
72 pub video_url: Option<VideoUrl>,
73}
74
75impl ContentPart {
76 pub fn text(text: impl Into<String>) -> Self {
78 Self {
79 content_type: ContentType::Text,
80 text: Some(text.into()),
81 image_url: None,
82 input_audio: None,
83 video_url: None,
84 }
85 }
86
87 pub fn image_url(url: impl Into<String>) -> Self {
89 Self {
90 content_type: ContentType::ImageUrl,
91 text: None,
92 image_url: Some(ImageUrl {
93 url: url.into(),
94 detail: None,
95 }),
96 input_audio: None,
97 video_url: None,
98 }
99 }
100
101 pub fn image_base64(media_type: &str, data: impl Into<String>) -> Self {
103 let url = format!("data:{};base64,{}", media_type, data.into());
104 Self {
105 content_type: ContentType::ImageUrl,
106 text: None,
107 image_url: Some(ImageUrl { url, detail: None }),
108 input_audio: None,
109 video_url: None,
110 }
111 }
112
113 pub fn audio_base64(data: impl Into<String>) -> Self {
115 Self {
116 content_type: ContentType::InputAudio,
117 text: None,
118 image_url: None,
119 input_audio: Some(InputAudio {
120 data: data.into(),
121 format: AudioInputFormat::Wav,
122 }),
123 video_url: None,
124 }
125 }
126
127 pub fn video_url(url: impl Into<String>) -> Self {
129 Self {
130 content_type: ContentType::VideoUrl,
131 text: None,
132 image_url: None,
133 input_audio: None,
134 video_url: Some(VideoUrl { url: url.into() }),
135 }
136 }
137}
138
139#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
141#[serde(rename_all = "snake_case")]
142pub enum ContentType {
143 Text,
145 ImageUrl,
147 InputAudio,
149 VideoUrl,
151}
152
153#[derive(Debug, Clone, Serialize, Deserialize)]
155pub struct ImageUrl {
156 pub url: String,
158 #[serde(skip_serializing_if = "Option::is_none")]
160 pub detail: Option<ImageDetail>,
161}
162
163#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
165#[serde(rename_all = "lowercase")]
166pub enum ImageDetail {
167 Auto,
169 Low,
171 High,
173}
174
175#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
177#[serde(rename_all = "lowercase")]
178pub enum AudioInputFormat {
179 Wav,
181 Mp3,
183}
184
185#[derive(Debug, Clone, Serialize, Deserialize)]
187pub struct InputAudio {
188 pub data: String,
190 pub format: AudioInputFormat,
192}
193
194#[derive(Debug, Clone, Serialize, Deserialize)]
196pub struct VideoUrl {
197 pub url: String,
199}
200
201#[derive(Debug, Clone, Serialize, Deserialize)]
203pub struct Message {
204 pub role: Role,
206 pub content: MessageContent,
208 #[serde(skip_serializing_if = "Option::is_none")]
210 pub name: Option<String>,
211 #[serde(skip_serializing_if = "Option::is_none")]
213 pub tool_calls: Option<Vec<ToolCall>>,
214 #[serde(skip_serializing_if = "Option::is_none")]
216 pub tool_call_id: Option<String>,
217 #[serde(skip_serializing_if = "Option::is_none")]
219 pub reasoning_content: Option<String>,
220 #[serde(skip_serializing_if = "Option::is_none")]
222 pub audio: Option<MessageAudio>,
223}
224
225impl Message {
226 pub fn new(role: Role, content: MessageContent) -> Self {
228 Self {
229 role,
230 content,
231 name: None,
232 tool_calls: None,
233 tool_call_id: None,
234 reasoning_content: None,
235 audio: None,
236 }
237 }
238
239 pub fn system(content: impl Into<MessageContent>) -> Self {
241 Self::new(Role::System, content.into())
242 }
243
244 pub fn developer(content: impl Into<MessageContent>) -> Self {
246 Self::new(Role::Developer, content.into())
247 }
248
249 pub fn user(content: impl Into<MessageContent>) -> Self {
251 Self::new(Role::User, content.into())
252 }
253
254 pub fn assistant(content: impl Into<MessageContent>) -> Self {
256 Self::new(Role::Assistant, content.into())
257 }
258
259 pub fn tool(tool_call_id: impl Into<String>, content: impl Into<String>) -> Self {
261 Self {
262 role: Role::Tool,
263 content: MessageContent::Text(content.into()),
264 name: None,
265 tool_calls: None,
266 tool_call_id: Some(tool_call_id.into()),
267 reasoning_content: None,
268 audio: None,
269 }
270 }
271
272 pub fn with_name(mut self, name: impl Into<String>) -> Self {
274 self.name = Some(name.into());
275 self
276 }
277
278 pub fn with_tool_calls(mut self, tool_calls: Vec<ToolCall>) -> Self {
280 self.tool_calls = Some(tool_calls);
281 self
282 }
283
284 pub fn with_reasoning_content(mut self, content: impl Into<String>) -> Self {
286 self.reasoning_content = Some(content.into());
287 self
288 }
289}
290
291#[derive(Debug, Clone, Serialize, Deserialize)]
293pub struct MessageAudio {
294 pub id: Option<String>,
296 pub data: Option<String>,
298 pub expires_at: Option<i64>,
300 pub transcript: Option<String>,
302}
303
304#[derive(Debug, Clone, Serialize, Deserialize)]
306pub struct ToolCall {
307 pub id: String,
309 #[serde(rename = "type")]
311 pub tool_type: ToolCallType,
312 pub function: FunctionCall,
314}
315
316impl ToolCall {
317 pub fn new(id: impl Into<String>, function: FunctionCall) -> Self {
319 Self {
320 id: id.into(),
321 tool_type: ToolCallType::Function,
322 function,
323 }
324 }
325}
326
327#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
329#[serde(rename_all = "lowercase")]
330pub enum ToolCallType {
331 Function,
333}
334
335#[derive(Debug, Clone, Serialize, Deserialize)]
337pub struct FunctionCall {
338 pub name: String,
340 pub arguments: String,
342}
343
344impl FunctionCall {
345 pub fn new(name: impl Into<String>, arguments: impl Into<String>) -> Self {
347 Self {
348 name: name.into(),
349 arguments: arguments.into(),
350 }
351 }
352
353 pub fn parse_arguments<T: serde::de::DeserializeOwned>(&self) -> serde_json::Result<T> {
355 serde_json::from_str(&self.arguments)
356 }
357}
358
359#[cfg(test)]
360mod tests {
361 use super::*;
362
363 #[test]
364 fn test_message_creation() {
365 let msg = Message::user(MessageContent::Text("Hello".to_string()));
366 assert_eq!(msg.role, Role::User);
367 }
368
369 #[test]
370 fn test_message_serialization() {
371 let msg = Message::user(MessageContent::Text("Hello".to_string()));
372 let json = serde_json::to_string(&msg).unwrap();
373 assert!(json.contains("\"role\":\"user\""));
374 assert!(json.contains("\"content\":\"Hello\""));
375 }
376
377 #[test]
378 fn test_content_part_text() {
379 let part = ContentPart::text("Hello");
380 assert_eq!(part.content_type, ContentType::Text);
381 assert_eq!(part.text, Some("Hello".to_string()));
382 }
383
384 #[test]
385 fn test_content_part_image_url() {
386 let part = ContentPart::image_url("https://example.com/image.png");
387 assert_eq!(part.content_type, ContentType::ImageUrl);
388 assert!(part.image_url.is_some());
389 }
390
391 #[test]
392 fn test_content_part_video_url() {
393 let part = ContentPart::video_url("https://example.com/video.mp4");
394 assert_eq!(part.content_type, ContentType::VideoUrl);
395 assert!(part.video_url.is_some());
396 }
397
398 #[test]
399 fn test_function_call_parse() {
400 let fc = FunctionCall::new("test", r#"{"arg": "value"}"#);
401 let parsed: serde_json::Value = fc.parse_arguments().unwrap();
402 assert_eq!(parsed["arg"], "value");
403 }
404
405 #[test]
406 fn test_multimodal_message() {
407 let content = MessageContent::Parts(vec![
408 ContentPart::text("What's in this image?"),
409 ContentPart::image_url("https://example.com/image.png"),
410 ]);
411 let msg = Message::user(content);
412 assert_eq!(msg.role, Role::User);
413 }
414
415 #[test]
416 fn test_tool_message() {
417 let msg = Message::tool("call_123", "result data");
418 assert_eq!(msg.role, Role::Tool);
419 assert_eq!(msg.tool_call_id, Some("call_123".to_string()));
420 }
421}