1use super::*;
4use serde::{Deserialize, Serialize};
5
6#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
8#[serde(rename_all = "kebab-case")]
9pub enum Model {
10 MiMoV25Pro,
12 MiMoV25,
14 MiMoV25Tts,
16 MiMoV25TtsVoiceDesign,
18 MiMoV25TtsVoiceClone,
20 MiMoV2Pro,
22 MiMoV2Omni,
24 MiMoV2Tts,
26 MiMoV2Flash,
28}
29
30impl Model {
31 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#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
72#[serde(rename_all = "lowercase")]
73pub enum ThinkingType {
74 Enabled,
76 Disabled,
78}
79
80#[derive(Debug, Clone, Serialize, Deserialize)]
82pub struct Thinking {
83 #[serde(rename = "type")]
85 pub thinking_type: ThinkingType,
86}
87
88impl Thinking {
89 pub fn new(thinking_type: ThinkingType) -> Self {
91 Self { thinking_type }
92 }
93
94 pub fn enabled() -> Self {
96 Self::new(ThinkingType::Enabled)
97 }
98
99 pub fn disabled() -> Self {
101 Self::new(ThinkingType::Disabled)
102 }
103}
104
105#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
107#[serde(rename_all = "lowercase")]
108pub enum ToolChoice {
109 Auto,
111}
112
113#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
115#[serde(rename_all = "lowercase")]
116pub enum ResponseFormatType {
117 Text,
119 JsonObject,
121}
122
123#[derive(Debug, Clone, Serialize, Deserialize)]
125pub struct ResponseFormat {
126 #[serde(rename = "type")]
128 pub format_type: ResponseFormatType,
129}
130
131impl ResponseFormat {
132 pub fn new(format_type: ResponseFormatType) -> Self {
134 Self { format_type }
135 }
136
137 pub fn text() -> Self {
139 Self::new(ResponseFormatType::Text)
140 }
141
142 pub fn json_object() -> Self {
144 Self::new(ResponseFormatType::JsonObject)
145 }
146}
147
148#[derive(Debug, Clone, Serialize, Deserialize)]
150#[serde(untagged)]
151pub enum Stop {
152 Single(String),
154 Multiple(Vec<String>),
156}
157
158#[derive(Debug, Clone, Serialize, Deserialize)]
160pub struct ChatRequest {
161 pub model: String,
163 pub messages: Vec<Message>,
165 #[serde(skip_serializing_if = "Option::is_none")]
167 pub audio: Option<Audio>,
168 #[serde(skip_serializing_if = "Option::is_none")]
170 pub frequency_penalty: Option<f32>,
171 #[serde(skip_serializing_if = "Option::is_none")]
173 pub max_completion_tokens: Option<u32>,
174 #[serde(skip_serializing_if = "Option::is_none")]
176 pub presence_penalty: Option<f32>,
177 #[serde(skip_serializing_if = "Option::is_none")]
179 pub response_format: Option<ResponseFormat>,
180 #[serde(skip_serializing_if = "Option::is_none")]
182 pub stop: Option<Stop>,
183 #[serde(skip_serializing_if = "Option::is_none")]
185 pub stream: Option<bool>,
186 #[serde(skip_serializing_if = "Option::is_none")]
188 pub thinking: Option<Thinking>,
189 #[serde(skip_serializing_if = "Option::is_none")]
191 pub temperature: Option<f32>,
192 #[serde(skip_serializing_if = "Option::is_none")]
194 pub tool_choice: Option<ToolChoice>,
195 #[serde(skip_serializing_if = "Option::is_none")]
197 pub tools: Option<Vec<Tool>>,
198 #[serde(skip_serializing_if = "Option::is_none")]
200 pub top_p: Option<f32>,
201 #[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 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 pub fn flash() -> Self {
256 Self::new(Model::MiMoV2Flash.as_str())
257 }
258
259 pub fn pro() -> Self {
261 Self::new(Model::MiMoV2Pro.as_str())
262 }
263
264 pub fn v25_pro() -> Self {
266 Self::new(Model::MiMoV25Pro.as_str())
267 }
268
269 pub fn v25() -> Self {
271 Self::new(Model::MiMoV25.as_str())
272 }
273
274 pub fn omni() -> Self {
276 Self::new(Model::MiMoV2Omni.as_str())
277 }
278
279 pub fn v25_tts() -> Self {
281 Self::new(Model::MiMoV25Tts.as_str())
282 }
283
284 pub fn v25_tts_voicedesign() -> Self {
286 Self::new(Model::MiMoV25TtsVoiceDesign.as_str())
287 }
288
289 pub fn v25_tts_voiceclone() -> Self {
291 Self::new(Model::MiMoV25TtsVoiceClone.as_str())
292 }
293
294 pub fn tts() -> Self {
296 Self::new(Model::MiMoV2Tts.as_str())
297 }
298
299 pub fn model(mut self, model: impl Into<String>) -> Self {
301 self.model = model.into();
302 self
303 }
304
305 pub fn message(mut self, message: Message) -> Self {
307 self.messages.push(message);
308 self
309 }
310
311 pub fn messages(mut self, messages: Vec<Message>) -> Self {
317 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 pub fn system(mut self, content: impl Into<String>) -> Self {
344 let sys_msg = Message::system(MessageContent::Text(content.into()));
345 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 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 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 pub fn audio(mut self, audio: Audio) -> Self {
371 self.audio = Some(audio);
372 self
373 }
374
375 pub fn frequency_penalty(mut self, penalty: f32) -> Self {
377 self.frequency_penalty = Some(penalty);
378 self
379 }
380
381 pub fn max_completion_tokens(mut self, tokens: u32) -> Self {
383 self.max_completion_tokens = Some(tokens);
384 self
385 }
386
387 pub fn presence_penalty(mut self, penalty: f32) -> Self {
389 self.presence_penalty = Some(penalty);
390 self
391 }
392
393 pub fn response_format(mut self, format: ResponseFormat) -> Self {
395 self.response_format = Some(format);
396 self
397 }
398
399 pub fn stop(mut self, stop: Stop) -> Self {
401 self.stop = Some(stop);
402 self
403 }
404
405 pub fn stream(mut self, stream: bool) -> Self {
407 self.stream = Some(stream);
408 self
409 }
410
411 pub fn thinking(mut self, thinking: Thinking) -> Self {
413 self.thinking = Some(thinking);
414 self
415 }
416
417 pub fn enable_thinking(mut self) -> Self {
419 self.thinking = Some(Thinking::enabled());
420 self
421 }
422
423 pub fn disable_thinking(mut self) -> Self {
425 self.thinking = Some(Thinking::disabled());
426 self
427 }
428
429 pub fn temperature(mut self, temperature: f32) -> Self {
431 self.temperature = Some(temperature);
432 self
433 }
434
435 pub fn tool_choice(mut self, choice: ToolChoice) -> Self {
437 self.tool_choice = Some(choice);
438 self
439 }
440
441 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 pub fn tools(mut self, tools: Vec<Tool>) -> Self {
452 self.tools = Some(tools);
453 self
454 }
455
456 pub fn top_p(mut self, top_p: f32) -> Self {
458 self.top_p = Some(top_p);
459 self
460 }
461
462 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 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 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 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 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 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}