From 556088177ed7e2ea217dfcba21f1415936dc4f48 Mon Sep 17 00:00:00 2001 From: bzp2010 Date: Sun, 3 May 2026 13:15:44 +0800 Subject: [PATCH] feat(provider): add mistral --- src/config/entities/providers-schema.json | 2 + src/config/entities/providers.rs | 8 ++++ src/gateway/providers/mistral.rs | 45 +++++++++++++++++++++++ src/gateway/providers/mod.rs | 11 ++++-- src/proxy/provider.rs | 22 ++++++++++- ui/src/i18n/locales/en.json | 1 + ui/src/i18n/locales/zh-CN.json | 1 + ui/src/lib/api/types.ts | 6 +++ 8 files changed, 92 insertions(+), 4 deletions(-) create mode 100644 src/gateway/providers/mistral.rs diff --git a/src/config/entities/providers-schema.json b/src/config/entities/providers-schema.json index b062635..2447cab 100644 --- a/src/config/entities/providers-schema.json +++ b/src/config/entities/providers-schema.json @@ -12,6 +12,7 @@ "deepseek", "gemini", "groq", + "mistral", "openai", "openrouter" ] @@ -52,6 +53,7 @@ "deepseek", "gemini", "groq", + "mistral", "openai", "openrouter" ] diff --git a/src/config/entities/providers.rs b/src/config/entities/providers.rs index bf6f2d6..4f0a9e5 100644 --- a/src/config/entities/providers.rs +++ b/src/config/entities/providers.rs @@ -34,6 +34,8 @@ pub enum ProviderConfig { Gemini(configs::GeminiProviderConfig), #[serde(rename = "groq")] Groq(configs::GroqProviderConfig), + #[serde(rename = "mistral")] + Mistral(configs::MistralProviderConfig), #[serde(rename = "openai")] OpenAI(configs::OpenAIProviderConfig), #[serde(rename = "openrouter")] @@ -49,6 +51,7 @@ impl ProviderConfig { Self::DeepSeek(_) => identifiers::DEEPSEEK, Self::Gemini(_) => identifiers::GEMINI, Self::Groq(_) => identifiers::GROQ, + Self::Mistral(_) => identifiers::MISTRAL, Self::OpenAI(_) => identifiers::OPENAI, Self::OpenRouter(_) => identifiers::OPENROUTER, } @@ -151,6 +154,11 @@ mod tests { "type": "groq", "config": { "api_key": "test_key" } }), true, None)] + #[case::mistral_ok(json!({ + "name": "mistral-primary", + "type": "mistral", + "config": { "api_key": "test_key" } + }), true, None)] #[case::missing_type(json!({ "name": "openai-primary", "config": { "api_key": "test_key" } diff --git a/src/gateway/providers/mistral.rs b/src/gateway/providers/mistral.rs new file mode 100644 index 0000000..3f472d6 --- /dev/null +++ b/src/gateway/providers/mistral.rs @@ -0,0 +1,45 @@ +use serde::{Deserialize, Serialize}; + +use crate::gateway::providers::macros::provider; + +/// Provider identifier string used to look up Mistral in the gateway registry. +pub const IDENTIFIER: &str = "mistral"; + +/// Configuration for a Mistral provider deployment. +#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] +pub struct MistralProviderConfig { + pub api_key: String, + + #[serde(skip_serializing_if = "Option::is_none")] + pub api_base: Option, +} + +provider!(Mistral { + display_name: "mistral", + base_url: "https://api.mistral.ai", + auth: bearer, + quirks: { + tool_args_may_be_object: true, + } +}); + +#[cfg(test)] +mod tests { + use super::Mistral; + use crate::gateway::traits::{ChatTransform, ProviderMeta}; + + #[test] + fn provider_macro_expands_correctly() { + let provider = Mistral; + + pretty_assertions::assert_eq!(provider.name(), "mistral"); + pretty_assertions::assert_eq!(provider.default_base_url(), "https://api.mistral.ai"); + + pretty_assertions::assert_eq!( + provider.build_url(provider.default_base_url(), "ignored"), + "https://api.mistral.ai/v1/chat/completions" + ); + + pretty_assertions::assert_eq!(provider.default_quirks().tool_args_may_be_object, true); + } +} diff --git a/src/gateway/providers/mod.rs b/src/gateway/providers/mod.rs index bf6b67a..f5a0d96 100644 --- a/src/gateway/providers/mod.rs +++ b/src/gateway/providers/mod.rs @@ -5,6 +5,7 @@ pub mod deepseek; pub mod gemini; pub mod groq; pub mod macros; +pub mod mistral; pub mod openai; pub mod openrouter; @@ -14,11 +15,12 @@ pub use bedrock::BedrockDef; pub use deepseek::DeepSeek; pub use gemini::GoogleDef; pub use groq::Groq; +pub use mistral::Mistral; pub use openai::OpenAIDef; pub use openrouter::OpenRouter; pub mod identifiers { - use super::{anthropic, azure, bedrock, deepseek, gemini, groq, openai, openrouter}; + use super::{anthropic, azure, bedrock, deepseek, gemini, groq, mistral, openai, openrouter}; pub const ANTHROPIC: &str = anthropic::IDENTIFIER; pub const AZURE: &str = azure::IDENTIFIER; @@ -26,6 +28,7 @@ pub mod identifiers { pub const DEEPSEEK: &str = deepseek::IDENTIFIER; pub const GEMINI: &str = gemini::IDENTIFIER; pub const GROQ: &str = groq::IDENTIFIER; + pub const MISTRAL: &str = mistral::IDENTIFIER; pub const OPENAI: &str = openai::IDENTIFIER; pub const OPENROUTER: &str = openrouter::IDENTIFIER; } @@ -34,8 +37,8 @@ pub mod configs { pub use super::{ anthropic::AnthropicProviderConfig, azure::AzureProviderConfig, bedrock::BedrockProviderConfig, deepseek::DeepSeekProviderConfig, - gemini::GeminiProviderConfig, groq::GroqProviderConfig, openai::OpenAIProviderConfig, - openrouter::OpenRouterProviderConfig, + gemini::GeminiProviderConfig, groq::GroqProviderConfig, mistral::MistralProviderConfig, + openai::OpenAIProviderConfig, openrouter::OpenRouterProviderConfig, }; } @@ -49,6 +52,7 @@ pub fn default_provider_registry() -> Result { .register(DeepSeek)? .register(GoogleDef)? .register(Groq)? + .register(Mistral)? .register(OpenAIDef)? .register(OpenRouter)?; Ok(builder.build()) @@ -68,6 +72,7 @@ mod tests { assert_eq!(registry.get("bedrock").unwrap().name(), "bedrock"); assert_eq!(registry.get("gemini").unwrap().name(), "gemini"); assert_eq!(registry.get("groq").unwrap().name(), "groq"); + assert_eq!(registry.get("mistral").unwrap().name(), "mistral"); assert_eq!(registry.get("deepseek").unwrap().name(), "deepseek"); assert_eq!(registry.get("openrouter").unwrap().name(), "openrouter"); assert!(registry.get("missing").is_none()); diff --git a/src/proxy/provider.rs b/src/proxy/provider.rs index 6ccc53b..7a10b41 100644 --- a/src/proxy/provider.rs +++ b/src/proxy/provider.rs @@ -53,6 +53,10 @@ fn provider_auth_and_base_url(config: &ProviderConfig) -> Result<(ProviderAuth, ProviderAuth::ApiKey(config.api_key.clone()), parse_base_url(config.api_base.as_deref())?, ), + ProviderConfig::Mistral(config) => ( + ProviderAuth::ApiKey(config.api_key.clone()), + parse_base_url(config.api_base.as_deref())?, + ), ProviderConfig::OpenAI(config) => ( ProviderAuth::ApiKey(config.api_key.clone()), parse_base_url(config.api_base.as_deref())?, @@ -147,7 +151,7 @@ mod tests { use crate::{ config::entities::providers::ProviderConfig, gateway::providers::configs::{ - AzureProviderConfig, BedrockProviderConfig, GroqProviderConfig, + AzureProviderConfig, BedrockProviderConfig, GroqProviderConfig, MistralProviderConfig, OpenRouterProviderConfig, }, }; @@ -223,6 +227,22 @@ mod tests { ); } + #[test] + fn provider_auth_and_base_url_returns_mistral_api_key_and_optional_base_url() { + let config = ProviderConfig::Mistral(MistralProviderConfig { + api_key: "mistral-key".into(), + api_base: Some("https://api.mistral.ai".into()), + }); + + let (auth, base_url_override) = provider_auth_and_base_url(&config).unwrap(); + + assert_eq!(auth.api_key_for("mistral").unwrap(), "mistral-key"); + assert_eq!( + base_url_override.as_ref().map(Url::as_str), + Some("https://api.mistral.ai/") + ); + } + #[test] fn provider_auth_and_base_url_returns_bedrock_static_credentials() { let config = ProviderConfig::Bedrock(BedrockProviderConfig { diff --git a/ui/src/i18n/locales/en.json b/ui/src/i18n/locales/en.json index 9ecab3f..b719521 100644 --- a/ui/src/i18n/locales/en.json +++ b/ui/src/i18n/locales/en.json @@ -150,6 +150,7 @@ "openai": "OpenAI", "openrouter": "OpenRouter", "groq": "Groq", + "mistral": "Mistral", "azure": "Azure OpenAI", "anthropic": "Anthropic", "gemini": "Gemini", diff --git a/ui/src/i18n/locales/zh-CN.json b/ui/src/i18n/locales/zh-CN.json index 5bebc80..e42203c 100644 --- a/ui/src/i18n/locales/zh-CN.json +++ b/ui/src/i18n/locales/zh-CN.json @@ -150,6 +150,7 @@ "openai": "OpenAI", "openrouter": "OpenRouter", "groq": "Groq", + "mistral": "Mistral", "azure": "Azure OpenAI", "anthropic": "Anthropic", "gemini": "Gemini", diff --git a/ui/src/lib/api/types.ts b/ui/src/lib/api/types.ts index af3e02c..c87be64 100644 --- a/ui/src/lib/api/types.ts +++ b/ui/src/lib/api/types.ts @@ -41,6 +41,7 @@ export const PROVIDER_TYPE_VARIANTS = [ 'openai', 'openrouter', 'groq', + 'mistral', 'azure', 'anthropic', 'gemini', @@ -105,6 +106,11 @@ export type Provider = type: 'groq'; config: ApiBaseProviderConfig; } + | { + name: string; + type: 'mistral'; + config: ApiBaseProviderConfig; + } | { name: string; type: 'bedrock';