Skip to main content

mimo_api/types/
message.rs

1//! Message types for the MiMo API.
2
3use serde::{Deserialize, Serialize};
4
5/// Message role.
6#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
7#[serde(rename_all = "lowercase")]
8pub enum Role {
9    /// System message
10    System,
11    /// Developer message
12    Developer,
13    /// User message
14    User,
15    /// Assistant message
16    Assistant,
17    /// Tool message
18    Tool,
19}
20
21/// Message content - can be text or multi-part content.
22#[derive(Debug, Clone, Serialize, Deserialize)]
23#[serde(untagged)]
24pub enum MessageContent {
25    /// Simple text content
26    Text(String),
27    /// Multi-part content (for multi-modal messages)
28    Parts(Vec<ContentPart>),
29}
30
31impl MessageContent {
32    /// Create text content.
33    pub fn text(text: impl Into<String>) -> Self {
34        MessageContent::Text(text.into())
35    }
36
37    /// Create multi-part content.
38    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/// Content part for multi-modal messages.
56#[derive(Debug, Clone, Serialize, Deserialize)]
57pub struct ContentPart {
58    /// Content type
59    #[serde(rename = "type")]
60    pub content_type: ContentType,
61    /// Text content (for text type)
62    #[serde(skip_serializing_if = "Option::is_none")]
63    pub text: Option<String>,
64    /// Image URL (for image type)
65    #[serde(skip_serializing_if = "Option::is_none")]
66    pub image_url: Option<ImageUrl>,
67    /// Input audio (for audio type)
68    #[serde(skip_serializing_if = "Option::is_none")]
69    pub input_audio: Option<InputAudio>,
70    /// Video URL (for video type)
71    #[serde(skip_serializing_if = "Option::is_none")]
72    pub video_url: Option<VideoUrl>,
73}
74
75impl ContentPart {
76    /// Create a text content part.
77    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    /// Create an image content part from a URL.
88    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    /// Create an image content part from base64 data.
102    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    /// Create an audio content part from base64 data.
117    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    /// Create a video content part from a URL.
131    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/// Content type.
143#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
144#[serde(rename_all = "snake_case")]
145pub enum ContentType {
146    /// Text content
147    Text,
148    /// Image URL content
149    ImageUrl,
150    /// Input audio content
151    InputAudio,
152    /// Video URL content
153    VideoUrl,
154}
155
156/// Image URL configuration.
157#[derive(Debug, Clone, Serialize, Deserialize)]
158pub struct ImageUrl {
159    /// The image URL (can be a regular URL or data URL)
160    pub url: String,
161    /// Image detail level
162    #[serde(skip_serializing_if = "Option::is_none")]
163    pub detail: Option<ImageDetail>,
164}
165
166/// Image detail level.
167#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
168#[serde(rename_all = "lowercase")]
169pub enum ImageDetail {
170    /// Auto detect
171    Auto,
172    /// Low detail
173    Low,
174    /// High detail
175    High,
176}
177
178/// Audio input format.
179#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
180#[serde(rename_all = "lowercase")]
181pub enum AudioInputFormat {
182    /// WAV format
183    Wav,
184    /// MP3 format
185    Mp3,
186}
187
188/// Input audio configuration.
189#[derive(Debug, Clone, Serialize, Deserialize)]
190pub struct InputAudio {
191    /// Base64 encoded audio data
192    pub data: String,
193    /// Audio format
194    pub format: AudioInputFormat,
195}
196
197/// Video URL configuration.
198#[derive(Debug, Clone, Serialize, Deserialize)]
199pub struct VideoUrl {
200    /// The video URL
201    pub url: String,
202}
203
204/// A message in the conversation.
205#[derive(Debug, Clone, Serialize, Deserialize)]
206pub struct Message {
207    /// The role of the message author
208    pub role: Role,
209    /// The content of the message
210    pub content: MessageContent,
211    /// Optional name for the participant
212    #[serde(skip_serializing_if = "Option::is_none")]
213    pub name: Option<String>,
214    /// Tool calls (for assistant messages)
215    #[serde(skip_serializing_if = "Option::is_none")]
216    pub tool_calls: Option<Vec<ToolCall>>,
217    /// Tool call ID (for tool messages)
218    #[serde(skip_serializing_if = "Option::is_none")]
219    pub tool_call_id: Option<String>,
220    /// Reasoning content (for thinking mode)
221    #[serde(skip_serializing_if = "Option::is_none")]
222    pub reasoning_content: Option<String>,
223    /// Audio data (for assistant messages with audio output)
224    #[serde(skip_serializing_if = "Option::is_none")]
225    pub audio: Option<MessageAudio>,
226}
227
228impl Message {
229    /// Create a new message.
230    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    /// Create a system message.
243    pub fn system(content: impl Into<MessageContent>) -> Self {
244        Self::new(Role::System, content.into())
245    }
246
247    /// Create a developer message.
248    pub fn developer(content: impl Into<MessageContent>) -> Self {
249        Self::new(Role::Developer, content.into())
250    }
251
252    /// Create a user message.
253    pub fn user(content: impl Into<MessageContent>) -> Self {
254        Self::new(Role::User, content.into())
255    }
256
257    /// Create an assistant message.
258    pub fn assistant(content: impl Into<MessageContent>) -> Self {
259        Self::new(Role::Assistant, content.into())
260    }
261
262    /// Create a tool response message.
263    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    /// Set the name.
276    pub fn with_name(mut self, name: impl Into<String>) -> Self {
277        self.name = Some(name.into());
278        self
279    }
280
281    /// Set the tool calls.
282    pub fn with_tool_calls(mut self, tool_calls: Vec<ToolCall>) -> Self {
283        self.tool_calls = Some(tool_calls);
284        self
285    }
286
287    /// Set the reasoning content.
288    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/// Message audio data.
295#[derive(Debug, Clone, Serialize, Deserialize)]
296pub struct MessageAudio {
297    /// Audio ID
298    pub id: Option<String>,
299    /// Base64 encoded audio data
300    pub data: Option<String>,
301    /// Expiration timestamp
302    pub expires_at: Option<i64>,
303    /// Audio transcript
304    pub transcript: Option<String>,
305}
306
307/// A tool call.
308#[derive(Debug, Clone, Serialize, Deserialize)]
309pub struct ToolCall {
310    /// The ID of the tool call
311    pub id: String,
312    /// The type of tool
313    #[serde(rename = "type")]
314    pub tool_type: ToolCallType,
315    /// The function call
316    pub function: FunctionCall,
317}
318
319impl ToolCall {
320    /// Create a new tool call.
321    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/// Tool call type.
331#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
332#[serde(rename_all = "lowercase")]
333pub enum ToolCallType {
334    /// Function tool
335    Function,
336}
337
338/// A function call.
339#[derive(Debug, Clone, Serialize, Deserialize)]
340pub struct FunctionCall {
341    /// The name of the function to call
342    pub name: String,
343    /// The arguments to pass to the function (JSON string)
344    pub arguments: String,
345}
346
347impl FunctionCall {
348    /// Create a new function call.
349    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    /// Parse the arguments as JSON.
357    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}