rig_core/providers/mistral/
client.rs1use crate::{
2 client::{
3 self, BearerAuth, Capabilities, Capable, DebugExt, Nothing, Provider, ProviderBuilder,
4 ProviderClient,
5 },
6 http_client,
7 providers::mistral::MistralModelLister,
8};
9use serde::{Deserialize, Serialize};
10use std::fmt::Debug;
11
12const MISTRAL_API_BASE_URL: &str = "https://api.mistral.ai";
13
14#[derive(Debug, Default, Clone, Copy)]
15pub struct MistralExt;
16#[derive(Debug, Default, Clone, Copy)]
17pub struct MistralBuilder;
18
19type MistralApiKey = BearerAuth;
20
21pub type Client<H = reqwest::Client> = client::Client<MistralExt, H>;
22pub type ClientBuilder<H = crate::markers::Missing> =
23 client::ClientBuilder<MistralBuilder, MistralApiKey, H>;
24
25impl Provider for MistralExt {
26 type Builder = MistralBuilder;
27 const VERIFY_PATH: &'static str = "/models";
28}
29
30impl crate::providers::openai::completion::OpenAICompatibleProvider for MistralExt {
31 const PROVIDER_NAME: &'static str = "mistral";
32
33 type StreamingUsage = Usage;
34
35 const EMITS_COMPLETE_SINGLE_CHUNK_TOOL_CALLS: bool = true;
36
37 const STREAM_INCLUDE_USAGE: bool = false;
40
41 type Response = super::CompletionResponse;
42
43 fn completion_path(&self, _model: &str) -> String {
46 "/v1/chat/completions".to_string()
47 }
48
49 fn finalize_request_body(
50 &self,
51 body: &mut serde_json::Value,
52 ) -> Result<(), crate::completion::CompletionError> {
53 let Some(map) = body.as_object_mut() else {
54 return Ok(());
55 };
56
57 if let Some(tool_choice) = map.get_mut("tool_choice")
59 && tool_choice.as_str() == Some("required")
60 {
61 *tool_choice = serde_json::Value::String("any".to_string());
62 }
63
64 if let Some(messages) = map
65 .get_mut("messages")
66 .and_then(serde_json::Value::as_array_mut)
67 {
68 for message in messages {
69 let Some(message) = message.as_object_mut() else {
70 continue;
71 };
72 let is_assistant =
73 message.get("role").and_then(serde_json::Value::as_str) == Some("assistant");
74
75 if let Some(content) = message.get_mut("content") {
77 crate::providers::openai::completion::flatten_text_content_parts(
78 content, "", false,
79 );
80 }
81
82 if is_assistant {
83 if !message.contains_key("content") {
84 message.insert(
85 "content".to_string(),
86 serde_json::Value::String(String::new()),
87 );
88 }
89 message
91 .entry("prefix")
92 .or_insert(serde_json::Value::Bool(false));
93 message.remove("reasoning_content");
96 }
97 }
98 }
99
100 Ok(())
101 }
102}
103
104impl<H> Capabilities<H> for MistralExt {
105 type Completion = Capable<super::CompletionModel<H>>;
106 type Embeddings = Capable<super::EmbeddingModel<H>>;
107
108 type Transcription = Capable<super::TranscriptionModel<H>>;
109 type ModelListing = Capable<MistralModelLister<H>>;
110 #[cfg(feature = "image")]
111 type ImageGeneration = Nothing;
112
113 #[cfg(feature = "audio")]
114 type AudioGeneration = Nothing;
115 type Rerank = Nothing;
116}
117
118impl DebugExt for MistralExt {}
119
120impl ProviderBuilder for MistralBuilder {
121 type Extension<H>
122 = MistralExt
123 where
124 H: http_client::HttpClientExt;
125 type ApiKey = MistralApiKey;
126
127 const BASE_URL: &'static str = MISTRAL_API_BASE_URL;
128
129 fn build<H>(
130 _builder: &client::ClientBuilder<Self, Self::ApiKey, H>,
131 ) -> http_client::Result<Self::Extension<H>>
132 where
133 H: http_client::HttpClientExt,
134 {
135 Ok(MistralExt)
136 }
137}
138
139impl ProviderClient for Client {
140 type Input = String;
141 type Error = crate::client::ProviderClientError;
142
143 fn from_env() -> Result<Self, Self::Error>
145 where
146 Self: Sized,
147 {
148 let api_key = crate::client::required_env_var("MISTRAL_API_KEY")?;
149 Self::new(&api_key).map_err(Into::into)
150 }
151
152 fn from_val(input: Self::Input) -> Result<Self, Self::Error> {
153 Self::new(&input).map_err(Into::into)
154 }
155}
156
157#[derive(Clone, Debug, Default, Deserialize, Serialize)]
163pub struct PromptTokensDetails {
164 #[serde(default)]
166 pub cached_tokens: u64,
167}
168
169#[derive(Clone, Debug, Default, Deserialize, Serialize)]
176pub struct Usage {
177 pub completion_tokens: usize,
178 pub prompt_tokens: usize,
179 pub total_tokens: usize,
180 #[serde(default, skip_serializing_if = "Option::is_none")]
182 pub prompt_audio_seconds: Option<u64>,
183 #[serde(default, skip_serializing_if = "Option::is_none")]
187 pub num_cached_tokens: Option<u64>,
188 #[serde(
190 default,
191 alias = "prompt_token_details",
192 skip_serializing_if = "Option::is_none"
193 )]
194 pub prompt_tokens_details: Option<PromptTokensDetails>,
195}
196
197impl Usage {
198 pub fn cached_tokens(&self) -> u64 {
202 self.prompt_tokens_details
203 .as_ref()
204 .map(|d| d.cached_tokens)
205 .or(self.num_cached_tokens)
206 .unwrap_or(0)
207 }
208}
209
210impl std::fmt::Display for Usage {
211 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
212 write!(
213 f,
214 "Prompt tokens: {} Total tokens: {}",
215 self.prompt_tokens, self.total_tokens
216 )
217 }
218}
219
220#[derive(Debug, Deserialize)]
221pub struct ApiErrorResponse {
222 pub(crate) message: String,
223}
224
225#[derive(Debug, Deserialize)]
226#[serde(untagged)]
227pub(crate) enum ApiResponse<T> {
228 Ok(T),
229 Err(ApiErrorResponse),
230}
231
232#[cfg(test)]
233mod tests {
234 #[test]
235 fn test_client_initialization() {
236 let _client =
237 crate::providers::mistral::Client::new("dummy-key").expect("Client::new() failed");
238 let builder: crate::providers::mistral::ClientBuilder =
239 crate::providers::mistral::Client::builder().api_key("dummy-key");
240 let _client_from_builder = builder.build().expect("Client::builder() failed");
241 }
242}