Skip to main content

rig_core/providers/minimax/
extension.rs

1//! MiniMax's typed request options and reply extras. One entry serves its
2//! OpenAI-format API
3//! (<https://platform.minimax.io/docs/api-reference/text-openai-api>) and its
4//! Anthropic-format Messages API; each section goes only to its route.
5//!
6//! ```
7//! use rig_core::completion::CompletionRequest;
8//! use rig_core::providers::minimax::extension::MiniMaxOptions;
9//!
10//! let options = MiniMaxOptions::new().reasoning_split(true);
11//! let request = CompletionRequest::new("hi").provider_option(options);
12//! # let _ = request;
13//! ```
14
15use serde::ser::SerializeMap;
16use serde::{Serialize, Serializer};
17use serde_json::Value;
18
19use crate::completion::provider_options::reply_field;
20use crate::completion::{ExtensionOptions, ProviderExtension, ReplyExtras};
21use crate::message::Api;
22use crate::providers::anthropic::extension::{MessagesStop, Nested};
23use crate::providers::anthropic::wire::MESSAGES_API;
24
25/// MiniMax's extension marker.
26#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
27pub struct MiniMaxExt;
28
29impl ProviderExtension for MiniMaxExt {
30    const PROVIDER: &'static str = super::PROVIDER_NAME;
31    type Options = MiniMaxOptions;
32    type Extras = MiniMaxExtras;
33}
34
35/// MiniMax's request options, by route. The field setters here write the
36/// same field as the section setters, which replace every field of their
37/// section set before them, and each names the one route that sends it.
38#[non_exhaustive]
39#[derive(Clone, Debug, Default, PartialEq, Serialize)]
40pub struct MiniMaxOptions {
41    /// Sent on the OpenAI-format API only.
42    #[serde(rename = "openai.chat")]
43    pub chat: MiniMaxChat,
44    /// Sent on the Anthropic-format Messages API only.
45    #[serde(rename = "anthropic.messages")]
46    pub messages: MiniMaxMessages,
47}
48
49impl MiniMaxOptions {
50    /// No option set.
51    pub fn new() -> Self {
52        Self::default()
53    }
54
55    /// Send `chat` on the OpenAI-format API.
56    pub fn chat(mut self, chat: MiniMaxChat) -> Self {
57        self.chat = chat;
58        self
59    }
60
61    /// Send `messages` on the Messages API.
62    pub fn messages(mut self, messages: MiniMaxMessages) -> Self {
63        self.messages = messages;
64        self
65    }
66
67    /// Return the reasoning apart from the content when `split`, on the
68    /// OpenAI-format API only, as [`MiniMaxChat::reasoning_split`].
69    pub fn reasoning_split(mut self, split: bool) -> Self {
70        self.chat = std::mem::take(&mut self.chat).reasoning_split(split);
71        self
72    }
73
74    /// Send `user_id` as `metadata.user_id`, on the Messages API only, as
75    /// [`MiniMaxMessages::metadata_user_id`].
76    pub fn metadata_user_id(mut self, user_id: impl Into<String>) -> Self {
77        self.messages = std::mem::take(&mut self.messages).metadata_user_id(user_id);
78        self
79    }
80}
81
82impl ExtensionOptions for MiniMaxOptions {
83    type Ext = MiniMaxExt;
84}
85
86/// The fields MiniMax's OpenAI-format API takes.
87#[non_exhaustive]
88#[derive(Clone, Debug, Default, PartialEq, Serialize)]
89pub struct MiniMaxChat {
90    /// Whether the reasoning comes back in `reasoning_details` rather than
91    /// in `<think>` tags in the content.
92    #[serde(skip_serializing_if = "Option::is_none")]
93    pub reasoning_split: Option<bool>,
94}
95
96impl MiniMaxChat {
97    /// No option set.
98    pub fn new() -> Self {
99        Self::default()
100    }
101
102    /// Return the reasoning apart from the content when `split`.
103    pub fn reasoning_split(mut self, split: bool) -> Self {
104        self.reasoning_split = Some(split);
105        self
106    }
107}
108
109/// The fields MiniMax's Messages API takes. Each unset field is left out
110/// of the body.
111#[non_exhaustive]
112#[derive(Clone, Debug, Default, PartialEq)]
113pub struct MiniMaxMessages {
114    /// `metadata.user_id`: an opaque id of the end user.
115    pub metadata_user_id: Option<String>,
116}
117
118impl MiniMaxMessages {
119    /// No option set.
120    pub fn new() -> Self {
121        Self::default()
122    }
123
124    /// Send `user_id` as `metadata.user_id`.
125    pub fn metadata_user_id(mut self, user_id: impl Into<String>) -> Self {
126        self.metadata_user_id = Some(user_id.into());
127        self
128    }
129}
130
131impl Serialize for MiniMaxMessages {
132    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
133        let Self { metadata_user_id } = self;
134        let mut map = serializer.serialize_map(None)?;
135        if let Some(user_id) = metadata_user_id {
136            map.serialize_entry("metadata", &Nested("user_id", user_id))?;
137        }
138        map.end()
139    }
140}
141
142/// MiniMax's reply fields. Each is `None` when the reply lacks it.
143#[non_exhaustive]
144#[derive(Clone, Debug, Default, PartialEq)]
145pub struct MiniMaxExtras {
146    /// The first choice's reasoning, split from the content. OpenAI-format
147    /// API only.
148    pub reasoning_details: Option<Vec<Value>>,
149    /// MiniMax's status envelope. OpenAI-format API only.
150    pub base_resp: Option<Value>,
151    /// `stop_reason`, verbatim. Messages route.
152    pub stop_reason: Option<String>,
153    /// `stop_sequence`: the stop sequence the turn ended on. Messages route.
154    pub stop_sequence: Option<String>,
155}
156
157impl ReplyExtras for MiniMaxExtras {
158    fn from_reply(api: &Api, raw: &Value) -> Result<Self, serde_json::Error> {
159        if api.as_str() == MESSAGES_API {
160            let MessagesStop {
161                stop_reason,
162                stop_sequence,
163            } = MessagesStop::read("MiniMax", api, raw)?;
164            return Ok(Self {
165                stop_reason,
166                stop_sequence,
167                ..Self::default()
168            });
169        }
170        if api.as_str() != "openai.chat" {
171            return Ok(Self::default());
172        }
173        Ok(Self {
174            reasoning_details: reply_field(raw, "/choices/0/message/reasoning_details")?,
175            base_resp: reply_field(raw, "/base_resp")?,
176            ..Self::default()
177        })
178    }
179}
180
181#[cfg(test)]
182mod tests;