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}