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