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 { url, detail: None }),
108            input_audio: None,
109            video_url: None,
110        }
111    }
112
113    /// Create an audio content part from base64 data.
114    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    /// Create a video content part from a URL.
128    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/// Content type.
140#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
141#[serde(rename_all = "snake_case")]
142pub enum ContentType {
143    /// Text content
144    Text,
145    /// Image URL content
146    ImageUrl,
147    /// Input audio content
148    InputAudio,
149    /// Video URL content
150    VideoUrl,
151}
152
153/// Image URL configuration.
154#[derive(Debug, Clone, Serialize, Deserialize)]
155pub struct ImageUrl {
156    /// The image URL (can be a regular URL or data URL)
157    pub url: String,
158    /// Image detail level
159    #[serde(skip_serializing_if = "Option::is_none")]
160    pub detail: Option<ImageDetail>,
161}
162
163/// Image detail level.
164#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
165#[serde(rename_all = "lowercase")]
166pub enum ImageDetail {
167    /// Auto detect
168    Auto,
169    /// Low detail
170    Low,
171    /// High detail
172    High,
173}
174
175/// Audio input format.
176#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
177#[serde(rename_all = "lowercase")]
178pub enum AudioInputFormat {
179    /// WAV format
180    Wav,
181    /// MP3 format
182    Mp3,
183}
184
185/// Input audio configuration.
186#[derive(Debug, Clone, Serialize, Deserialize)]
187pub struct InputAudio {
188    /// Base64 encoded audio data
189    pub data: String,
190    /// Audio format
191    pub format: AudioInputFormat,
192}
193
194/// Video URL configuration.
195#[derive(Debug, Clone, Serialize, Deserialize)]
196pub struct VideoUrl {
197    /// The video URL
198    pub url: String,
199}
200
201/// A message in the conversation.
202#[derive(Debug, Clone, Serialize, Deserialize)]
203pub struct Message {
204    /// The role of the message author
205    pub role: Role,
206    /// The content of the message
207    pub content: MessageContent,
208    /// Optional name for the participant
209    #[serde(skip_serializing_if = "Option::is_none")]
210    pub name: Option<String>,
211    /// Tool calls (for assistant messages)
212    #[serde(skip_serializing_if = "Option::is_none")]
213    pub tool_calls: Option<Vec<ToolCall>>,
214    /// Tool call ID (for tool messages)
215    #[serde(skip_serializing_if = "Option::is_none")]
216    pub tool_call_id: Option<String>,
217    /// Reasoning content (for thinking mode)
218    #[serde(skip_serializing_if = "Option::is_none")]
219    pub reasoning_content: Option<String>,
220    /// Audio data (for assistant messages with audio output)
221    #[serde(skip_serializing_if = "Option::is_none")]
222    pub audio: Option<MessageAudio>,
223}
224
225impl Message {
226    /// Create a new message.
227    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    /// Create a system message.
240    pub fn system(content: impl Into<MessageContent>) -> Self {
241        Self::new(Role::System, content.into())
242    }
243
244    /// Create a developer message.
245    pub fn developer(content: impl Into<MessageContent>) -> Self {
246        Self::new(Role::Developer, content.into())
247    }
248
249    /// Create a user message.
250    pub fn user(content: impl Into<MessageContent>) -> Self {
251        Self::new(Role::User, content.into())
252    }
253
254    /// Create an assistant message.
255    pub fn assistant(content: impl Into<MessageContent>) -> Self {
256        Self::new(Role::Assistant, content.into())
257    }
258
259    /// Create a tool response message.
260    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    /// Set the name.
273    pub fn with_name(mut self, name: impl Into<String>) -> Self {
274        self.name = Some(name.into());
275        self
276    }
277
278    /// Set the tool calls.
279    pub fn with_tool_calls(mut self, tool_calls: Vec<ToolCall>) -> Self {
280        self.tool_calls = Some(tool_calls);
281        self
282    }
283
284    /// Set the reasoning content.
285    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/// Message audio data.
292#[derive(Debug, Clone, Serialize, Deserialize)]
293pub struct MessageAudio {
294    /// Audio ID
295    pub id: Option<String>,
296    /// Base64 encoded audio data
297    pub data: Option<String>,
298    /// Expiration timestamp
299    pub expires_at: Option<i64>,
300    /// Audio transcript
301    pub transcript: Option<String>,
302}
303
304/// A tool call.
305#[derive(Debug, Clone, Serialize, Deserialize)]
306pub struct ToolCall {
307    /// The ID of the tool call
308    pub id: String,
309    /// The type of tool
310    #[serde(rename = "type")]
311    pub tool_type: ToolCallType,
312    /// The function call
313    pub function: FunctionCall,
314}
315
316impl ToolCall {
317    /// Create a new tool call.
318    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/// Tool call type.
328#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
329#[serde(rename_all = "lowercase")]
330pub enum ToolCallType {
331    /// Function tool
332    Function,
333}
334
335/// A function call.
336#[derive(Debug, Clone, Serialize, Deserialize)]
337pub struct FunctionCall {
338    /// The name of the function to call
339    pub name: String,
340    /// The arguments to pass to the function (JSON string)
341    pub arguments: String,
342}
343
344impl FunctionCall {
345    /// Create a new function call.
346    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    /// Parse the arguments as JSON.
354    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}