rig/providers/
mira.rs

1//! Mira API client and Rig integration
2//!
3//! # Example
4//! ```
5//! use rig::providers::mira;
6//!
7//! let client = mira::Client::new("YOUR_API_KEY");
8//!
9//! ```
10use crate::client::{CompletionClient, ProviderClient, VerifyClient, VerifyError};
11use crate::http_client::{self, HttpClientExt};
12use crate::json_utils::merge;
13use crate::message::{Document, DocumentSourceKind};
14use crate::providers::openai;
15use crate::providers::openai::send_compatible_streaming_request;
16use crate::streaming::StreamingCompletionResponse;
17use crate::{
18    OneOrMany,
19    completion::{self, CompletionError, CompletionRequest},
20    impl_conversion_traits,
21    message::{self, AssistantContent, Message, UserContent},
22};
23use http::Method;
24use reqwest::header::{CONTENT_TYPE, HeaderMap, HeaderValue};
25use serde::{Deserialize, Serialize};
26use serde_json::{Value, json};
27use std::string::FromUtf8Error;
28use thiserror::Error;
29use tracing::{self, Instrument, info_span};
30
31#[derive(Debug, Error)]
32pub enum MiraError {
33    #[error("Invalid API key")]
34    InvalidApiKey,
35    #[error("API error: {0}")]
36    ApiError(u16),
37    #[error("Request error: {0}")]
38    RequestError(#[from] http_client::Error),
39    #[error("UTF-8 error: {0}")]
40    Utf8Error(#[from] FromUtf8Error),
41    #[error("JSON error: {0}")]
42    JsonError(#[from] serde_json::Error),
43}
44
45#[derive(Debug, Deserialize)]
46struct ApiErrorResponse {
47    message: String,
48}
49
50#[derive(Debug, Deserialize, Clone, Serialize)]
51pub struct RawMessage {
52    pub role: String,
53    pub content: String,
54}
55
56const MIRA_API_BASE_URL: &str = "https://api.mira.network";
57
58impl TryFrom<RawMessage> for message::Message {
59    type Error = CompletionError;
60
61    fn try_from(raw: RawMessage) -> Result<Self, Self::Error> {
62        match raw.role.as_str() {
63            "user" => Ok(message::Message::User {
64                content: OneOrMany::one(UserContent::Text(message::Text { text: raw.content })),
65            }),
66            "assistant" => Ok(message::Message::Assistant {
67                id: None,
68                content: OneOrMany::one(AssistantContent::Text(message::Text {
69                    text: raw.content,
70                })),
71            }),
72            _ => Err(CompletionError::ResponseError(format!(
73                "Unsupported message role: {}",
74                raw.role
75            ))),
76        }
77    }
78}
79
80#[derive(Debug, Deserialize, Serialize)]
81#[serde(untagged)]
82pub enum CompletionResponse {
83    Structured {
84        id: String,
85        object: String,
86        created: u64,
87        model: String,
88        choices: Vec<ChatChoice>,
89        #[serde(skip_serializing_if = "Option::is_none")]
90        usage: Option<Usage>,
91    },
92    Simple(String),
93}
94
95#[derive(Debug, Deserialize, Serialize)]
96pub struct ChatChoice {
97    pub message: RawMessage,
98    #[serde(default)]
99    pub finish_reason: Option<String>,
100    #[serde(default)]
101    pub index: Option<usize>,
102}
103
104#[derive(Debug, Deserialize, Serialize)]
105struct ModelsResponse {
106    data: Vec<ModelInfo>,
107}
108
109#[derive(Debug, Deserialize, Serialize)]
110struct ModelInfo {
111    id: String,
112}
113
114pub struct ClientBuilder<'a, T = reqwest::Client> {
115    api_key: &'a str,
116    base_url: &'a str,
117    http_client: T,
118}
119
120impl<'a, T> ClientBuilder<'a, T>
121where
122    T: Default,
123{
124    pub fn new(api_key: &'a str) -> Self {
125        Self {
126            api_key,
127            base_url: MIRA_API_BASE_URL,
128            http_client: Default::default(),
129        }
130    }
131}
132
133impl<'a, T> ClientBuilder<'a, T> {
134    pub fn new_with_client(api_key: &'a str, http_client: T) -> Self {
135        Self {
136            api_key,
137            base_url: MIRA_API_BASE_URL,
138            http_client,
139        }
140    }
141
142    pub fn base_url(mut self, base_url: &'a str) -> Self {
143        self.base_url = base_url;
144        self
145    }
146
147    pub fn with_client<U>(self, http_client: U) -> ClientBuilder<'a, U> {
148        ClientBuilder {
149            api_key: self.api_key,
150            base_url: self.base_url,
151            http_client,
152        }
153    }
154
155    pub fn build(self) -> Client<T> {
156        let mut headers = HeaderMap::new();
157        headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
158        headers.insert(
159            reqwest::header::ACCEPT,
160            HeaderValue::from_static("application/json"),
161        );
162        headers.insert(
163            reqwest::header::USER_AGENT,
164            HeaderValue::from_static("rig-client/1.0"),
165        );
166
167        Client {
168            base_url: self.base_url.to_string(),
169            http_client: self.http_client,
170            api_key: self.api_key.to_string(),
171            headers,
172        }
173    }
174}
175
176#[derive(Clone)]
177/// Client for interacting with the Mira API
178pub struct Client<T = reqwest::Client> {
179    base_url: String,
180    http_client: T,
181    api_key: String,
182    headers: HeaderMap,
183}
184
185impl<T> std::fmt::Debug for Client<T>
186where
187    T: std::fmt::Debug,
188{
189    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
190        f.debug_struct("Client")
191            .field("base_url", &self.base_url)
192            .field("http_client", &self.http_client)
193            .field("api_key", &"<REDACTED>")
194            .field("headers", &self.headers)
195            .finish()
196    }
197}
198
199impl<T> Client<T>
200where
201    T: HttpClientExt,
202{
203    /// List available models
204    pub async fn list_models(&self) -> Result<Vec<String>, MiraError> {
205        let req = self.get("/v1/models").and_then(|req| {
206            req.body(http_client::NoBody)
207                .map_err(http_client::Error::Protocol)
208        })?;
209
210        let response = self.http_client.send(req).await?;
211
212        let status = response.status();
213
214        if !status.is_success() {
215            // Log the error text but don't store it in an unused variable
216            let error_text = http_client::text(response).await.unwrap_or_default();
217            tracing::error!("Error response: {}", error_text);
218            return Err(MiraError::ApiError(status.as_u16()));
219        }
220
221        let response_text = http_client::text(response).await?;
222
223        let models: ModelsResponse = serde_json::from_str(&response_text).map_err(|e| {
224            tracing::error!("Failed to parse response: {}", e);
225            MiraError::JsonError(e)
226        })?;
227
228        Ok(models.data.into_iter().map(|model| model.id).collect())
229    }
230
231    fn req(
232        &self,
233        method: http_client::Method,
234        path: &str,
235    ) -> http_client::Result<http_client::Builder> {
236        let url = format!("{}/{}", self.base_url, path.trim_start_matches('/'));
237
238        let mut req = http_client::Builder::new().method(method).uri(url);
239
240        if let Some(hs) = req.headers_mut() {
241            *hs = self.headers.clone();
242        }
243
244        http_client::with_bearer_auth(req, &self.api_key)
245    }
246
247    pub(crate) fn get(&self, path: &str) -> http_client::Result<http_client::Builder> {
248        self.req(http_client::Method::POST, path)
249    }
250}
251
252impl Client<reqwest::Client> {
253    pub fn builder(api_key: &str) -> ClientBuilder<'_, reqwest::Client> {
254        ClientBuilder::new(api_key)
255    }
256
257    pub fn new(api_key: &str) -> Self {
258        Self::builder(api_key).build()
259    }
260
261    pub fn from_env() -> Self {
262        <Self as ProviderClient>::from_env()
263    }
264}
265
266impl<T> ProviderClient for Client<T>
267where
268    T: HttpClientExt + Clone + std::fmt::Debug + Default + Send + 'static,
269{
270    /// Create a new Mira client from the `MIRA_API_KEY` environment variable.
271    /// Panics if the environment variable is not set.
272    fn from_env() -> Self {
273        let api_key = std::env::var("MIRA_API_KEY").expect("MIRA_API_KEY not set");
274        ClientBuilder::<T>::new(&api_key).build()
275    }
276
277    fn from_val(input: crate::client::ProviderValue) -> Self {
278        let crate::client::ProviderValue::Simple(api_key) = input else {
279            panic!("Incorrect provider value type")
280        };
281        ClientBuilder::<T>::new(&api_key).build()
282    }
283}
284
285impl<T> CompletionClient for Client<T>
286where
287    T: HttpClientExt + Clone + std::fmt::Debug + Default + Send + 'static,
288{
289    type CompletionModel = CompletionModel<T>;
290
291    /// Create a completion model with the given name.
292    fn completion_model(&self, model: &str) -> Self::CompletionModel {
293        CompletionModel::new(self.to_owned(), model)
294    }
295}
296
297impl<T> VerifyClient for Client<T>
298where
299    T: HttpClientExt + Clone + std::fmt::Debug + Default + Send + 'static,
300{
301    #[cfg_attr(feature = "worker", worker::send)]
302    async fn verify(&self) -> Result<(), VerifyError> {
303        let req = self
304            .get("/user-credits")?
305            .body(http_client::NoBody)
306            .map_err(http_client::Error::from)?;
307
308        let response = HttpClientExt::send(&self.http_client, req).await?;
309
310        match response.status() {
311            reqwest::StatusCode::OK => Ok(()),
312            reqwest::StatusCode::UNAUTHORIZED => Err(VerifyError::InvalidAuthentication),
313            reqwest::StatusCode::INTERNAL_SERVER_ERROR
314            | reqwest::StatusCode::SERVICE_UNAVAILABLE
315            | reqwest::StatusCode::BAD_GATEWAY => {
316                let text = http_client::text(response).await?;
317                Err(VerifyError::ProviderError(text))
318            }
319            _ => {
320                //response.error_for_status()?;
321                Ok(())
322            }
323        }
324    }
325}
326
327impl_conversion_traits!(
328    AsEmbeddings,
329    AsTranscription,
330    AsImageGeneration,
331    AsAudioGeneration for Client<T>
332);
333
334#[derive(Clone)]
335pub struct CompletionModel<T> {
336    client: Client<T>,
337    /// Name of the model
338    pub model: String,
339}
340
341impl<T> CompletionModel<T> {
342    pub fn new(client: Client<T>, model: &str) -> Self {
343        Self {
344            client,
345            model: model.to_string(),
346        }
347    }
348
349    fn create_completion_request(
350        &self,
351        completion_request: CompletionRequest,
352    ) -> Result<Value, CompletionError> {
353        if completion_request.tool_choice.is_some() {
354            tracing::warn!("WARNING: `tool_choice` not supported on Mira AI");
355        }
356
357        let mut messages = Vec::new();
358
359        // Add preamble as user message if available
360        if let Some(preamble) = &completion_request.preamble {
361            messages.push(serde_json::json!({
362                "role": "user",
363                "content": preamble.to_string()
364            }));
365        }
366
367        // Add docs
368        if let Some(Message::User { content }) = completion_request.normalized_documents() {
369            let text = content
370                .into_iter()
371                .filter_map(|doc| match doc {
372                    UserContent::Document(Document {
373                        data: DocumentSourceKind::Base64(data) | DocumentSourceKind::String(data),
374                        ..
375                    }) => Some(data),
376                    UserContent::Text(text) => Some(text.text),
377
378                    // This should always be `Document`
379                    _ => None,
380                })
381                .collect::<Vec<_>>()
382                .join("\n");
383
384            messages.push(serde_json::json!({
385                "role": "user",
386                "content": text
387            }));
388        }
389
390        // Add chat history
391        for msg in completion_request.chat_history {
392            let (role, content) = match msg {
393                Message::User { content } => {
394                    let text = content
395                        .iter()
396                        .map(|c| match c {
397                            UserContent::Text(text) => &text.text,
398                            _ => "",
399                        })
400                        .collect::<Vec<_>>()
401                        .join("\n");
402                    ("user", text)
403                }
404                Message::Assistant { content, .. } => {
405                    let text = content
406                        .iter()
407                        .map(|c| match c {
408                            AssistantContent::Text(text) => &text.text,
409                            _ => "",
410                        })
411                        .collect::<Vec<_>>()
412                        .join("\n");
413                    ("assistant", text)
414                }
415            };
416            messages.push(serde_json::json!({
417                "role": role,
418                "content": content
419            }));
420        }
421
422        let request = serde_json::json!({
423            "model": self.model,
424            "messages": messages,
425            "temperature": completion_request.temperature.map(|t| t as f32).unwrap_or(0.7),
426            "max_tokens": completion_request.max_tokens.map(|t| t as u32).unwrap_or(100),
427            "stream": false
428        });
429
430        Ok(request)
431    }
432}
433
434impl<T> completion::CompletionModel for CompletionModel<T>
435where
436    T: HttpClientExt + Clone + Default + std::fmt::Debug + Send + 'static,
437{
438    type Response = CompletionResponse;
439    type StreamingResponse = openai::StreamingCompletionResponse;
440
441    #[cfg_attr(feature = "worker", worker::send)]
442    async fn completion(
443        &self,
444        completion_request: CompletionRequest,
445    ) -> Result<completion::CompletionResponse<CompletionResponse>, CompletionError> {
446        if !completion_request.tools.is_empty() {
447            tracing::warn!(target: "rig::completions",
448                "Tool calls are not supported by the Mira provider. {len} tools will be ignored.",
449                len = completion_request.tools.len()
450            );
451        }
452
453        let preamble = completion_request.preamble.clone();
454
455        let request = self.create_completion_request(completion_request)?;
456
457        let span = if tracing::Span::current().is_disabled() {
458            info_span!(
459                target: "rig::completions",
460                "chat",
461                gen_ai.operation.name = "chat",
462                gen_ai.provider.name = "mira",
463                gen_ai.request.model = self.model,
464                gen_ai.system_instructions = preamble,
465                gen_ai.response.id = tracing::field::Empty,
466                gen_ai.response.model = tracing::field::Empty,
467                gen_ai.usage.output_tokens = tracing::field::Empty,
468                gen_ai.usage.input_tokens = tracing::field::Empty,
469                gen_ai.input.messages = serde_json::to_string(&request.get("messages").unwrap()).unwrap(),
470                gen_ai.output.messages = tracing::field::Empty,
471            )
472        } else {
473            tracing::Span::current()
474        };
475
476        let body = serde_json::to_vec(&request)?;
477
478        let req = self
479            .client
480            .req(Method::POST, "/v1/chat/completions")?
481            .header("Content-Type", "application/json")
482            .body(body)
483            .map_err(http_client::Error::from)?;
484
485        let async_block = async move {
486            let response = self
487                .client
488                .http_client
489                .send::<_, bytes::Bytes>(req)
490                .await
491                .map_err(|e| CompletionError::ProviderError(e.to_string()))?;
492
493            let status = response.status();
494            let response_body = response.into_body().into_future().await?.to_vec();
495
496            if !status.is_success() {
497                let status = status.as_u16();
498                let error_text = String::from_utf8_lossy(&response_body).to_string();
499                return Err(CompletionError::ProviderError(format!(
500                    "API error: {status} - {error_text}"
501                )));
502            }
503
504            let response: CompletionResponse = serde_json::from_slice(&response_body)?;
505
506            if let CompletionResponse::Structured {
507                id,
508                model,
509                choices,
510                usage,
511                ..
512            } = &response
513            {
514                let span = tracing::Span::current();
515                span.record("gen_ai.response.model_name", model);
516                span.record("gen_ai.response.id", id);
517                span.record(
518                    "gen_ai.output.messages",
519                    serde_json::to_string(choices).unwrap(),
520                );
521                if let Some(usage) = usage {
522                    span.record("gen_ai.usage.input_tokens", usage.prompt_tokens);
523                    span.record(
524                        "gen_ai.usage.output_tokens",
525                        usage.total_tokens - usage.prompt_tokens,
526                    );
527                }
528            }
529
530            response.try_into()
531        };
532
533        async_block.instrument(span).await
534    }
535
536    #[cfg_attr(feature = "worker", worker::send)]
537    async fn stream(
538        &self,
539        completion_request: CompletionRequest,
540    ) -> Result<StreamingCompletionResponse<Self::StreamingResponse>, CompletionError> {
541        let preamble = completion_request.preamble.clone();
542        let mut request = self.create_completion_request(completion_request)?;
543
544        let span = if tracing::Span::current().is_disabled() {
545            info_span!(
546                target: "rig::completions",
547                "chat_streaming",
548                gen_ai.operation.name = "chat_streaming",
549                gen_ai.provider.name = "mira",
550                gen_ai.request.model = self.model,
551                gen_ai.system_instructions = preamble,
552                gen_ai.response.id = tracing::field::Empty,
553                gen_ai.response.model = tracing::field::Empty,
554                gen_ai.usage.output_tokens = tracing::field::Empty,
555                gen_ai.usage.input_tokens = tracing::field::Empty,
556                gen_ai.input.messages = serde_json::to_string(&request.get("messages").unwrap()).unwrap(),
557                gen_ai.output.messages = tracing::field::Empty,
558            )
559        } else {
560            tracing::Span::current()
561        };
562        request = merge(request, json!({"stream": true}));
563        let body = serde_json::to_vec(&request)?;
564
565        let req = self
566            .client
567            .req(Method::POST, "/v1/chat/completions")?
568            .header("Content-Type", "application/json")
569            .body(body)
570            .map_err(http_client::Error::from)?;
571
572        send_compatible_streaming_request(self.client.http_client.clone(), req)
573            .instrument(span)
574            .await
575    }
576}
577
578impl From<ApiErrorResponse> for CompletionError {
579    fn from(err: ApiErrorResponse) -> Self {
580        CompletionError::ProviderError(err.message)
581    }
582}
583
584impl TryFrom<CompletionResponse> for completion::CompletionResponse<CompletionResponse> {
585    type Error = CompletionError;
586
587    fn try_from(response: CompletionResponse) -> Result<Self, Self::Error> {
588        let (content, usage) = match &response {
589            CompletionResponse::Structured { choices, usage, .. } => {
590                let choice = choices.first().ok_or_else(|| {
591                    CompletionError::ResponseError("Response contained no choices".to_owned())
592                })?;
593
594                let usage = usage
595                    .as_ref()
596                    .map(|usage| completion::Usage {
597                        input_tokens: usage.prompt_tokens as u64,
598                        output_tokens: (usage.total_tokens - usage.prompt_tokens) as u64,
599                        total_tokens: usage.total_tokens as u64,
600                    })
601                    .unwrap_or_default();
602
603                // Convert RawMessage to message::Message
604                let message = message::Message::try_from(choice.message.clone())?;
605
606                let content = match message {
607                    Message::Assistant { content, .. } => {
608                        if content.is_empty() {
609                            return Err(CompletionError::ResponseError(
610                                "Response contained empty content".to_owned(),
611                            ));
612                        }
613
614                        // Log warning for unsupported content types
615                        for c in content.iter() {
616                            if !matches!(c, AssistantContent::Text(_)) {
617                                tracing::warn!(target: "rig",
618                                    "Unsupported content type encountered: {:?}. The Mira provider currently only supports text content", c
619                                );
620                            }
621                        }
622
623                        content.iter().map(|c| {
624                            match c {
625                                AssistantContent::Text(text) => Ok(completion::AssistantContent::text(&text.text)),
626                                other => Err(CompletionError::ResponseError(
627                                    format!("Unsupported content type: {other:?}. The Mira provider currently only supports text content")
628                                ))
629                            }
630                        }).collect::<Result<Vec<_>, _>>()?
631                    }
632                    Message::User { .. } => {
633                        tracing::warn!(target: "rig", "Received user message in response where assistant message was expected");
634                        return Err(CompletionError::ResponseError(
635                            "Received user message in response where assistant message was expected".to_owned()
636                        ));
637                    }
638                };
639
640                (content, usage)
641            }
642            CompletionResponse::Simple(text) => (
643                vec![completion::AssistantContent::text(text)],
644                completion::Usage::new(),
645            ),
646        };
647
648        let choice = OneOrMany::many(content).map_err(|_| {
649            CompletionError::ResponseError(
650                "Response contained no message or tool call (empty)".to_owned(),
651            )
652        })?;
653
654        Ok(completion::CompletionResponse {
655            choice,
656            usage,
657            raw_response: response,
658        })
659    }
660}
661
662#[derive(Clone, Debug, Deserialize, Serialize)]
663pub struct Usage {
664    pub prompt_tokens: usize,
665    pub total_tokens: usize,
666}
667
668impl std::fmt::Display for Usage {
669    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
670        write!(
671            f,
672            "Prompt tokens: {} Total tokens: {}",
673            self.prompt_tokens, self.total_tokens
674        )
675    }
676}
677
678impl From<Message> for serde_json::Value {
679    fn from(msg: Message) -> Self {
680        match msg {
681            Message::User { content } => {
682                let text = content
683                    .iter()
684                    .map(|c| match c {
685                        UserContent::Text(text) => &text.text,
686                        _ => "",
687                    })
688                    .collect::<Vec<_>>()
689                    .join("\n");
690                serde_json::json!({
691                    "role": "user",
692                    "content": text
693                })
694            }
695            Message::Assistant { content, .. } => {
696                let text = content
697                    .iter()
698                    .map(|c| match c {
699                        AssistantContent::Text(text) => &text.text,
700                        _ => "",
701                    })
702                    .collect::<Vec<_>>()
703                    .join("\n");
704                serde_json::json!({
705                    "role": "assistant",
706                    "content": text
707                })
708            }
709        }
710    }
711}
712
713impl TryFrom<serde_json::Value> for Message {
714    type Error = CompletionError;
715
716    fn try_from(value: serde_json::Value) -> Result<Self, Self::Error> {
717        let role = value["role"].as_str().ok_or_else(|| {
718            CompletionError::ResponseError("Message missing role field".to_owned())
719        })?;
720
721        // Handle both string and array content formats
722        let content = match value.get("content") {
723            Some(content) => match content {
724                serde_json::Value::String(s) => s.clone(),
725                serde_json::Value::Array(arr) => arr
726                    .iter()
727                    .filter_map(|c| {
728                        c.get("text")
729                            .and_then(|t| t.as_str())
730                            .map(|text| text.to_string())
731                    })
732                    .collect::<Vec<_>>()
733                    .join("\n"),
734                _ => {
735                    return Err(CompletionError::ResponseError(
736                        "Message content must be string or array".to_owned(),
737                    ));
738                }
739            },
740            None => {
741                return Err(CompletionError::ResponseError(
742                    "Message missing content field".to_owned(),
743                ));
744            }
745        };
746
747        match role {
748            "user" => Ok(Message::User {
749                content: OneOrMany::one(UserContent::Text(message::Text { text: content })),
750            }),
751            "assistant" => Ok(Message::Assistant {
752                id: None,
753                content: OneOrMany::one(AssistantContent::Text(message::Text { text: content })),
754            }),
755            _ => Err(CompletionError::ResponseError(format!(
756                "Unsupported message role: {role}"
757            ))),
758        }
759    }
760}
761
762#[cfg(test)]
763mod tests {
764    use super::*;
765    use crate::message::UserContent;
766    use serde_json::json;
767
768    #[test]
769    fn test_deserialize_message() {
770        // Test string content format
771        let assistant_message_json = json!({
772            "role": "assistant",
773            "content": "Hello there, how may I assist you today?"
774        });
775
776        let user_message_json = json!({
777            "role": "user",
778            "content": "What can you help me with?"
779        });
780
781        // Test array content format
782        let assistant_message_array_json = json!({
783            "role": "assistant",
784            "content": [{
785                "type": "text",
786                "text": "Hello there, how may I assist you today?"
787            }]
788        });
789
790        let assistant_message = Message::try_from(assistant_message_json).unwrap();
791        let user_message = Message::try_from(user_message_json).unwrap();
792        let assistant_message_array = Message::try_from(assistant_message_array_json).unwrap();
793
794        // Test string content format
795        match assistant_message {
796            Message::Assistant { content, .. } => {
797                assert_eq!(
798                    content.first(),
799                    AssistantContent::Text(message::Text {
800                        text: "Hello there, how may I assist you today?".to_string()
801                    })
802                );
803            }
804            _ => panic!("Expected assistant message"),
805        }
806
807        match user_message {
808            Message::User { content } => {
809                assert_eq!(
810                    content.first(),
811                    UserContent::Text(message::Text {
812                        text: "What can you help me with?".to_string()
813                    })
814                );
815            }
816            _ => panic!("Expected user message"),
817        }
818
819        // Test array content format
820        match assistant_message_array {
821            Message::Assistant { content, .. } => {
822                assert_eq!(
823                    content.first(),
824                    AssistantContent::Text(message::Text {
825                        text: "Hello there, how may I assist you today?".to_string()
826                    })
827                );
828            }
829            _ => panic!("Expected assistant message"),
830        }
831    }
832
833    #[test]
834    fn test_message_conversion() {
835        // Test converting from our Message type to Mira's format and back
836        let original_message = message::Message::User {
837            content: OneOrMany::one(message::UserContent::text("Hello")),
838        };
839
840        // Convert to Mira format
841        let mira_value: serde_json::Value = original_message.clone().into();
842
843        // Convert back to our Message type
844        let converted_message: Message = mira_value.try_into().unwrap();
845
846        assert_eq!(original_message, converted_message);
847    }
848
849    #[test]
850    fn test_completion_response_conversion() {
851        let mira_response = CompletionResponse::Structured {
852            id: "resp_123".to_string(),
853            object: "chat.completion".to_string(),
854            created: 1234567890,
855            model: "deepseek-r1".to_string(),
856            choices: vec![ChatChoice {
857                message: RawMessage {
858                    role: "assistant".to_string(),
859                    content: "Test response".to_string(),
860                },
861                finish_reason: Some("stop".to_string()),
862                index: Some(0),
863            }],
864            usage: Some(Usage {
865                prompt_tokens: 10,
866                total_tokens: 20,
867            }),
868        };
869
870        let completion_response: completion::CompletionResponse<CompletionResponse> =
871            mira_response.try_into().unwrap();
872
873        assert_eq!(
874            completion_response.choice.first(),
875            completion::AssistantContent::text("Test response")
876        );
877    }
878}