Skip to main content

mimo_api/types/
chat.rs

1//! Chat request types.
2
3use super::*;
4use serde::{Deserialize, Serialize};
5
6/// Available MiMo models.
7#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
8#[serde(rename_all = "kebab-case")]
9pub enum Model {
10    /// MiMo V2.5 Pro - Latest flagship model
11    MiMoV25Pro,
12    /// MiMo V2.5 - Balanced performance model
13    MiMoV25,
14    /// MiMo V2.5 TTS - Text-to-speech with preset voices
15    MiMoV25Tts,
16    /// MiMo V2.5 TTS VoiceDesign - Voice design via text description
17    MiMoV25TtsVoiceDesign,
18    /// MiMo V2.5 TTS VoiceClone - Voice cloning via audio sample
19    MiMoV25TtsVoiceClone,
20    /// MiMo V2 Pro - Agent-oriented flagship model
21    MiMoV2Pro,
22    /// MiMo V2 Omni - Multi-modal agent model
23    MiMoV2Omni,
24    /// MiMo V2 TTS - Text-to-speech model (legacy)
25    MiMoV2Tts,
26    /// MiMo V2 Flash - Fast and efficient model
27    MiMoV2Flash,
28}
29
30impl Model {
31    /// Get the model identifier string.
32    pub fn as_str(&self) -> &'static str {
33        match self {
34            Model::MiMoV25Pro => "mimo-v2.5-pro",
35            Model::MiMoV25 => "mimo-v2.5",
36            Model::MiMoV25Tts => "mimo-v2.5-tts",
37            Model::MiMoV25TtsVoiceDesign => "mimo-v2.5-tts-voicedesign",
38            Model::MiMoV25TtsVoiceClone => "mimo-v2.5-tts-voiceclone",
39            Model::MiMoV2Pro => "mimo-v2-pro",
40            Model::MiMoV2Omni => "mimo-v2-omni",
41            Model::MiMoV2Tts => "mimo-v2-tts",
42            Model::MiMoV2Flash => "mimo-v2-flash",
43        }
44    }
45}
46
47impl std::fmt::Display for Model {
48    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
49        write!(f, "{}", self.as_str())
50    }
51}
52
53impl From<&str> for Model {
54    fn from(s: &str) -> Self {
55        match s {
56            "mimo-v2.5-pro" => Model::MiMoV25Pro,
57            "mimo-v2.5" => Model::MiMoV25,
58            "mimo-v2.5-tts" => Model::MiMoV25Tts,
59            "mimo-v2.5-tts-voicedesign" => Model::MiMoV25TtsVoiceDesign,
60            "mimo-v2.5-tts-voiceclone" => Model::MiMoV25TtsVoiceClone,
61            "mimo-v2-pro" => Model::MiMoV2Pro,
62            "mimo-v2-omni" => Model::MiMoV2Omni,
63            "mimo-v2-tts" => Model::MiMoV2Tts,
64            "mimo-v2-flash" => Model::MiMoV2Flash,
65            _ => Model::MiMoV2Flash,
66        }
67    }
68}
69
70/// Thinking mode configuration.
71#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
72#[serde(rename_all = "lowercase")]
73pub enum ThinkingType {
74    /// Enable thinking mode (deep reasoning)
75    Enabled,
76    /// Disable thinking mode
77    Disabled,
78}
79
80/// Thinking configuration.
81#[derive(Debug, Clone, Serialize, Deserialize)]
82pub struct Thinking {
83    /// Whether to enable thinking mode
84    #[serde(rename = "type")]
85    pub thinking_type: ThinkingType,
86}
87
88impl Thinking {
89    /// Create a new thinking configuration.
90    pub fn new(thinking_type: ThinkingType) -> Self {
91        Self { thinking_type }
92    }
93
94    /// Enable thinking mode.
95    pub fn enabled() -> Self {
96        Self::new(ThinkingType::Enabled)
97    }
98
99    /// Disable thinking mode.
100    pub fn disabled() -> Self {
101        Self::new(ThinkingType::Disabled)
102    }
103}
104
105/// Tool choice configuration.
106#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
107#[serde(rename_all = "lowercase")]
108pub enum ToolChoice {
109    /// Let the model decide whether to call tools
110    Auto,
111}
112
113/// Response format type.
114#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
115#[serde(rename_all = "lowercase")]
116pub enum ResponseFormatType {
117    /// Plain text response
118    Text,
119    /// JSON object response
120    JsonObject,
121}
122
123/// Response format configuration.
124#[derive(Debug, Clone, Serialize, Deserialize)]
125pub struct ResponseFormat {
126    /// The format type
127    #[serde(rename = "type")]
128    pub format_type: ResponseFormatType,
129}
130
131impl ResponseFormat {
132    /// Create a new response format.
133    pub fn new(format_type: ResponseFormatType) -> Self {
134        Self { format_type }
135    }
136
137    /// Create a text response format.
138    pub fn text() -> Self {
139        Self::new(ResponseFormatType::Text)
140    }
141
142    /// Create a JSON object response format.
143    pub fn json_object() -> Self {
144        Self::new(ResponseFormatType::JsonObject)
145    }
146}
147
148/// Stop sequence configuration.
149#[derive(Debug, Clone, Serialize, Deserialize)]
150#[serde(untagged)]
151pub enum Stop {
152    /// Single stop sequence
153    Single(String),
154    /// Multiple stop sequences (max 4)
155    Multiple(Vec<String>),
156}
157
158/// Chat completion request.
159#[derive(Debug, Clone, Serialize, Deserialize)]
160pub struct ChatRequest {
161    /// The model to use for generation
162    pub model: String,
163    /// List of messages in the conversation
164    pub messages: Vec<Message>,
165    /// Audio output parameters (for TTS)
166    #[serde(skip_serializing_if = "Option::is_none")]
167    pub audio: Option<Audio>,
168    /// Frequency penalty (-2.0 to 2.0)
169    #[serde(skip_serializing_if = "Option::is_none")]
170    pub frequency_penalty: Option<f32>,
171    /// Maximum completion tokens
172    #[serde(skip_serializing_if = "Option::is_none")]
173    pub max_completion_tokens: Option<u32>,
174    /// Presence penalty (-2.0 to 2.0)
175    #[serde(skip_serializing_if = "Option::is_none")]
176    pub presence_penalty: Option<f32>,
177    /// Response format
178    #[serde(skip_serializing_if = "Option::is_none")]
179    pub response_format: Option<ResponseFormat>,
180    /// Stop sequences
181    #[serde(skip_serializing_if = "Option::is_none")]
182    pub stop: Option<Stop>,
183    /// Enable streaming response
184    #[serde(skip_serializing_if = "Option::is_none")]
185    pub stream: Option<bool>,
186    /// Thinking mode configuration
187    #[serde(skip_serializing_if = "Option::is_none")]
188    pub thinking: Option<Thinking>,
189    /// Sampling temperature (0 to 1.5)
190    #[serde(skip_serializing_if = "Option::is_none")]
191    pub temperature: Option<f32>,
192    /// Tool choice
193    #[serde(skip_serializing_if = "Option::is_none")]
194    pub tool_choice: Option<ToolChoice>,
195    /// List of tools
196    #[serde(skip_serializing_if = "Option::is_none")]
197    pub tools: Option<Vec<Tool>>,
198    /// Top-p sampling (0.01 to 1.0)
199    #[serde(skip_serializing_if = "Option::is_none")]
200    pub top_p: Option<f32>,
201    /// Enable web search capability.
202    ///
203    /// **Note:** You must first enable the "联网服务插件" (Web Search Plugin)
204    /// in the MiMo console before using this feature. If the plugin is not
205    /// enabled, setting this to `true` will result in a 400 error.
206    #[serde(skip_serializing_if = "Option::is_none", rename = "webSearchEnabled")]
207    pub web_search_enabled: Option<bool>,
208}
209
210impl Default for ChatRequest {
211    fn default() -> Self {
212        Self {
213            model: "mimo-v2-flash".to_string(),
214            messages: Vec::new(),
215            audio: None,
216            frequency_penalty: None,
217            max_completion_tokens: None,
218            presence_penalty: None,
219            response_format: None,
220            stop: None,
221            stream: None,
222            thinking: None,
223            temperature: None,
224            tool_choice: None,
225            tools: None,
226            top_p: None,
227            web_search_enabled: None,
228        }
229    }
230}
231
232impl ChatRequest {
233    /// Create a new chat request with the specified model.
234    pub fn new(model: impl Into<String>) -> Self {
235        Self {
236            model: model.into(),
237            messages: Vec::new(),
238            audio: None,
239            frequency_penalty: None,
240            max_completion_tokens: None,
241            presence_penalty: None,
242            response_format: None,
243            stop: None,
244            stream: None,
245            thinking: None,
246            temperature: None,
247            tool_choice: None,
248            tools: None,
249            top_p: None,
250            web_search_enabled: None,
251        }
252    }
253
254    /// Create a chat request with the MiMo V2 Flash model.
255    pub fn flash() -> Self {
256        Self::new(Model::MiMoV2Flash.as_str())
257    }
258
259    /// Create a chat request with the MiMo V2 Pro model.
260    pub fn pro() -> Self {
261        Self::new(Model::MiMoV2Pro.as_str())
262    }
263
264    /// Create a chat request with the MiMo V2.5 Pro model.
265    pub fn v25_pro() -> Self {
266        Self::new(Model::MiMoV25Pro.as_str())
267    }
268
269    /// Create a chat request with the MiMo V2.5 model.
270    pub fn v25() -> Self {
271        Self::new(Model::MiMoV25.as_str())
272    }
273
274    /// Create a chat request with the MiMo V2 Omni model.
275    pub fn omni() -> Self {
276        Self::new(Model::MiMoV2Omni.as_str())
277    }
278
279    /// Create a chat request with the MiMo V2.5 TTS model (preset voices).
280    pub fn v25_tts() -> Self {
281        Self::new(Model::MiMoV25Tts.as_str())
282    }
283
284    /// Create a chat request with the MiMo V2.5 TTS VoiceDesign model.
285    pub fn v25_tts_voicedesign() -> Self {
286        Self::new(Model::MiMoV25TtsVoiceDesign.as_str())
287    }
288
289    /// Create a chat request with the MiMo V2.5 TTS VoiceClone model.
290    pub fn v25_tts_voiceclone() -> Self {
291        Self::new(Model::MiMoV25TtsVoiceClone.as_str())
292    }
293
294    /// Create a chat request with the MiMo V2 TTS model (legacy).
295    pub fn tts() -> Self {
296        Self::new(Model::MiMoV2Tts.as_str())
297    }
298
299    /// Set the model.
300    pub fn model(mut self, model: impl Into<String>) -> Self {
301        self.model = model.into();
302        self
303    }
304
305    /// Add a message to the conversation.
306    pub fn message(mut self, message: Message) -> Self {
307        self.messages.push(message);
308        self
309    }
310
311    /// Add multiple messages to the conversation.
312    ///
313    /// If a system message was previously set via [`system()`] and the new
314    /// messages do not already contain a system message, the existing system
315    /// message is preserved and placed at the beginning.
316    pub fn messages(mut self, messages: Vec<Message>) -> Self {
317        // If the new messages already start with a system message,
318        // use them as-is. Otherwise preserve any existing system message.
319        let new_has_system = messages
320            .first()
321            .is_some_and(|m| m.role == super::message::Role::System);
322
323        if new_has_system {
324            self.messages = messages;
325        } else {
326            let existing_system = self.messages
327                .first()
328                .filter(|m| m.role == super::message::Role::System)
329                .cloned();
330            self.messages = messages;
331            if let Some(sys_msg) = existing_system {
332                self.messages.insert(0, sys_msg);
333            }
334        }
335        self
336    }
337
338    /// Add a system message.
339    ///
340    /// The system message is always placed at the beginning of the message
341    /// list (index 0), regardless of whether this method is called before
342    /// or after other messages are added.
343    pub fn system(mut self, content: impl Into<String>) -> Self {
344        let sys_msg = Message::system(MessageContent::Text(content.into()));
345        // If there's already a system message at index 0, replace it;
346        // otherwise insert at the beginning
347        if self.messages.first().is_some_and(|m| m.role == super::message::Role::System) {
348            self.messages[0] = sys_msg;
349        } else {
350            self.messages.insert(0, sys_msg);
351        }
352        self
353    }
354
355    /// Add a user message.
356    pub fn user(mut self, content: impl Into<String>) -> Self {
357        self.messages
358            .push(Message::user(MessageContent::Text(content.into())));
359        self
360    }
361
362    /// Add an assistant message.
363    pub fn assistant(mut self, content: impl Into<String>) -> Self {
364        self.messages
365            .push(Message::assistant(MessageContent::Text(content.into())));
366        self
367    }
368
369    /// Set audio output parameters (for TTS).
370    pub fn audio(mut self, audio: Audio) -> Self {
371        self.audio = Some(audio);
372        self
373    }
374
375    /// Set the frequency penalty.
376    pub fn frequency_penalty(mut self, penalty: f32) -> Self {
377        self.frequency_penalty = Some(penalty);
378        self
379    }
380
381    /// Set the maximum completion tokens.
382    pub fn max_completion_tokens(mut self, tokens: u32) -> Self {
383        self.max_completion_tokens = Some(tokens);
384        self
385    }
386
387    /// Set the presence penalty.
388    pub fn presence_penalty(mut self, penalty: f32) -> Self {
389        self.presence_penalty = Some(penalty);
390        self
391    }
392
393    /// Set the response format.
394    pub fn response_format(mut self, format: ResponseFormat) -> Self {
395        self.response_format = Some(format);
396        self
397    }
398
399    /// Set the stop sequences.
400    pub fn stop(mut self, stop: Stop) -> Self {
401        self.stop = Some(stop);
402        self
403    }
404
405    /// Enable or disable streaming.
406    pub fn stream(mut self, stream: bool) -> Self {
407        self.stream = Some(stream);
408        self
409    }
410
411    /// Set the thinking mode.
412    pub fn thinking(mut self, thinking: Thinking) -> Self {
413        self.thinking = Some(thinking);
414        self
415    }
416
417    /// Enable thinking mode.
418    pub fn enable_thinking(mut self) -> Self {
419        self.thinking = Some(Thinking::enabled());
420        self
421    }
422
423    /// Disable thinking mode.
424    pub fn disable_thinking(mut self) -> Self {
425        self.thinking = Some(Thinking::disabled());
426        self
427    }
428
429    /// Set the temperature.
430    pub fn temperature(mut self, temperature: f32) -> Self {
431        self.temperature = Some(temperature);
432        self
433    }
434
435    /// Set the tool choice.
436    pub fn tool_choice(mut self, choice: ToolChoice) -> Self {
437        self.tool_choice = Some(choice);
438        self
439    }
440
441    /// Add a tool.
442    pub fn tool(mut self, tool: Tool) -> Self {
443        if self.tools.is_none() {
444            self.tools = Some(Vec::new());
445        }
446        self.tools.as_mut().unwrap().push(tool);
447        self
448    }
449
450    /// Set the tools.
451    pub fn tools(mut self, tools: Vec<Tool>) -> Self {
452        self.tools = Some(tools);
453        self
454    }
455
456    /// Set the top-p.
457    pub fn top_p(mut self, top_p: f32) -> Self {
458        self.top_p = Some(top_p);
459        self
460    }
461
462    /// Enable or disable web search.
463    ///
464    /// **Note:** You must first enable the "联网服务插件" (Web Search Plugin)
465    /// in the MiMo console before using this feature. If the plugin is not
466    /// enabled, setting this to `true` will result in a 400 error.
467    pub fn web_search_enabled(mut self, enabled: bool) -> Self {
468        self.web_search_enabled = Some(enabled);
469        self
470    }
471}
472
473#[cfg(test)]
474mod tests {
475    use super::*;
476
477    #[test]
478    fn test_model_as_str() {
479        assert_eq!(Model::MiMoV25Pro.as_str(), "mimo-v2.5-pro");
480        assert_eq!(Model::MiMoV25.as_str(), "mimo-v2.5");
481        assert_eq!(Model::MiMoV25Tts.as_str(), "mimo-v2.5-tts");
482        assert_eq!(
483            Model::MiMoV25TtsVoiceDesign.as_str(),
484            "mimo-v2.5-tts-voicedesign"
485        );
486        assert_eq!(
487            Model::MiMoV25TtsVoiceClone.as_str(),
488            "mimo-v2.5-tts-voiceclone"
489        );
490        assert_eq!(Model::MiMoV2Pro.as_str(), "mimo-v2-pro");
491        assert_eq!(Model::MiMoV2Omni.as_str(), "mimo-v2-omni");
492        assert_eq!(Model::MiMoV2Tts.as_str(), "mimo-v2-tts");
493        assert_eq!(Model::MiMoV2Flash.as_str(), "mimo-v2-flash");
494    }
495
496    #[test]
497    fn test_model_from_str() {
498        assert_eq!(Model::from("mimo-v2.5-pro"), Model::MiMoV25Pro);
499        assert_eq!(Model::from("mimo-v2.5-tts"), Model::MiMoV25Tts);
500        assert_eq!(Model::from("mimo-v2-pro"), Model::MiMoV2Pro);
501        assert_eq!(Model::from("mimo-v2-flash"), Model::MiMoV2Flash);
502        assert_eq!(Model::from("unknown"), Model::MiMoV2Flash);
503    }
504
505    #[test]
506    fn test_model_display() {
507        assert_eq!(format!("{}", Model::MiMoV25Pro), "mimo-v2.5-pro");
508    }
509
510    #[test]
511    fn test_thinking() {
512        let enabled = Thinking::enabled();
513        assert_eq!(enabled.thinking_type, ThinkingType::Enabled);
514
515        let disabled = Thinking::disabled();
516        assert_eq!(disabled.thinking_type, ThinkingType::Disabled);
517    }
518
519    #[test]
520    fn test_response_format() {
521        let text = ResponseFormat::text();
522        assert_eq!(text.format_type, ResponseFormatType::Text);
523
524        let json = ResponseFormat::json_object();
525        assert_eq!(json.format_type, ResponseFormatType::JsonObject);
526    }
527
528    #[test]
529    fn test_chat_request_builder() {
530        let request = ChatRequest::flash()
531            .system("You are a helpful assistant.")
532            .user("Hello!")
533            .temperature(0.7)
534            .max_completion_tokens(1024);
535
536        assert_eq!(request.model, "mimo-v2-flash");
537        assert_eq!(request.messages.len(), 2);
538        assert_eq!(request.temperature, Some(0.7));
539        assert_eq!(request.max_completion_tokens, Some(1024));
540    }
541
542    #[test]
543    fn test_system_then_messages() {
544        // system() first, then messages() — system prompt should be preserved at index 0
545        let messages = vec![
546            Message::user(MessageContent::Text("Hello".into())),
547            Message::assistant(MessageContent::Text("Hi".into())),
548        ];
549        let request = ChatRequest::flash()
550            .system("You are a helpful assistant.")
551            .messages(messages);
552
553        assert_eq!(request.messages.len(), 3);
554        assert_eq!(request.messages[0].role, Role::System);
555        assert_eq!(request.messages[1].role, Role::User);
556        assert_eq!(request.messages[2].role, Role::Assistant);
557    }
558
559    #[test]
560    fn test_messages_then_system() {
561        // messages() first, then system() — system prompt should be at index 0
562        let messages = vec![
563            Message::user(MessageContent::Text("Hello".into())),
564            Message::assistant(MessageContent::Text("Hi".into())),
565        ];
566        let request = ChatRequest::flash()
567            .messages(messages)
568            .system("You are a helpful assistant.");
569
570        assert_eq!(request.messages.len(), 3);
571        assert_eq!(request.messages[0].role, Role::System);
572        assert_eq!(request.messages[1].role, Role::User);
573        assert_eq!(request.messages[2].role, Role::Assistant);
574    }
575
576    #[test]
577    fn test_system_replaces_existing_system() {
578        // Calling system() twice should replace the first system message
579        let request = ChatRequest::flash()
580            .system("Old prompt")
581            .system("New prompt")
582            .user("Hello");
583
584        assert_eq!(request.messages.len(), 2);
585        assert_eq!(request.messages[0].role, Role::System);
586        assert_eq!(request.messages[1].role, Role::User);
587    }
588
589    #[test]
590    fn test_messages_preserves_system() {
591        // Calling system() first, then messages() with a vec that does NOT
592        // have its own system — the old system is preserved at index 0.
593        let messages = vec![
594            Message::user(MessageContent::Text("Question".into())),
595        ];
596        let request = ChatRequest::flash()
597            .system("Old system.")
598            .messages(messages);
599
600        assert_eq!(request.messages.len(), 2);
601        assert_eq!(request.messages[0].role, Role::System);
602        assert_eq!(request.messages[1].role, Role::User);
603    }
604
605    #[test]
606    fn test_messages_with_own_system_replaces() {
607        // Calling system() first, then messages() with a vec that already
608        // has a system message — the new vec replaces the old system.
609        let messages = vec![
610            Message::system(MessageContent::Text("New system.".into())),
611            Message::user(MessageContent::Text("Question".into())),
612        ];
613        let request = ChatRequest::flash()
614            .system("Old system.")
615            .messages(messages);
616
617        assert_eq!(request.messages.len(), 2);
618        assert_eq!(request.messages[0].role, Role::System);
619        assert_eq!(request.messages[1].role, Role::User);
620    }
621
622    #[test]
623    fn test_chat_request_serialization() {
624        let request = ChatRequest::new("mimo-v2-flash")
625            .user("Hello!")
626            .temperature(0.5);
627
628        let json = serde_json::to_string(&request).unwrap();
629        assert!(json.contains("\"model\":\"mimo-v2-flash\""));
630        assert!(json.contains("\"temperature\":0.5"));
631    }
632}