1use 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)]
177pub 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 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 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 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 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 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 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 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 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 _ => 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 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 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 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 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 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 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 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 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 let original_message = message::Message::User {
837 content: OneOrMany::one(message::UserContent::text("Hello")),
838 };
839
840 let mira_value: serde_json::Value = original_message.clone().into();
842
843 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}