rig_core/providers/mistral/
client.rs1use crate::{
2 client::{self, BearerAuth, DebugExt, Provider},
3 providers::mistral::MistralModelLister,
4};
5use serde::{Deserialize, Serialize};
6use std::fmt::Debug;
7
8const MISTRAL_API_BASE_URL: &str = "https://api.mistral.ai";
9
10#[derive(Debug, Default, Clone, Copy)]
11pub struct MistralExt;
12#[derive(Debug, Default, Clone, Copy)]
13pub struct MistralBuilder;
14
15type MistralApiKey = BearerAuth;
16
17pub type Client<H = reqwest::Client> = client::Client<MistralExt, H>;
18pub type ClientBuilder<H = crate::markers::Missing> =
19 client::ClientBuilder<MistralBuilder, MistralApiKey, H>;
20
21impl Provider for MistralExt {
22 type Builder = MistralBuilder;
23 const VERIFY_PATH: &'static str = "/v1/models";
28}
29
30impl crate::providers::openai::completion::OpenAICompatibleProvider for MistralExt {
31 const PROVIDER_NAME: &'static str = "mistral";
32
33 const REQUEST_ID_HEADER: Option<&'static str> = Some("mistral-correlation-id");
38
39 type StreamingUsage = Usage;
40
41 const EMITS_COMPLETE_SINGLE_CHUNK_TOOL_CALLS: bool = true;
42
43 const STREAM_INCLUDE_USAGE: bool = false;
46
47 type Response = super::CompletionResponse;
48
49 fn completion_path(&self, _model: &str) -> String {
52 "/v1/chat/completions".to_string()
53 }
54
55 fn finalize_request_body(
56 &self,
57 body: &mut serde_json::Value,
58 ) -> Result<(), crate::completion::CompletionError> {
59 let Some(map) = body.as_object_mut() else {
60 return Ok(());
61 };
62
63 if let Some(tool_choice) = map.get_mut("tool_choice")
65 && tool_choice.as_str() == Some("required")
66 {
67 *tool_choice = serde_json::Value::String("any".to_string());
68 }
69
70 let forces_a_tool_call = map
85 .get("tool_choice")
86 .is_some_and(|choice| !matches!(choice.as_str(), Some("auto" | "none")));
87 let has_tools = map
88 .get("tools")
89 .and_then(serde_json::Value::as_array)
90 .is_some_and(|tools| !tools.is_empty());
91 let has_structured_format = map
92 .get("response_format")
93 .and_then(|format| format.get("type"))
94 .and_then(serde_json::Value::as_str)
95 .is_some_and(|kind| matches!(kind, "json_schema" | "json_object"));
96 if forces_a_tool_call && has_tools && has_structured_format {
97 tracing::debug!(
98 "relaxing tool_choice to `auto`: Mistral rejects a forced tool choice \
99 alongside a response format"
100 );
101 map.insert(
102 "tool_choice".to_string(),
103 serde_json::Value::String("auto".to_string()),
104 );
105 }
106
107 if let Some(messages) = map
108 .get_mut("messages")
109 .and_then(serde_json::Value::as_array_mut)
110 {
111 for message in messages {
112 let Some(message) = message.as_object_mut() else {
113 continue;
114 };
115 let is_assistant =
116 message.get("role").and_then(serde_json::Value::as_str) == Some("assistant");
117
118 if let Some(content) = message.get_mut("content") {
123 super::completion::normalize_request_content(content)?;
124 }
125
126 if is_assistant {
127 if !message.contains_key("content") {
128 message.insert(
129 "content".to_string(),
130 serde_json::Value::String(String::new()),
131 );
132 }
133 message
135 .entry("prefix")
136 .or_insert(serde_json::Value::Bool(false));
137 message.remove("reasoning_content");
140 }
141 }
142 }
143
144 Ok(())
145 }
146}
147
148client::impl_capabilities!(
149 MistralExt,
150 completion = super::CompletionModel<H>,
151 embeddings = super::EmbeddingModel<H>,
152 transcription = super::TranscriptionModel<H>,
153 model_listing = MistralModelLister<H>,
154);
155
156impl DebugExt for MistralExt {}
157
158client::impl_default_provider_builder!(
159 MistralBuilder => MistralExt,
160 api_key = MistralApiKey,
161 base_url = MISTRAL_API_BASE_URL,
162);
163
164client::impl_provider_client!(Client, input = String, api_key_env = "MISTRAL_API_KEY");
165
166#[derive(Clone, Debug, Default, Deserialize, Serialize)]
172pub struct PromptTokensDetails {
173 #[serde(default)]
175 pub cached_tokens: u64,
176 #[serde(default)]
180 pub audio_tokens: u64,
181}
182
183#[derive(Clone, Debug, Default, Deserialize, Serialize)]
190pub struct Usage {
191 pub completion_tokens: usize,
192 pub prompt_tokens: usize,
193 pub total_tokens: usize,
194 #[serde(default, skip_serializing_if = "Option::is_none")]
202 pub service_tier: Option<String>,
203 #[serde(default, skip_serializing_if = "Option::is_none")]
205 pub prompt_audio_seconds: Option<u64>,
206 #[serde(default, skip_serializing_if = "Option::is_none")]
210 pub num_cached_tokens: Option<u64>,
211 #[serde(
213 default,
214 alias = "prompt_token_details",
215 skip_serializing_if = "Option::is_none"
216 )]
217 pub prompt_tokens_details: Option<PromptTokensDetails>,
218}
219
220impl Usage {
221 pub fn cached_tokens(&self) -> u64 {
225 self.prompt_tokens_details
226 .as_ref()
227 .map(|d| d.cached_tokens)
228 .or(self.num_cached_tokens)
229 .unwrap_or(0)
230 }
231
232 pub fn audio_tokens(&self) -> u64 {
234 self.prompt_tokens_details
235 .as_ref()
236 .map_or(0, |details| details.audio_tokens)
237 }
238
239 pub fn input_tokens(&self) -> u64 {
247 self.prompt_tokens as u64 + self.audio_tokens()
248 }
249}
250
251impl From<&Usage> for crate::completion::Usage {
252 fn from(usage: &Usage) -> Self {
253 crate::providers::internal::completion_usage(
254 usage.input_tokens(),
255 usage.completion_tokens as u64,
256 usage.total_tokens as u64,
257 usage.cached_tokens(),
258 )
259 }
260}
261
262impl From<Usage> for crate::completion::Usage {
263 fn from(usage: Usage) -> Self {
264 Self::from(&usage)
265 }
266}
267
268impl std::fmt::Display for Usage {
269 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
270 write!(
271 f,
272 "Prompt tokens: {} Total tokens: {}",
273 self.prompt_tokens, self.total_tokens
274 )
275 }
276}
277
278#[cfg(test)]
279mod tests {
280 use super::Usage;
281
282 #[test]
283 fn test_client_initialization() {
284 let _client =
285 crate::providers::mistral::Client::new("dummy-key").expect("Client::new() failed");
286 let builder: crate::providers::mistral::ClientBuilder =
287 crate::providers::mistral::Client::builder().api_key("dummy-key");
288 let _client_from_builder = builder.build().expect("Client::builder() failed");
289 }
290
291 #[test]
292 fn usage_retains_live_service_tier() {
293 let usage: Usage = serde_json::from_value(serde_json::json!({
294 "completion_tokens": 4,
295 "prompt_tokens": 20,
296 "total_tokens": 24,
297 "prompt_tokens_details": { "cached_tokens": 0 },
298 "service_tier": "standard"
299 }))
300 .expect("live Mistral usage should deserialize");
301
302 assert_eq!(usage.service_tier.as_deref(), Some("standard"));
303 assert_eq!(
304 serde_json::to_value(usage).expect("Mistral usage should serialize")["service_tier"],
305 "standard"
306 );
307 }
308}