Skip to main content

opendev_http/adapters/
mistral.rs

1//! Mistral AI adapter.
2//!
3//! Mistral's API is OpenAI-compatible (Chat Completions format) but with
4//! minor differences in tool calling structure. This adapter handles:
5//! - Passing requests mostly unchanged (OpenAI Chat Completions format)
6//! - Normalizing tool call responses (Mistral may omit `type` field)
7//! - Endpoint: `https://api.mistral.ai/v1/chat/completions`
8
9use serde_json::{Value, json};
10
11const DEFAULT_API_URL: &str = "https://api.mistral.ai/v1/chat/completions";
12
13/// Adapter for the Mistral AI Chat Completions API.
14///
15/// Mistral uses an OpenAI-compatible format but with slight differences
16/// in how tool calls are structured (e.g., `type` field may be absent
17/// in tool call responses, and `arguments` may be a JSON object instead
18/// of a string).
19#[derive(Debug, Clone)]
20pub struct MistralAdapter {
21    api_url: String,
22}
23
24impl MistralAdapter {
25    /// Create a new Mistral adapter with the default API URL.
26    pub fn new() -> Self {
27        Self {
28            api_url: DEFAULT_API_URL.to_string(),
29        }
30    }
31
32    /// Create with a custom API URL.
33    pub fn with_url(url: impl Into<String>) -> Self {
34        Self {
35            api_url: url.into(),
36        }
37    }
38
39    /// Normalize tool calls in the response.
40    ///
41    /// Mistral may return tool calls with:
42    /// - Missing `type` field (should be "function")
43    /// - `arguments` as a JSON object instead of a string
44    fn normalize_tool_calls(response: &mut Value) {
45        if let Some(choices) = response.get_mut("choices").and_then(|c| c.as_array_mut()) {
46            for choice in choices.iter_mut() {
47                if let Some(tool_calls) = choice
48                    .get_mut("message")
49                    .and_then(|m| m.get_mut("tool_calls"))
50                    .and_then(|tc| tc.as_array_mut())
51                {
52                    for tc in tool_calls.iter_mut() {
53                        // Ensure type field is present
54                        if tc.get("type").is_none() {
55                            tc["type"] = json!("function");
56                        }
57
58                        // If arguments is an object, serialize it to a string
59                        if let Some(func) = tc.get_mut("function")
60                            && let Some(args) = func.get("arguments")
61                            && (args.is_object() || args.is_array())
62                        {
63                            let args_str = serde_json::to_string(args).unwrap_or_default();
64                            func["arguments"] = Value::String(args_str);
65                        }
66                    }
67                }
68            }
69        }
70    }
71
72    /// Remove unsupported parameters from the request payload.
73    ///
74    /// Mistral does not support some OpenAI-specific parameters.
75    fn clean_request(payload: &mut Value) {
76        if let Some(obj) = payload.as_object_mut() {
77            obj.remove("logprobs");
78            obj.remove("top_logprobs");
79            obj.remove("n");
80            obj.remove("seed");
81        }
82    }
83}
84
85impl Default for MistralAdapter {
86    fn default() -> Self {
87        Self::new()
88    }
89}
90
91#[async_trait::async_trait]
92impl super::base::ProviderAdapter for MistralAdapter {
93    fn provider_name(&self) -> &str {
94        "mistral"
95    }
96
97    fn convert_request(&self, mut payload: Value) -> Value {
98        Self::clean_request(&mut payload);
99        // Strip internal reasoning effort field
100        payload
101            .as_object_mut()
102            .map(|obj| obj.remove("_reasoning_effort"));
103        payload
104    }
105
106    fn convert_response(&self, mut response: Value) -> Value {
107        Self::normalize_tool_calls(&mut response);
108        response
109    }
110
111    fn api_url(&self) -> &str {
112        &self.api_url
113    }
114}
115
116#[cfg(test)]
117#[path = "mistral_tests.rs"]
118mod tests;