Skip to main content

rig_core/providers/
xiaomimimo.rs

1//! Xiaomi MiMo API clients and Rig integrations.
2//!
3//! Xiaomi exposes both OpenAI-compatible and Anthropic-compatible chat APIs
4//! under a single global host.
5//!
6//! # OpenAI-compatible example
7//! ```no_run
8//! use rig_core::client::CompletionClient;
9//! use rig_core::providers::xiaomimimo;
10//!
11//! let client = xiaomimimo::Client::new("YOUR_API_KEY").expect("Failed to build client");
12//! let model = client.completion_model(xiaomimimo::MIMO_V2_5_PRO);
13//! ```
14//!
15//! # Anthropic-compatible example
16//! ```no_run
17//! use rig_core::client::CompletionClient;
18//! use rig_core::providers::xiaomimimo;
19//!
20//! let client = xiaomimimo::AnthropicClient::new("YOUR_API_KEY").expect("Failed to build client");
21//! let model = client.completion_model(xiaomimimo::MIMO_V2_5_PRO);
22//! ```
23
24use crate::client::{
25    self, BearerAuth, Capabilities, Capable, DebugExt, ModelLister, Nothing, Provider,
26    ProviderBuilder, ProviderClient,
27};
28use crate::http_client::{self, HttpClientExt};
29use crate::model::{Model, ModelList, ModelListingError};
30use crate::providers::anthropic::client::{
31    AnthropicBuilder as AnthropicCompatBuilder, AnthropicKey, finish_anthropic_builder,
32};
33use crate::wasm_compat::{WasmCompatSend, WasmCompatSync};
34
35/// OpenAI-compatible base URL.
36pub const API_BASE_URL: &str = "https://api.xiaomimimo.com/v1";
37/// Anthropic-compatible base URL.
38pub const ANTHROPIC_API_BASE_URL: &str = "https://api.xiaomimimo.com/anthropic/v1";
39
40/// `mimo-v2-flash`
41pub const MIMO_V2_FLASH: &str = "mimo-v2-flash";
42/// `mimo-v2-omni`
43pub const MIMO_V2_OMNI: &str = "mimo-v2-omni";
44/// `mimo-v2-pro`
45pub const MIMO_V2_PRO: &str = "mimo-v2-pro";
46/// `mimo-v2.5`
47pub const MIMO_V2_5: &str = "mimo-v2.5";
48/// `mimo-v2.5-pro`
49pub const MIMO_V2_5_PRO: &str = "mimo-v2.5-pro";
50
51#[derive(Debug, Default, Clone, Copy)]
52pub struct XiaomiMimoExt;
53
54#[derive(Debug, Default, Clone, Copy)]
55pub struct XiaomiMimoBuilder;
56
57#[derive(Debug, Default, Clone)]
58pub struct XiaomiMimoAnthropicBuilder {
59    anthropic: AnthropicCompatBuilder,
60}
61
62#[derive(Debug, Default, Clone, Copy)]
63pub struct XiaomiMimoAnthropicExt;
64
65type XiaomiMimoApiKey = BearerAuth;
66
67pub type Client<H = reqwest::Client> = client::Client<XiaomiMimoExt, H>;
68pub type ClientBuilder<H = crate::markers::Missing> =
69    client::ClientBuilder<XiaomiMimoBuilder, XiaomiMimoApiKey, H>;
70
71pub type AnthropicClient<H = reqwest::Client> = client::Client<XiaomiMimoAnthropicExt, H>;
72pub type AnthropicClientBuilder<H = crate::markers::Missing> =
73    client::ClientBuilder<XiaomiMimoAnthropicBuilder, AnthropicKey, H>;
74
75impl Provider for XiaomiMimoExt {
76    type Builder = XiaomiMimoBuilder;
77
78    const VERIFY_PATH: &'static str = "/models";
79}
80
81impl Provider for XiaomiMimoAnthropicExt {
82    type Builder = XiaomiMimoAnthropicBuilder;
83
84    const VERIFY_PATH: &'static str = "/v1/models";
85}
86
87impl<H> Capabilities<H> for XiaomiMimoExt {
88    type Completion = Capable<super::openai::completion::GenericCompletionModel<XiaomiMimoExt, H>>;
89    type Embeddings = Nothing;
90    type Transcription = Nothing;
91    type ModelListing = Capable<XiaomiMimoModelLister<H>>;
92    #[cfg(feature = "image")]
93    type ImageGeneration = Nothing;
94    #[cfg(feature = "audio")]
95    type AudioGeneration = Nothing;
96    type Rerank = Nothing;
97}
98
99impl<H> Capabilities<H> for XiaomiMimoAnthropicExt {
100    type Completion =
101        Capable<super::anthropic::completion::GenericCompletionModel<XiaomiMimoAnthropicExt, H>>;
102    type Embeddings = Nothing;
103    type Transcription = Nothing;
104    type ModelListing = Nothing;
105    #[cfg(feature = "image")]
106    type ImageGeneration = Nothing;
107    #[cfg(feature = "audio")]
108    type AudioGeneration = Nothing;
109    type Rerank = Nothing;
110}
111
112impl DebugExt for XiaomiMimoExt {}
113impl DebugExt for XiaomiMimoAnthropicExt {}
114
115impl ProviderBuilder for XiaomiMimoBuilder {
116    type Extension<H>
117        = XiaomiMimoExt
118    where
119        H: HttpClientExt;
120    type ApiKey = XiaomiMimoApiKey;
121
122    const BASE_URL: &'static str = API_BASE_URL;
123
124    fn build<H>(
125        _builder: &client::ClientBuilder<Self, Self::ApiKey, H>,
126    ) -> http_client::Result<Self::Extension<H>>
127    where
128        H: HttpClientExt,
129    {
130        Ok(XiaomiMimoExt)
131    }
132}
133
134impl ProviderBuilder for XiaomiMimoAnthropicBuilder {
135    type Extension<H>
136        = XiaomiMimoAnthropicExt
137    where
138        H: HttpClientExt;
139    type ApiKey = AnthropicKey;
140
141    const BASE_URL: &'static str = ANTHROPIC_API_BASE_URL;
142
143    fn build<H>(
144        _builder: &client::ClientBuilder<Self, Self::ApiKey, H>,
145    ) -> http_client::Result<Self::Extension<H>>
146    where
147        H: HttpClientExt,
148    {
149        Ok(XiaomiMimoAnthropicExt)
150    }
151
152    fn finish<H>(
153        &self,
154        builder: client::ClientBuilder<Self, AnthropicKey, H>,
155    ) -> http_client::Result<client::ClientBuilder<Self, AnthropicKey, H>> {
156        finish_anthropic_builder(&self.anthropic, builder)
157    }
158}
159
160impl super::anthropic::completion::AnthropicCompatibleProvider for XiaomiMimoAnthropicExt {
161    const PROVIDER_NAME: &'static str = "xiaomimimo";
162
163    fn default_max_tokens(_model: &str) -> Option<u64> {
164        Some(4096)
165    }
166}
167
168impl ProviderClient for Client {
169    type Input = XiaomiMimoApiKey;
170    type Error = crate::client::ProviderClientError;
171
172    fn from_env() -> Result<Self, Self::Error> {
173        let api_key = crate::client::required_env_var("XIAOMI_MIMO_API_KEY")?;
174        let mut builder = Self::builder().api_key(api_key);
175
176        if let Some(base_url) = crate::client::optional_env_var("XIAOMI_MIMO_API_BASE")? {
177            builder = builder.base_url(base_url);
178        }
179
180        builder.build().map_err(Into::into)
181    }
182
183    fn from_val(input: Self::Input) -> Result<Self, Self::Error> {
184        Self::new(input).map_err(Into::into)
185    }
186}
187
188impl ProviderClient for AnthropicClient {
189    type Input = String;
190    type Error = crate::client::ProviderClientError;
191
192    fn from_env() -> Result<Self, Self::Error> {
193        let api_key = crate::client::required_env_var("XIAOMI_MIMO_API_KEY")?;
194        let mut builder = Self::builder().api_key(api_key);
195
196        if let Some(base_url) =
197            anthropic_base_override("XIAOMI_MIMO_ANTHROPIC_API_BASE", "XIAOMI_MIMO_API_BASE")?
198        {
199            builder = builder.base_url(base_url);
200        }
201
202        builder.build().map_err(Into::into)
203    }
204
205    fn from_val(input: Self::Input) -> Result<Self, Self::Error> {
206        Self::builder().api_key(input).build().map_err(Into::into)
207    }
208}
209
210fn anthropic_base_override(
211    primary_env: &'static str,
212    fallback_env: &'static str,
213) -> crate::client::ProviderClientResult<Option<String>> {
214    let primary = crate::client::optional_env_var(primary_env)?;
215    let fallback = crate::client::optional_env_var(fallback_env)?;
216
217    Ok(resolve_anthropic_base_override(
218        primary.as_deref(),
219        fallback.as_deref(),
220    ))
221}
222
223fn resolve_anthropic_base_override(
224    primary: Option<&str>,
225    fallback: Option<&str>,
226) -> Option<String> {
227    primary
228        .map(str::to_owned)
229        .or_else(|| fallback.and_then(normalize_anthropic_base_url))
230}
231
232fn normalize_anthropic_base_url(base_url: &str) -> Option<String> {
233    if base_url.contains("/anthropic") {
234        return Some(base_url.to_owned());
235    }
236
237    if base_url.trim_end_matches('/') == API_BASE_URL {
238        return Some(ANTHROPIC_API_BASE_URL.to_owned());
239    }
240
241    let mut url = url::Url::parse(base_url).ok()?;
242    if !matches!(url.path(), "/v1" | "/v1/") {
243        return None;
244    }
245    url.set_path("/anthropic/v1");
246    Some(url.to_string())
247}
248
249impl<H> AnthropicClientBuilder<H> {
250    pub fn anthropic_version(self, anthropic_version: &str) -> Self {
251        self.over_ext(|mut ext| {
252            ext.anthropic.anthropic_version = anthropic_version.into();
253            ext
254        })
255    }
256
257    pub fn anthropic_betas(self, anthropic_betas: &[&str]) -> Self {
258        self.over_ext(|mut ext| {
259            ext.anthropic
260                .anthropic_betas
261                .extend(anthropic_betas.iter().copied().map(String::from));
262            ext
263        })
264    }
265
266    pub fn anthropic_beta(self, anthropic_beta: &str) -> Self {
267        self.over_ext(|mut ext| {
268            ext.anthropic.anthropic_betas.push(anthropic_beta.into());
269            ext
270        })
271    }
272}
273
274#[derive(Debug, serde::Deserialize)]
275struct ListModelsResponse {
276    data: Vec<ListModelEntry>,
277}
278
279#[derive(Debug, serde::Deserialize)]
280struct ListModelEntry {
281    id: String,
282    owned_by: String,
283}
284
285impl From<ListModelEntry> for Model {
286    fn from(value: ListModelEntry) -> Self {
287        let mut model = Model::from_id(value.id);
288        model.owned_by = Some(value.owned_by);
289        model
290    }
291}
292
293/// [`ModelLister`] implementation for the Xiaomi MiMo API (`GET /models`).
294#[derive(Clone)]
295pub struct XiaomiMimoModelLister<H = reqwest::Client> {
296    client: Client<H>,
297}
298
299impl<H> ModelLister<H> for XiaomiMimoModelLister<H>
300where
301    H: HttpClientExt + WasmCompatSend + WasmCompatSync + 'static,
302{
303    type Client = Client<H>;
304
305    fn new(client: Self::Client) -> Self {
306        Self { client }
307    }
308
309    async fn list_all(&self) -> Result<ModelList, ModelListingError> {
310        let path = "/models";
311        let req = self.client.get(path)?.body(http_client::NoBody)?;
312        let response = self
313            .client
314            .send::<_, Vec<u8>>(req)
315            .await
316            .map_err(|error| match error {
317                http_client::Error::InvalidStatusCodeWithMessage(status, message) => {
318                    ModelListingError::api_error_with_context(
319                        "Xiaomi MiMo",
320                        path,
321                        status.as_u16(),
322                        message.as_bytes(),
323                    )
324                }
325                other => ModelListingError::from(other),
326            })?;
327
328        if !response.status().is_success() {
329            let status_code = response.status().as_u16();
330            let body = response.into_body().await?;
331            return Err(ModelListingError::api_error_with_context(
332                "Xiaomi MiMo",
333                path,
334                status_code,
335                &body,
336            ));
337        }
338
339        let body = response.into_body().await?;
340        let api_resp: ListModelsResponse = serde_json::from_slice(&body).map_err(|error| {
341            ModelListingError::parse_error_with_context("Xiaomi MiMo", path, &error, &body)
342        })?;
343
344        let models = api_resp.data.into_iter().map(Model::from).collect();
345
346        Ok(ModelList::new(models))
347    }
348}
349
350#[cfg(test)]
351mod tests {
352    use super::{
353        ANTHROPIC_API_BASE_URL, API_BASE_URL, normalize_anthropic_base_url,
354        resolve_anthropic_base_override,
355    };
356
357    #[test]
358    fn test_client_initialization() {
359        let _client =
360            crate::providers::xiaomimimo::Client::new("dummy-key").expect("Client::new()");
361        let _client_from_builder = crate::providers::xiaomimimo::Client::builder()
362            .api_key("dummy-key")
363            .build()
364            .expect("Client::builder()");
365        let _anthropic_client = crate::providers::xiaomimimo::AnthropicClient::new("dummy-key")
366            .expect("AnthropicClient::new()");
367        let _anthropic_client_from_builder =
368            crate::providers::xiaomimimo::AnthropicClient::builder()
369                .api_key("dummy-key")
370                .build()
371                .expect("AnthropicClient::builder()");
372    }
373
374    #[test]
375    fn normalize_openai_bases_to_anthropic_bases() {
376        assert_eq!(
377            normalize_anthropic_base_url(API_BASE_URL).as_deref(),
378            Some(ANTHROPIC_API_BASE_URL)
379        );
380        assert_eq!(
381            normalize_anthropic_base_url("https://proxy.example.com/v1").as_deref(),
382            Some("https://proxy.example.com/anthropic/v1")
383        );
384    }
385
386    #[test]
387    fn normalize_preserves_existing_anthropic_base() {
388        assert_eq!(
389            normalize_anthropic_base_url(ANTHROPIC_API_BASE_URL).as_deref(),
390            Some(ANTHROPIC_API_BASE_URL)
391        );
392    }
393
394    #[test]
395    fn anthropic_primary_override_wins() {
396        let override_url = resolve_anthropic_base_override(
397            Some("https://primary.example.com/anthropic/v1"),
398            Some(API_BASE_URL),
399        );
400
401        assert_eq!(
402            override_url.as_deref(),
403            Some("https://primary.example.com/anthropic/v1")
404        );
405    }
406}