Skip to main content

rig_core/providers/mistral/
client.rs

1use 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    // Mistral is strict about unknown parameters and reports usage on the
38    // final stream chunk without `stream_options`.
39    const STREAM_INCLUDE_USAGE: bool = false;
40
41    type Response = super::CompletionResponse;
42
43    // The client base URL is the bare host; other Mistral capabilities
44    // (embeddings, transcription, model listing) build their own v1 paths.
45    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        // Mistral spells the "must call some tool" mode `any`, not `required`.
58        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                // Mistral takes message `content` as a plain string.
76                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                    // `prefix` is part of Mistral's assistant message schema.
90                    message
91                        .entry("prefix")
92                        .or_insert(serde_json::Value::Bool(false));
93                    // Mistral rejects unknown assistant fields; hidden
94                    // reasoning cannot be echoed back.
95                    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    /// Create a new Mistral client from the `MISTRAL_API_KEY` environment variable.
144    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/// In-depth details on prompt tokens.
158///
159/// Mirrors Mistral's `PromptTokensDetails` schema. The Mistral API also exposes
160/// the same shape under the singular field name `prompt_token_details`; the
161/// `Usage` field accepts either form via `serde(alias = ...)`.
162#[derive(Clone, Debug, Default, Deserialize, Serialize)]
163pub struct PromptTokensDetails {
164    /// Number of tokens served from the prompt cache.
165    #[serde(default)]
166    pub cached_tokens: u64,
167}
168
169/// Token usage returned by Mistral's chat completions and embeddings endpoints.
170///
171/// See <https://docs.mistral.ai/api/> (`UsageInfo` schema). The three counts are
172/// always present; the remaining fields are populated by Mistral on a best-effort
173/// basis (e.g. cached-token information appears once a prompt is large enough to
174/// be cached).
175#[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    /// Duration in seconds of audio tokens in the prompt (audio-input models only).
181    #[serde(default, skip_serializing_if = "Option::is_none")]
182    pub prompt_audio_seconds: Option<u64>,
183    /// Total cached prompt tokens reported at the top level. Some Mistral
184    /// responses populate this in addition to (or instead of)
185    /// `prompt_tokens_details.cached_tokens`.
186    #[serde(default, skip_serializing_if = "Option::is_none")]
187    pub num_cached_tokens: Option<u64>,
188    /// In-depth breakdown of prompt token usage (currently only cached tokens).
189    #[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    /// Returns the number of cached prompt tokens, preferring the structured
199    /// `prompt_tokens_details.cached_tokens` field and falling back to the
200    /// top-level `num_cached_tokens`. Returns 0 when neither is present.
201    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}