Skip to main content

mimo_api/
client.rs

1//! HTTP client for the MiMo API.
2
3use crate::error::{Error, Result};
4use crate::types::*;
5use eventsource_stream::Eventsource;
6use futures::stream::BoxStream;
7use futures::StreamExt;
8use reqwest::header::{HeaderMap, HeaderValue, CONTENT_TYPE};
9use std::env;
10
11const API_BASE_URL: &str = "https://api.xiaomimimo.com/v1";
12const ENV_API_KEY: &str = "XIAOMI_API_KEY";
13
14/// HTTP client for the MiMo API.
15#[derive(Debug, Clone)]
16pub struct Client {
17    /// The underlying HTTP client.
18    http_client: reqwest::Client,
19    /// The API key for authentication.
20    api_key: String,
21    /// The base URL for the API.
22    base_url: String,
23}
24
25impl Client {
26    /// Create a new client with the given API key.
27    ///
28    /// # Example
29    ///
30    /// ```rust
31    /// use mimo_api::Client;
32    ///
33    /// let client = Client::new("your-api-key");
34    /// ```
35    pub fn new(api_key: impl Into<String>) -> Self {
36        Self {
37            http_client: reqwest::Client::new(),
38            api_key: api_key.into(),
39            base_url: API_BASE_URL.to_string(),
40        }
41    }
42
43    /// Create a new client from the `XIAOMI_API_KEY` environment variable.
44    ///
45    /// # Errors
46    ///
47    /// Returns an error if the `XIAOMI_API_KEY` environment variable is not set.
48    ///
49    /// # Example
50    ///
51    /// ```rust,no_run
52    /// use mimo_api::Client;
53    ///
54    /// // Assuming XIAOMI_API_KEY is set in environment
55    /// let client = Client::from_env()?;
56    /// # Ok::<(), mimo::Error>(())
57    /// ```
58    pub fn from_env() -> Result<Self> {
59        let api_key = env::var(ENV_API_KEY).map_err(|_| Error::MissingApiKey)?;
60        Ok(Self::new(api_key))
61    }
62
63    /// Set a custom base URL for the API.
64    ///
65    /// This is useful for testing or using a custom API endpoint.
66    pub fn with_base_url(mut self, base_url: impl Into<String>) -> Self {
67        self.base_url = base_url.into();
68        self
69    }
70
71    /// Build headers for the request.
72    fn build_headers(&self) -> Result<HeaderMap> {
73        let mut headers = HeaderMap::new();
74        headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
75        headers.insert(
76            "api-key",
77            HeaderValue::from_str(&self.api_key)
78                .map_err(|_| Error::InvalidParameter("Invalid API key".into()))?,
79        );
80        Ok(headers)
81    }
82
83    /// Send a chat completion request.
84    ///
85    /// # Example
86    ///
87    /// ```rust,no_run
88    /// use mimo_api::{Client, ChatRequest, Message};
89    ///
90    /// #[tokio::main]
91    /// async fn main() -> Result<(), Box<dyn std::error::Error>> {
92    ///     let client = Client::from_env()?;
93    ///     let request = ChatRequest::new("mimo-v2-flash")
94    ///         .message(Message::user("Hello!"));
95    ///     let response = client.chat(request).await?;
96    ///     println!("{}", response.choices[0].message.content);
97    ///     Ok(())
98    /// }
99    /// ```
100    pub async fn chat(&self, request: ChatRequest) -> Result<ChatResponse> {
101        let url = format!("{}/chat/completions", self.base_url);
102        let headers = self.build_headers()?;
103
104        let response = self
105            .http_client
106            .post(&url)
107            .headers(headers)
108            .json(&request)
109            .send()
110            .await?;
111
112        let status = response.status();
113        if !status.is_success() {
114            let error_text = response.text().await.unwrap_or_default();
115            return Err(Error::api_error(status.as_u16(), error_text));
116        }
117
118        response.json().await.map_err(Error::from)
119    }
120
121    /// Send a chat completion request with streaming response.
122    ///
123    /// Returns a stream of `StreamChunk` objects.
124    ///
125    /// # Example
126    ///
127    /// ```rust,no_run
128    /// use mimo_api::{Client, ChatRequest, Message};
129    /// use futures::StreamExt;
130    ///
131    /// #[tokio::main]
132    /// async fn main() -> Result<(), Box<dyn std::error::Error>> {
133    ///     let client = Client::from_env()?;
134    ///     let request = ChatRequest::new("mimo-v2-flash")
135    ///         .message(Message::user("Tell me a story."))
136    ///         .stream(true);
137    ///     
138    ///     let mut stream = client.chat_stream(request).await?;
139    ///     while let Some(chunk) = stream.next().await {
140    ///         match chunk {
141    ///             Ok(chunk) => {
142    ///                 if let Some(content) = &chunk.choices[0].delta.content {
143    ///                     print!("{}", content);
144    ///                 }
145    ///             }
146    ///             Err(e) => eprintln!("Error: {}", e),
147    ///         }
148    ///     }
149    ///     Ok(())
150    /// }
151    /// ```
152    pub async fn chat_stream(
153        &self,
154        request: ChatRequest,
155    ) -> Result<BoxStream<'static, Result<StreamChunk>>> {
156        let mut request = request;
157        request.stream = Some(true);
158
159        let url = format!("{}/chat/completions", self.base_url);
160        let headers = self.build_headers()?;
161
162        let response = self
163            .http_client
164            .post(&url)
165            .headers(headers)
166            .json(&request)
167            .send()
168            .await?;
169
170        let status = response.status();
171        if !status.is_success() {
172            let error_text = response.text().await.unwrap_or_default();
173            return Err(Error::api_error(status.as_u16(), error_text));
174        }
175
176        let stream = response
177            .bytes_stream()
178            .eventsource()
179            .filter_map(|event| async move {
180                match event {
181                    Ok(event) => {
182                        if event.data == "[DONE]" {
183                            None
184                        } else {
185                            match serde_json::from_str::<StreamChunk>(&event.data) {
186                                Ok(chunk) => Some(Ok(chunk)),
187                                Err(e) => Some(Err(Error::StreamError(e.to_string()))),
188                            }
189                        }
190                    }
191                    Err(e) => Some(Err(Error::StreamError(e.to_string()))),
192                }
193            })
194            .boxed();
195
196        Ok(stream)
197    }
198
199    /// Create a text-to-speech request builder.
200    ///
201    /// This method creates a builder for synthesizing speech from text using the `mimo-v2-tts` model.
202    ///
203    /// # Arguments
204    ///
205    /// * `text` - The text to synthesize. This text will be placed in an `assistant` message.
206    ///
207    /// # Example
208    ///
209    /// ```rust,no_run
210    /// use mimo_api::{Client, Voice};
211    ///
212    /// #[tokio::main]
213    /// async fn main() -> Result<(), Box<dyn std::error::Error>> {
214    ///     let client = Client::from_env()?;
215    ///     
216    ///     let response = client.tts("Hello, world!")
217    ///         .voice(Voice::DefaultEn)
218    ///         .send()
219    ///         .await?;
220    ///     
221    ///     let audio = response.audio()?;
222    ///     let audio_bytes = audio.decode_data()?;
223    ///     std::fs::write("output.wav", audio_bytes)?;
224    ///     Ok(())
225    /// }
226    /// ```
227    pub fn tts(&self, text: impl Into<String>) -> TtsRequestBuilder {
228        TtsRequestBuilder::new(self.clone(), text.into())
229    }
230
231    /// Create a text-to-speech request builder with styled text.
232    ///
233    /// This method allows you to apply style controls to the synthesized speech.
234    ///
235    /// # Example
236    ///
237    /// ```rust,no_run
238    /// use mimo_api::{Client, Voice};
239    ///
240    /// #[tokio::main]
241    /// async fn main() -> Result<(), Box<dyn std::error::Error>> {
242    ///     let client = Client::from_env()?;
243    ///     
244    ///     // Synthesize speech with "开心" (happy) style
245    ///     let response = client.tts_styled("开心", "明天就是周五了,真开心!")
246    ///         .voice(Voice::DefaultZh)
247    ///         .send()
248    ///         .await?;
249    ///     
250    ///     let audio = response.audio()?;
251    ///     let audio_bytes = audio.decode_data()?;
252    ///     std::fs::write("output.wav", audio_bytes)?;
253    ///     Ok(())
254    /// }
255    /// ```
256    pub fn tts_styled(&self, style: &str, text: &str) -> TtsRequestBuilder {
257        TtsRequestBuilder::new(self.clone(), styled_text(style, text))
258    }
259}
260
261/// Builder for text-to-speech requests.
262///
263/// This builder provides a fluent API for configuring TTS requests.
264#[derive(Debug, Clone)]
265pub struct TtsRequestBuilder {
266    client: Client,
267    text: String,
268    user_message: Option<String>,
269    voice: Voice,
270    format: AudioFormat,
271}
272
273impl TtsRequestBuilder {
274    /// Create a new TTS request builder.
275    fn new(client: Client, text: String) -> Self {
276        Self {
277            client,
278            text,
279            user_message: None,
280            voice: Voice::default(),
281            format: AudioFormat::default(),
282        }
283    }
284
285    /// Set the voice for synthesis.
286    ///
287    /// Available voices:
288    /// - `Voice::MimoDefault` - MiMo default voice (balanced tone)
289    /// - `Voice::DefaultEn` - Default English female voice
290    /// - `Voice::DefaultZh` - Default Chinese female voice
291    pub fn voice(mut self, voice: Voice) -> Self {
292        self.voice = voice;
293        self
294    }
295
296    /// Set the audio output format.
297    ///
298    /// Available formats:
299    /// - `AudioFormat::Wav` - WAV format (recommended for high quality)
300    /// - `AudioFormat::Mp3` - MP3 format (smaller file size)
301    /// - `AudioFormat::Pcm` - PCM format (for streaming)
302    pub fn format(mut self, format: AudioFormat) -> Self {
303        self.format = format;
304        self
305    }
306
307    /// Add a user message to influence the synthesis style.
308    ///
309    /// The user message can help adjust the tone and style of the synthesized speech.
310    pub fn user_message(mut self, message: impl Into<String>) -> Self {
311        self.user_message = Some(message.into());
312        self
313    }
314
315    /// Send the TTS request and return the response.
316    ///
317    /// # Returns
318    ///
319    /// A `TtsResponse` containing the synthesized audio data.
320    ///
321    /// # Example
322    ///
323    /// ```rust,no_run
324    /// use mimo_api::{Client, Voice, AudioFormat};
325    ///
326    /// #[tokio::main]
327    /// async fn main() -> Result<(), Box<dyn std::error::Error>> {
328    ///     let client = Client::from_env()?;
329    ///     
330    ///     let response = client.tts("Hello, world!")
331    ///         .voice(Voice::DefaultEn)
332    ///         .format(AudioFormat::Mp3)
333    ///         .send()
334    ///         .await?;
335    ///     
336    ///     let audio = response.audio()?;
337    ///     println!("Audio ID: {}", audio.id);
338    ///     println!("Transcript: {:?}", audio.transcript());
339    ///     Ok(())
340    /// }
341    /// ```
342    pub async fn send(self) -> Result<TtsResponse> {
343        let mut messages = Vec::new();
344
345        // Add optional user message
346        if let Some(user_msg) = self.user_message {
347            messages.push(Message::user(MessageContent::Text(user_msg)));
348        }
349
350        // Add assistant message with text to synthesize
351        messages.push(Message::assistant(MessageContent::Text(self.text)));
352
353        let request = ChatRequest {
354            model: Model::MiMoV2Tts.to_string(),
355            messages,
356            audio: Some(Audio {
357                format: Some(self.format),
358                voice: Some(self.voice),
359            }),
360            ..Default::default()
361        };
362
363        let response = self.client.chat(request).await?;
364        Ok(TtsResponse(response))
365    }
366}
367
368/// Response from a text-to-speech request.
369#[derive(Debug, Clone)]
370pub struct TtsResponse(pub ChatResponse);
371
372impl TtsResponse {
373    /// Get the audio data from the response.
374    ///
375    /// # Errors
376    ///
377    /// Returns an error if no audio data is present in the response.
378    pub fn audio(&self) -> Result<&ResponseAudio> {
379        self.0
380            .choices
381            .first()
382            .and_then(|c| c.message.audio.as_ref())
383            .ok_or_else(|| Error::InvalidResponse("No audio data in response".into()))
384    }
385
386    /// Get the content text from the response.
387    pub fn content(&self) -> Option<&str> {
388        self.0.choices.first().map(|c| c.message.content.as_str())
389    }
390
391    /// Get the underlying chat response.
392    pub fn into_inner(self) -> ChatResponse {
393        self.0
394    }
395}