1use 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
35pub const API_BASE_URL: &str = "https://api.xiaomimimo.com/v1";
37pub const ANTHROPIC_API_BASE_URL: &str = "https://api.xiaomimimo.com/anthropic/v1";
39
40pub const MIMO_V2_FLASH: &str = "mimo-v2-flash";
42pub const MIMO_V2_OMNI: &str = "mimo-v2-omni";
44pub const MIMO_V2_PRO: &str = "mimo-v2-pro";
46pub const MIMO_V2_5: &str = "mimo-v2.5";
48pub 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 super::openai::completion::OpenAICompatibleProvider for XiaomiMimoExt {
116 const PROVIDER_NAME: &'static str = "xiaomimimo";
117
118 type StreamingUsage = super::openai::Usage;
119
120 type Response = super::openai::CompletionResponse;
121}
122
123impl ProviderBuilder for XiaomiMimoBuilder {
124 type Extension<H>
125 = XiaomiMimoExt
126 where
127 H: HttpClientExt;
128 type ApiKey = XiaomiMimoApiKey;
129
130 const BASE_URL: &'static str = API_BASE_URL;
131
132 fn build<H>(
133 _builder: &client::ClientBuilder<Self, Self::ApiKey, H>,
134 ) -> http_client::Result<Self::Extension<H>>
135 where
136 H: HttpClientExt,
137 {
138 Ok(XiaomiMimoExt)
139 }
140}
141
142impl ProviderBuilder for XiaomiMimoAnthropicBuilder {
143 type Extension<H>
144 = XiaomiMimoAnthropicExt
145 where
146 H: HttpClientExt;
147 type ApiKey = AnthropicKey;
148
149 const BASE_URL: &'static str = ANTHROPIC_API_BASE_URL;
150
151 fn build<H>(
152 _builder: &client::ClientBuilder<Self, Self::ApiKey, H>,
153 ) -> http_client::Result<Self::Extension<H>>
154 where
155 H: HttpClientExt,
156 {
157 Ok(XiaomiMimoAnthropicExt)
158 }
159
160 fn finish<H>(
161 &self,
162 builder: client::ClientBuilder<Self, AnthropicKey, H>,
163 ) -> http_client::Result<client::ClientBuilder<Self, AnthropicKey, H>> {
164 finish_anthropic_builder(&self.anthropic, builder)
165 }
166}
167
168impl super::anthropic::completion::AnthropicCompatibleProvider for XiaomiMimoAnthropicExt {
169 const PROVIDER_NAME: &'static str = "xiaomimimo";
170
171 fn default_max_tokens(_model: &str) -> Option<u64> {
172 Some(4096)
173 }
174}
175
176impl ProviderClient for Client {
177 type Input = XiaomiMimoApiKey;
178 type Error = crate::client::ProviderClientError;
179
180 fn from_env() -> Result<Self, Self::Error> {
181 let api_key = crate::client::required_env_var("XIAOMI_MIMO_API_KEY")?;
182 let mut builder = Self::builder().api_key(api_key);
183
184 if let Some(base_url) = crate::client::optional_env_var("XIAOMI_MIMO_API_BASE")? {
185 builder = builder.base_url(base_url);
186 }
187
188 builder.build().map_err(Into::into)
189 }
190
191 fn from_val(input: Self::Input) -> Result<Self, Self::Error> {
192 Self::new(input).map_err(Into::into)
193 }
194}
195
196impl ProviderClient for AnthropicClient {
197 type Input = String;
198 type Error = crate::client::ProviderClientError;
199
200 fn from_env() -> Result<Self, Self::Error> {
201 let api_key = crate::client::required_env_var("XIAOMI_MIMO_API_KEY")?;
202 let mut builder = Self::builder().api_key(api_key);
203
204 if let Some(base_url) =
205 anthropic_base_override("XIAOMI_MIMO_ANTHROPIC_API_BASE", "XIAOMI_MIMO_API_BASE")?
206 {
207 builder = builder.base_url(base_url);
208 }
209
210 builder.build().map_err(Into::into)
211 }
212
213 fn from_val(input: Self::Input) -> Result<Self, Self::Error> {
214 Self::builder().api_key(input).build().map_err(Into::into)
215 }
216}
217
218fn anthropic_base_override(
219 primary_env: &'static str,
220 fallback_env: &'static str,
221) -> crate::client::ProviderClientResult<Option<String>> {
222 let primary = crate::client::optional_env_var(primary_env)?;
223 let fallback = crate::client::optional_env_var(fallback_env)?;
224
225 Ok(resolve_anthropic_base_override(
226 primary.as_deref(),
227 fallback.as_deref(),
228 ))
229}
230
231fn resolve_anthropic_base_override(
232 primary: Option<&str>,
233 fallback: Option<&str>,
234) -> Option<String> {
235 primary
236 .map(str::to_owned)
237 .or_else(|| fallback.and_then(normalize_anthropic_base_url))
238}
239
240fn normalize_anthropic_base_url(base_url: &str) -> Option<String> {
241 if base_url.contains("/anthropic") {
242 return Some(base_url.to_owned());
243 }
244
245 if base_url.trim_end_matches('/') == API_BASE_URL {
246 return Some(ANTHROPIC_API_BASE_URL.to_owned());
247 }
248
249 let mut url = url::Url::parse(base_url).ok()?;
250 if !matches!(url.path(), "/v1" | "/v1/") {
251 return None;
252 }
253 url.set_path("/anthropic/v1");
254 Some(url.to_string())
255}
256
257impl<H> AnthropicClientBuilder<H> {
258 pub fn anthropic_version(self, anthropic_version: &str) -> Self {
259 self.over_ext(|mut ext| {
260 ext.anthropic.anthropic_version = anthropic_version.into();
261 ext
262 })
263 }
264
265 pub fn anthropic_betas(self, anthropic_betas: &[&str]) -> Self {
266 self.over_ext(|mut ext| {
267 ext.anthropic
268 .anthropic_betas
269 .extend(anthropic_betas.iter().copied().map(String::from));
270 ext
271 })
272 }
273
274 pub fn anthropic_beta(self, anthropic_beta: &str) -> Self {
275 self.over_ext(|mut ext| {
276 ext.anthropic.anthropic_betas.push(anthropic_beta.into());
277 ext
278 })
279 }
280}
281
282#[derive(Debug, serde::Deserialize)]
283struct ListModelsResponse {
284 data: Vec<ListModelEntry>,
285}
286
287#[derive(Debug, serde::Deserialize)]
288struct ListModelEntry {
289 id: String,
290 owned_by: String,
291}
292
293impl From<ListModelEntry> for Model {
294 fn from(value: ListModelEntry) -> Self {
295 let mut model = Model::from_id(value.id);
296 model.owned_by = Some(value.owned_by);
297 model
298 }
299}
300
301#[derive(Clone)]
303pub struct XiaomiMimoModelLister<H = reqwest::Client> {
304 client: Client<H>,
305}
306
307impl<H> ModelLister<H> for XiaomiMimoModelLister<H>
308where
309 H: HttpClientExt + WasmCompatSend + WasmCompatSync + 'static,
310{
311 type Client = Client<H>;
312
313 fn new(client: Self::Client) -> Self {
314 Self { client }
315 }
316
317 async fn list_all(&self) -> Result<ModelList, ModelListingError> {
318 let path = "/models";
319 let req = self.client.get(path)?.body(http_client::NoBody)?;
320 let response = self
321 .client
322 .send::<_, Vec<u8>>(req)
323 .await
324 .map_err(|error| match error {
325 http_client::Error::InvalidStatusCodeWithMessage(status, message) => {
326 ModelListingError::api_error_with_context(
327 "Xiaomi MiMo",
328 path,
329 status.as_u16(),
330 message.as_bytes(),
331 )
332 }
333 other => ModelListingError::from(other),
334 })?;
335
336 if !response.status().is_success() {
337 let status_code = response.status().as_u16();
338 let body = response.into_body().await?;
339 return Err(ModelListingError::api_error_with_context(
340 "Xiaomi MiMo",
341 path,
342 status_code,
343 &body,
344 ));
345 }
346
347 let body = response.into_body().await?;
348 let api_resp: ListModelsResponse = serde_json::from_slice(&body).map_err(|error| {
349 ModelListingError::parse_error_with_context("Xiaomi MiMo", path, &error, &body)
350 })?;
351
352 let models = api_resp.data.into_iter().map(Model::from).collect();
353
354 Ok(ModelList::new(models))
355 }
356}
357
358#[cfg(test)]
359mod tests {
360 use super::{
361 ANTHROPIC_API_BASE_URL, API_BASE_URL, normalize_anthropic_base_url,
362 resolve_anthropic_base_override,
363 };
364
365 #[test]
366 fn test_client_initialization() {
367 let _client =
368 crate::providers::xiaomimimo::Client::new("dummy-key").expect("Client::new()");
369 let _client_from_builder = crate::providers::xiaomimimo::Client::builder()
370 .api_key("dummy-key")
371 .build()
372 .expect("Client::builder()");
373 let _anthropic_client = crate::providers::xiaomimimo::AnthropicClient::new("dummy-key")
374 .expect("AnthropicClient::new()");
375 let _anthropic_client_from_builder =
376 crate::providers::xiaomimimo::AnthropicClient::builder()
377 .api_key("dummy-key")
378 .build()
379 .expect("AnthropicClient::builder()");
380 }
381
382 #[test]
383 fn normalize_openai_bases_to_anthropic_bases() {
384 assert_eq!(
385 normalize_anthropic_base_url(API_BASE_URL).as_deref(),
386 Some(ANTHROPIC_API_BASE_URL)
387 );
388 assert_eq!(
389 normalize_anthropic_base_url("https://proxy.example.com/v1").as_deref(),
390 Some("https://proxy.example.com/anthropic/v1")
391 );
392 }
393
394 #[test]
395 fn normalize_preserves_existing_anthropic_base() {
396 assert_eq!(
397 normalize_anthropic_base_url(ANTHROPIC_API_BASE_URL).as_deref(),
398 Some(ANTHROPIC_API_BASE_URL)
399 );
400 }
401
402 #[test]
403 fn anthropic_primary_override_wins() {
404 let override_url = resolve_anthropic_base_override(
405 Some("https://primary.example.com/anthropic/v1"),
406 Some(API_BASE_URL),
407 );
408
409 assert_eq!(
410 override_url.as_deref(),
411 Some("https://primary.example.com/anthropic/v1")
412 );
413 }
414}