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 {
108 url,
109 detail: None,
110 }),
111 input_audio: None,
112 video_url: None,
113 }
114 }
115
116 pub fn audio_base64(data: impl Into<String>) -> Self {
118 Self {
119 content_type: ContentType::InputAudio,
120 text: None,
121 image_url: None,
122 input_audio: Some(InputAudio {
123 data: data.into(),
124 format: AudioInputFormat::Wav,
125 }),
126 video_url: None,
127 }
128 }
129
130 pub fn video_url(url: impl Into<String>) -> Self {
132 Self {
133 content_type: ContentType::VideoUrl,
134 text: None,
135 image_url: None,
136 input_audio: None,
137 video_url: Some(VideoUrl { url: url.into() }),
138 }
139 }
140}
141
142#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
144#[serde(rename_all = "snake_case")]
145pub enum ContentType {
146 Text,
148 ImageUrl,
150 InputAudio,
152 VideoUrl,
154}
155
156#[derive(Debug, Clone, Serialize, Deserialize)]
158pub struct ImageUrl {
159 pub url: String,
161 #[serde(skip_serializing_if = "Option::is_none")]
163 pub detail: Option<ImageDetail>,
164}
165
166#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
168#[serde(rename_all = "lowercase")]
169pub enum ImageDetail {
170 Auto,
172 Low,
174 High,
176}
177
178#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
180#[serde(rename_all = "lowercase")]
181pub enum AudioInputFormat {
182 Wav,
184 Mp3,
186}
187
188#[derive(Debug, Clone, Serialize, Deserialize)]
190pub struct InputAudio {
191 pub data: String,
193 pub format: AudioInputFormat,
195}
196
197#[derive(Debug, Clone, Serialize, Deserialize)]
199pub struct VideoUrl {
200 pub url: String,
202}
203
204#[derive(Debug, Clone, Serialize, Deserialize)]
206pub struct Message {
207 pub role: Role,
209 pub content: MessageContent,
211 #[serde(skip_serializing_if = "Option::is_none")]
213 pub name: Option<String>,
214 #[serde(skip_serializing_if = "Option::is_none")]
216 pub tool_calls: Option<Vec<ToolCall>>,
217 #[serde(skip_serializing_if = "Option::is_none")]
219 pub tool_call_id: Option<String>,
220 #[serde(skip_serializing_if = "Option::is_none")]
222 pub reasoning_content: Option<String>,
223 #[serde(skip_serializing_if = "Option::is_none")]
225 pub audio: Option<MessageAudio>,
226}
227
228impl Message {
229 pub fn new(role: Role, content: MessageContent) -> Self {
231 Self {
232 role,
233 content,
234 name: None,
235 tool_calls: None,
236 tool_call_id: None,
237 reasoning_content: None,
238 audio: None,
239 }
240 }
241
242 pub fn system(content: impl Into<MessageContent>) -> Self {
244 Self::new(Role::System, content.into())
245 }
246
247 pub fn developer(content: impl Into<MessageContent>) -> Self {
249 Self::new(Role::Developer, content.into())
250 }
251
252 pub fn user(content: impl Into<MessageContent>) -> Self {
254 Self::new(Role::User, content.into())
255 }
256
257 pub fn assistant(content: impl Into<MessageContent>) -> Self {
259 Self::new(Role::Assistant, content.into())
260 }
261
262 pub fn tool(tool_call_id: impl Into<String>, content: impl Into<String>) -> Self {
264 Self {
265 role: Role::Tool,
266 content: MessageContent::Text(content.into()),
267 name: None,
268 tool_calls: None,
269 tool_call_id: Some(tool_call_id.into()),
270 reasoning_content: None,
271 audio: None,
272 }
273 }
274
275 pub fn with_name(mut self, name: impl Into<String>) -> Self {
277 self.name = Some(name.into());
278 self
279 }
280
281 pub fn with_tool_calls(mut self, tool_calls: Vec<ToolCall>) -> Self {
283 self.tool_calls = Some(tool_calls);
284 self
285 }
286
287 pub fn with_reasoning_content(mut self, content: impl Into<String>) -> Self {
289 self.reasoning_content = Some(content.into());
290 self
291 }
292}
293
294#[derive(Debug, Clone, Serialize, Deserialize)]
296pub struct MessageAudio {
297 pub id: Option<String>,
299 pub data: Option<String>,
301 pub expires_at: Option<i64>,
303 pub transcript: Option<String>,
305}
306
307#[derive(Debug, Clone, Serialize, Deserialize)]
309pub struct ToolCall {
310 pub id: String,
312 #[serde(rename = "type")]
314 pub tool_type: ToolCallType,
315 pub function: FunctionCall,
317}
318
319impl ToolCall {
320 pub fn new(id: impl Into<String>, function: FunctionCall) -> Self {
322 Self {
323 id: id.into(),
324 tool_type: ToolCallType::Function,
325 function,
326 }
327 }
328}
329
330#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
332#[serde(rename_all = "lowercase")]
333pub enum ToolCallType {
334 Function,
336}
337
338#[derive(Debug, Clone, Serialize, Deserialize)]
340pub struct FunctionCall {
341 pub name: String,
343 pub arguments: String,
345}
346
347impl FunctionCall {
348 pub fn new(name: impl Into<String>, arguments: impl Into<String>) -> Self {
350 Self {
351 name: name.into(),
352 arguments: arguments.into(),
353 }
354 }
355
356 pub fn parse_arguments<T: serde::de::DeserializeOwned>(&self) -> serde_json::Result<T> {
358 serde_json::from_str(&self.arguments)
359 }
360}
361
362#[cfg(test)]
363mod tests {
364 use super::*;
365
366 #[test]
367 fn test_message_creation() {
368 let msg = Message::user(MessageContent::Text("Hello".to_string()));
369 assert_eq!(msg.role, Role::User);
370 }
371
372 #[test]
373 fn test_message_serialization() {
374 let msg = Message::user(MessageContent::Text("Hello".to_string()));
375 let json = serde_json::to_string(&msg).unwrap();
376 assert!(json.contains("\"role\":\"user\""));
377 assert!(json.contains("\"content\":\"Hello\""));
378 }
379
380 #[test]
381 fn test_content_part_text() {
382 let part = ContentPart::text("Hello");
383 assert_eq!(part.content_type, ContentType::Text);
384 assert_eq!(part.text, Some("Hello".to_string()));
385 }
386
387 #[test]
388 fn test_content_part_image_url() {
389 let part = ContentPart::image_url("https://example.com/image.png");
390 assert_eq!(part.content_type, ContentType::ImageUrl);
391 assert!(part.image_url.is_some());
392 }
393
394 #[test]
395 fn test_content_part_video_url() {
396 let part = ContentPart::video_url("https://example.com/video.mp4");
397 assert_eq!(part.content_type, ContentType::VideoUrl);
398 assert!(part.video_url.is_some());
399 }
400
401 #[test]
402 fn test_function_call_parse() {
403 let fc = FunctionCall::new("test", r#"{"arg": "value"}"#);
404 let parsed: serde_json::Value = fc.parse_arguments().unwrap();
405 assert_eq!(parsed["arg"], "value");
406 }
407
408 #[test]
409 fn test_multimodal_message() {
410 let content = MessageContent::Parts(vec![
411 ContentPart::text("What's in this image?"),
412 ContentPart::image_url("https://example.com/image.png"),
413 ]);
414 let msg = Message::user(content);
415 assert_eq!(msg.role, Role::User);
416 }
417
418 #[test]
419 fn test_tool_message() {
420 let msg = Message::tool("call_123", "result data");
421 assert_eq!(msg.role, Role::Tool);
422 assert_eq!(msg.tool_call_id, Some("call_123".to_string()));
423 }
424}