Add Mistral Provider

This commit is contained in:
Alishahryar1
2026-05-23 16:16:59 -07:00
parent b8c1f72865
commit 870576937f
18 changed files with 427 additions and 48 deletions
+7 -1
View File
@@ -6,6 +6,10 @@ NVIDIA_NIM_API_KEY=""
OPENROUTER_API_KEY=""
# Mistral La Plateforme Config (Experiment plan free tier rate limits; OpenAI-compatible at api.mistral.ai/v1)
MISTRAL_API_KEY=""
# DeepSeek Config (uses native Anthropic Messages at api.deepseek.com/anthropic)
DEEPSEEK_API_KEY=""
@@ -44,7 +48,7 @@ OLLAMA_BASE_URL="http://localhost:11434"
# All Claude model requests are mapped to these models, plain model is fallback
# Format: provider_type/model/name
# Valid providers: "nvidia_nim" | "open_router" | "deepseek" | "lmstudio" | "llamacpp" | "ollama" | "kimi" | "wafer" | "opencode" | "opencode_go" | "zai" | "fireworks"
# Valid providers: "nvidia_nim" | "open_router" | "mistral" | "deepseek" | "kimi" | "wafer" | "lmstudio" | "llamacpp" | "ollama" | "opencode" | "opencode_go" | "zai" | "fireworks"
MODEL_OPUS=
MODEL_SONNET=
MODEL_HAIKU=
@@ -55,6 +59,7 @@ MODEL="nvidia_nim/nvidia/nemotron-3-super-120b-a12b"
# provider even when MODEL/MODEL_* route to a different provider.
FCC_SMOKE_MODEL_NVIDIA_NIM=
FCC_SMOKE_MODEL_OPEN_ROUTER=
FCC_SMOKE_MODEL_MISTRAL=
FCC_SMOKE_MODEL_DEEPSEEK=
FCC_SMOKE_MODEL_LMSTUDIO=
FCC_SMOKE_MODEL_LLAMACPP=
@@ -84,6 +89,7 @@ ENABLE_MODEL_THINKING=true
# Per-provider proxy support: http and socks5, example: "http://username:password@host:port"
NVIDIA_NIM_PROXY=""
OPENROUTER_PROXY=""
MISTRAL_PROXY=""
LMSTUDIO_PROXY=""
LLAMACPP_PROXY=""
KIMI_PROXY=""
+39 -26
View File
@@ -37,7 +37,7 @@ Free Claude Code routes Anthropic Messages API traffic from Claude Code to any p
## What You Get
- Drop-in proxy for Claude Code's Anthropic API calls.
- Eleven provider backends: NVIDIA NIM, Kimi, Wafer, OpenRouter, DeepSeek, LM Studio, llama.cpp, Ollama, OpenCode Zen, OpenCode Go, and Z.ai.
- Twelve provider backends: NVIDIA NIM, OpenRouter, Mistral La Plateforme, DeepSeek, Kimi, Wafer, LM Studio, llama.cpp, Ollama, OpenCode Zen, OpenCode Go, and Z.ai.
- Per-model routing: send Opus, Sonnet, Haiku, and fallback traffic to different providers.
- Native Claude Code `/model` picker support through the proxy's `/v1/models` endpoint (Claude Code must opt in to Gateway model discovery; see [Model Picker](#model-picker)).
- Streaming, tool use, reasoning/thinking block handling, and local request optimizations.
@@ -123,7 +123,36 @@ Popular examples:
Browse models at [build.nvidia.com](https://build.nvidia.com/explore/discover).
### 2. [Kimi](https://platform.moonshot.ai/)
### 2. [OpenRouter](https://openrouter.ai/)
Get a key at [openrouter.ai/keys](https://openrouter.ai/keys).
In the Admin UI, paste it into `OPENROUTER_API_KEY`, then set `MODEL` to an OpenRouter slug such as `open_router/stepfun/step-3.5-flash:free`.
Browse [all models](https://openrouter.ai/models) or [free models](https://openrouter.ai/collections/free-models).
### 3. [Mistral La Plateforme](https://console.mistral.ai/)
[Mistral](https://mistral.ai) hosts an OpenAI-compatible Chat Completions API at `https://api.mistral.ai/v1`. Activate the **Experiment** plan on [console.mistral.ai](https://console.mistral.ai/) for free-tier API access with rate limits (upgrade for higher quotas).
In the Admin UI, paste your API key into `MISTRAL_API_KEY`, then set `MODEL` to a Mistral model slug such as `mistral/devstral-small-latest` or `mistral/mistral-small-latest`.
Popular examples:
- `mistral/devstral-small-latest`
- `mistral/mistral-small-latest`
Browse models at [Mistral documentation](https://docs.mistral.ai/).
### 4. [DeepSeek](https://platform.deepseek.com/)
Get a key at [platform.deepseek.com/api_keys](https://platform.deepseek.com/api_keys).
In the Admin UI, paste it into `DEEPSEEK_API_KEY`, then set `MODEL` to a DeepSeek slug such as `deepseek/deepseek-chat`.
This provider uses DeepSeek's Anthropic-compatible endpoint, not the OpenAI chat-completions endpoint.
### 5. [Kimi](https://platform.moonshot.ai/)
Get a key at [platform.moonshot.ai/console/api-keys](https://platform.moonshot.ai/console/api-keys).
@@ -131,7 +160,7 @@ In the Admin UI, paste it into `KIMI_API_KEY`, then set `MODEL` to a Kimi slug s
Browse models at [platform.moonshot.ai](https://platform.moonshot.ai).
### 3. [Wafer](https://wafer.ai/)
### 6. [Wafer](https://wafer.ai/)
Get a key from [wafer.ai](https://wafer.ai). In the Admin UI, paste it into `WAFER_API_KEY`, then set `MODEL` to a Wafer Pass model such as `wafer/DeepSeek-V4-Pro`.
@@ -144,29 +173,13 @@ Popular examples:
This provider uses Wafer's Anthropic-compatible endpoint at `https://pass.wafer.ai/v1/messages`.
### 4. [OpenRouter](https://openrouter.ai/)
Get a key at [openrouter.ai/keys](https://openrouter.ai/keys).
In the Admin UI, paste it into `OPENROUTER_API_KEY`, then set `MODEL` to an OpenRouter slug such as `open_router/stepfun/step-3.5-flash:free`.
Browse [all models](https://openrouter.ai/models) or [free models](https://openrouter.ai/collections/free-models).
### 5. [DeepSeek](https://platform.deepseek.com/)
Get a key at [platform.deepseek.com/api_keys](https://platform.deepseek.com/api_keys).
In the Admin UI, paste it into `DEEPSEEK_API_KEY`, then set `MODEL` to a DeepSeek slug such as `deepseek/deepseek-chat`.
This provider uses DeepSeek's Anthropic-compatible endpoint, not the OpenAI chat-completions endpoint.
### 6. [LM Studio](https://lmstudio.ai/)
### 7. [LM Studio](https://lmstudio.ai/)
Start LM Studio's local server and load a model. In the Admin UI, keep or update `LM_STUDIO_BASE_URL`, then set `MODEL` to the model identifier shown by LM Studio, prefixed with `lmstudio/`.
Prefer models with tool-use support for Claude Code workflows.
### 7. [llama.cpp](https://github.com/ggml-org/llama.cpp)
### 8. [llama.cpp](https://github.com/ggml-org/llama.cpp)
Start `llama-server` with an Anthropic-compatible `/v1/messages` endpoint and enough context for Claude Code requests.
@@ -174,7 +187,7 @@ In the Admin UI, keep or update `LLAMACPP_BASE_URL`, then set `MODEL` to the loc
For local coding models, context size matters. If llama.cpp returns HTTP 400 for normal Claude Code requests, increase `--ctx-size` and verify the model/server build supports the requested features.
### 8. [Ollama](https://ollama.com/)
### 9. [Ollama](https://ollama.com/)
Run Ollama and pull a model:
@@ -187,7 +200,7 @@ In the Admin UI, keep or update `OLLAMA_BASE_URL`, then set `MODEL` to the same
`OLLAMA_BASE_URL` is the Ollama server root; do not append `/v1`. Example model slugs include `ollama/llama3.1` and `ollama/llama3.1:8b`.
### 9. [OpenCode Zen](https://opencode.ai/)
### 10. [OpenCode Zen](https://opencode.ai/)
Get an API key at [opencode.ai/auth](https://opencode.ai/auth).
@@ -206,7 +219,7 @@ Popular examples:
Browse available models at [opencode.ai](https://opencode.ai).
### 10. [OpenCode Go](https://opencode.ai/)
### 11. [OpenCode Go](https://opencode.ai/)
Get an API key at [opencode.ai/auth](https://opencode.ai/auth) (same as OpenCode Zen).
@@ -220,7 +233,7 @@ Popular examples:
Browse available models at [opencode.ai](https://opencode.ai).
### 11. [Z.ai](https://z.ai/)
### 12. [Z.ai](https://z.ai/)
Get an API key at [Z.ai/manage-apikey/apikey-list](https://z.ai/manage-apikey/apikey-list).
@@ -235,7 +248,7 @@ Popular examples:
Browse models at [Z.ai](https://z.ai).
### 12. Mix Providers By Model Tier
### 13. Mix Providers By Model Tier
Each model tier can use a different provider by setting `MODEL_OPUS`, `MODEL_SONNET`, and `MODEL_HAIKU` in the Admin UI. Leave a tier blank to inherit `MODEL`.
+26
View File
@@ -134,6 +134,17 @@ FIELDS: tuple[ConfigFieldSpec, ...] = (
settings_attr="open_router_api_key",
secret=True,
),
ConfigFieldSpec(
"MISTRAL_API_KEY",
"Mistral API Key",
"providers",
"secret",
settings_attr="mistral_api_key",
secret=True,
description=(
"Mistral La Plateforme (api.mistral.ai); Experiment plan is free tier with rate limits."
),
),
ConfigFieldSpec(
"DEEPSEEK_API_KEY",
"DeepSeek API Key",
@@ -227,6 +238,15 @@ FIELDS: tuple[ConfigFieldSpec, ...] = (
secret=True,
advanced=True,
),
ConfigFieldSpec(
"MISTRAL_PROXY",
"Mistral Proxy",
"providers",
"secret",
settings_attr="mistral_proxy",
secret=True,
advanced=True,
),
ConfigFieldSpec(
"LMSTUDIO_PROXY",
"LM Studio Proxy",
@@ -699,6 +719,12 @@ FIELDS: tuple[ConfigFieldSpec, ...] = (
"smoke",
advanced=True,
),
ConfigFieldSpec(
"FCC_SMOKE_MODEL_MISTRAL",
"Smoke Mistral Model",
"smoke",
advanced=True,
),
ConfigFieldSpec(
"FCC_SMOKE_MODEL_DEEPSEEK",
"Smoke DeepSeek Model",
+31 -20
View File
@@ -21,6 +21,7 @@ DEEPSEEK_ANTHROPIC_DEFAULT_BASE = "https://api.deepseek.com/anthropic"
DEEPSEEK_DEFAULT_BASE = DEEPSEEK_ANTHROPIC_DEFAULT_BASE
FIREWORKS_DEFAULT_BASE = "https://api.fireworks.ai/inference/v1"
OPENROUTER_DEFAULT_BASE = "https://openrouter.ai/api/v1"
MISTRAL_DEFAULT_BASE = "https://api.mistral.ai/v1"
LMSTUDIO_DEFAULT_BASE = "http://localhost:1234/v1"
LLAMACPP_DEFAULT_BASE = "http://localhost:8080/v1"
OLLAMA_DEFAULT_BASE = "http://localhost:11434"
@@ -66,6 +67,16 @@ PROVIDER_CATALOG: dict[str, ProviderDescriptor] = {
proxy_attr="open_router_proxy",
capabilities=("chat", "streaming", "tools", "thinking", "native_anthropic"),
),
"mistral": ProviderDescriptor(
provider_id="mistral",
transport_type="openai_chat",
credential_env="MISTRAL_API_KEY",
credential_url="https://console.mistral.ai/",
credential_attr="mistral_api_key",
default_base_url=MISTRAL_DEFAULT_BASE,
proxy_attr="mistral_proxy",
capabilities=("chat", "streaming", "tools", "thinking", "rate_limit"),
),
"deepseek": ProviderDescriptor(
provider_id="deepseek",
transport_type="anthropic_messages",
@@ -75,6 +86,26 @@ PROVIDER_CATALOG: dict[str, ProviderDescriptor] = {
default_base_url=DEEPSEEK_ANTHROPIC_DEFAULT_BASE,
capabilities=("chat", "streaming", "tools", "thinking", "native_anthropic"),
),
"kimi": ProviderDescriptor(
provider_id="kimi",
transport_type="openai_chat",
credential_env="KIMI_API_KEY",
credential_url="https://platform.moonshot.cn/console/api-keys",
credential_attr="kimi_api_key",
default_base_url=KIMI_DEFAULT_BASE,
proxy_attr="kimi_proxy",
capabilities=("chat", "streaming", "tools"),
),
"wafer": ProviderDescriptor(
provider_id="wafer",
transport_type="anthropic_messages",
credential_env="WAFER_API_KEY",
credential_url="https://www.wafer.ai/pass",
credential_attr="wafer_api_key",
default_base_url=WAFER_DEFAULT_BASE,
proxy_attr="wafer_proxy",
capabilities=("chat", "streaming", "tools", "thinking", "native_anthropic"),
),
"lmstudio": ProviderDescriptor(
provider_id="lmstudio",
transport_type="anthropic_messages",
@@ -108,26 +139,6 @@ PROVIDER_CATALOG: dict[str, ProviderDescriptor] = {
"local",
),
),
"kimi": ProviderDescriptor(
provider_id="kimi",
transport_type="openai_chat",
credential_env="KIMI_API_KEY",
credential_url="https://platform.moonshot.cn/console/api-keys",
credential_attr="kimi_api_key",
default_base_url=KIMI_DEFAULT_BASE,
proxy_attr="kimi_proxy",
capabilities=("chat", "streaming", "tools"),
),
"wafer": ProviderDescriptor(
provider_id="wafer",
transport_type="anthropic_messages",
credential_env="WAFER_API_KEY",
credential_url="https://www.wafer.ai/pass",
credential_attr="wafer_api_key",
default_base_url=WAFER_DEFAULT_BASE,
proxy_attr="wafer_proxy",
capabilities=("chat", "streaming", "tools", "thinking", "native_anthropic"),
),
"opencode": ProviderDescriptor(
provider_id="opencode",
transport_type="openai_chat",
+4
View File
@@ -110,6 +110,9 @@ class Settings(BaseSettings):
# ==================== OpenRouter Config ====================
open_router_api_key: str = Field(default="", validation_alias="OPENROUTER_API_KEY")
# ==================== Mistral La Plateforme ====================
mistral_api_key: str = Field(default="", validation_alias="MISTRAL_API_KEY")
# ==================== DeepSeek Config ====================
deepseek_api_key: str = Field(default="", validation_alias="DEEPSEEK_API_KEY")
@@ -176,6 +179,7 @@ class Settings(BaseSettings):
# ==================== Per-Provider Proxy ====================
nvidia_nim_proxy: str = Field(default="", validation_alias="NVIDIA_NIM_PROXY")
open_router_proxy: str = Field(default="", validation_alias="OPENROUTER_PROXY")
mistral_proxy: str = Field(default="", validation_alias="MISTRAL_PROXY")
lmstudio_proxy: str = Field(default="", validation_alias="LMSTUDIO_PROXY")
llamacpp_proxy: str = Field(default="", validation_alias="LLAMACPP_PROXY")
kimi_proxy: str = Field(default="", validation_alias="KIMI_PROXY")
+2
View File
@@ -6,6 +6,7 @@ from config.provider_catalog import (
KIMI_DEFAULT_BASE,
LLAMACPP_DEFAULT_BASE,
LMSTUDIO_DEFAULT_BASE,
MISTRAL_DEFAULT_BASE,
NVIDIA_NIM_DEFAULT_BASE,
OLLAMA_DEFAULT_BASE,
OPENCODE_DEFAULT_BASE,
@@ -21,6 +22,7 @@ __all__ = (
"KIMI_DEFAULT_BASE",
"LLAMACPP_DEFAULT_BASE",
"LMSTUDIO_DEFAULT_BASE",
"MISTRAL_DEFAULT_BASE",
"NVIDIA_NIM_DEFAULT_BASE",
"OLLAMA_DEFAULT_BASE",
"OPENCODE_DEFAULT_BASE",
+7
View File
@@ -0,0 +1,7 @@
"""Mistral La Plateforme provider exports."""
from providers.defaults import MISTRAL_DEFAULT_BASE
from .client import MistralProvider
__all__ = ["MISTRAL_DEFAULT_BASE", "MistralProvider"]
+31
View File
@@ -0,0 +1,31 @@
"""Mistral La Plateforme provider implementation (OpenAI-compatible chat completions)."""
from __future__ import annotations
from typing import Any
from providers.base import ProviderConfig
from providers.defaults import MISTRAL_DEFAULT_BASE
from providers.openai_compat import OpenAIChatTransport
from .request import build_request_body
class MistralProvider(OpenAIChatTransport):
"""Mistral API using ``https://api.mistral.ai/v1/chat/completions``."""
def __init__(self, config: ProviderConfig):
super().__init__(
config,
provider_name="MISTRAL",
base_url=config.base_url or MISTRAL_DEFAULT_BASE,
api_key=config.api_key,
)
def _build_request_body(
self, request: Any, thinking_enabled: bool | None = None
) -> dict:
return build_request_body(
request,
thinking_enabled=self._is_thinking_enabled(request, thinking_enabled),
)
+37
View File
@@ -0,0 +1,37 @@
"""Request builder for Mistral La Plateforme (OpenAI-compatible chat completions)."""
from __future__ import annotations
from typing import Any
from loguru import logger
from core.anthropic import ReasoningReplayMode, build_base_request_body
from core.anthropic.conversion import OpenAIConversionError
from providers.exceptions import InvalidRequestError
def build_request_body(request_data: Any, *, thinking_enabled: bool) -> dict:
"""Build OpenAI-format request body from Anthropic request for Mistral."""
logger.debug(
"MISTRAL_REQUEST: conversion start model={} msgs={}",
getattr(request_data, "model", "?"),
len(getattr(request_data, "messages", [])),
)
try:
body = build_base_request_body(
request_data,
reasoning_replay=ReasoningReplayMode.REASONING_CONTENT
if thinking_enabled
else ReasoningReplayMode.DISABLED,
)
except OpenAIConversionError as exc:
raise InvalidRequestError(str(exc)) from exc
logger.debug(
"MISTRAL_REQUEST: conversion done model={} msgs={} tools={}",
body.get("model"),
len(body.get("messages", [])),
len(body.get("tools", [])),
)
return body
+7
View File
@@ -50,6 +50,12 @@ def _create_deepseek(config: ProviderConfig, _settings: Settings) -> BaseProvide
return DeepSeekProvider(config)
def _create_mistral(config: ProviderConfig, _settings: Settings) -> BaseProvider:
from providers.mistral import MistralProvider
return MistralProvider(config)
def _create_lmstudio(config: ProviderConfig, _settings: Settings) -> BaseProvider:
from providers.lmstudio import LMStudioProvider
@@ -107,6 +113,7 @@ def _create_fireworks(config: ProviderConfig, _settings: Settings) -> BaseProvid
PROVIDER_FACTORIES: dict[str, ProviderFactory] = {
"nvidia_nim": _create_nvidia_nim,
"open_router": _create_open_router,
"mistral": _create_mistral,
"deepseek": _create_deepseek,
"lmstudio": _create_lmstudio,
"llamacpp": _create_llamacpp,
+1 -1
View File
@@ -118,7 +118,7 @@ uv run pytest smoke/product -n 0 -s --tb=short
- `FCC_SMOKE_TARGETS`: comma-separated targets, or `all`.
- `FCC_SMOKE_PROVIDER_MATRIX`: comma-separated provider prefixes to require.
- `FCC_SMOKE_MODEL_NVIDIA_NIM`, `FCC_SMOKE_MODEL_OPEN_ROUTER`,
`FCC_SMOKE_MODEL_DEEPSEEK`, `FCC_SMOKE_MODEL_LMSTUDIO`,
`FCC_SMOKE_MODEL_MISTRAL`, `FCC_SMOKE_MODEL_DEEPSEEK`, `FCC_SMOKE_MODEL_LMSTUDIO`,
`FCC_SMOKE_MODEL_LLAMACPP`, `FCC_SMOKE_MODEL_OLLAMA`: optional per-provider
smoke model overrides. Values may include the provider prefix or just the model
name for that provider.
+3
View File
@@ -44,6 +44,7 @@ SECRET_KEY_PARTS = ("KEY", "TOKEN", "SECRET", "WEBHOOK", "AUTH")
PROVIDER_SMOKE_DEFAULT_MODELS: dict[str, str] = {
"nvidia_nim": "nvidia_nim/z-ai/glm4.7",
"open_router": "open_router/stepfun/step-3.5-flash:free",
"mistral": "mistral/devstral-small-latest",
"deepseek": "deepseek/deepseek-v4-pro",
"lmstudio": "lmstudio/local-model",
"llamacpp": "llamacpp/local-model",
@@ -224,6 +225,8 @@ class SmokeConfig:
return bool(self.settings.nvidia_nim_api_key.strip())
if provider == "open_router":
return bool(self.settings.open_router_api_key.strip())
if provider == "mistral":
return bool(self.settings.mistral_api_key.strip())
if provider == "deepseek":
return bool(self.settings.deepseek_api_key.strip())
if provider == "lmstudio":
+36
View File
@@ -18,6 +18,7 @@ from config.nim import NimSettings
from providers.deepseek import DeepSeekProvider
from providers.exceptions import ServiceUnavailableError, UnknownProviderTypeError
from providers.lmstudio import LMStudioProvider
from providers.mistral import MistralProvider
from providers.nvidia_nim import NvidiaNimProvider
from providers.ollama import OllamaProvider
from providers.open_router import OpenRouterProvider
@@ -35,12 +36,17 @@ def _make_mock_settings(**overrides):
mock.provider_rate_window = 60
mock.provider_max_concurrency = 5
mock.open_router_api_key = "test_openrouter_key"
mock.mistral_api_key = "test_mistral_key"
mock.deepseek_api_key = "test_deepseek_key"
mock.wafer_api_key = "test_wafer_key"
mock.opencode_api_key = "test_opencode_key"
mock.zai_api_key = "test_zai_key"
mock.lm_studio_base_url = "http://localhost:1234/v1"
mock.llamacpp_base_url = "http://localhost:8080/v1"
mock.ollama_base_url = "http://localhost:11434"
mock.nvidia_nim_proxy = ""
mock.open_router_proxy = ""
mock.mistral_proxy = ""
mock.lmstudio_proxy = ""
mock.llamacpp_proxy = ""
mock.kimi_proxy = ""
@@ -199,6 +205,19 @@ async def test_get_provider_deepseek_passes_enable_model_thinking():
assert provider._config.enable_thinking is False
@pytest.mark.asyncio
async def test_get_provider_mistral():
"""Test that provider_type=mistral returns MistralProvider."""
with patch("api.dependencies.get_settings") as mock_settings:
mock_settings.return_value = _make_mock_settings(provider_type="mistral")
provider = get_provider()
assert isinstance(provider, MistralProvider)
assert provider._base_url == "https://api.mistral.ai/v1"
assert provider._api_key == "test_mistral_key"
@pytest.mark.asyncio
async def test_get_provider_wafer():
"""Test that provider_type=wafer returns WaferProvider."""
@@ -331,6 +350,23 @@ async def test_get_provider_open_router_missing_api_key():
assert "openrouter.ai" in exc_info.value.detail
@pytest.mark.asyncio
async def test_get_provider_mistral_missing_api_key():
"""Mistral with empty API key raises HTTPException 503."""
with patch("api.dependencies.get_settings") as mock_settings:
mock_settings.return_value = _make_mock_settings(
provider_type="mistral",
mistral_api_key="",
)
with pytest.raises(HTTPException) as exc_info:
get_provider()
assert exc_info.value.status_code == 503
assert "MISTRAL_API_KEY" in exc_info.value.detail
assert "console.mistral.ai" in exc_info.value.detail
@pytest.mark.asyncio
async def test_get_provider_deepseek_missing_api_key():
"""DeepSeek with empty API key raises HTTPException 503."""
+7
View File
@@ -758,6 +758,9 @@ class TestPerModelMapping:
assert Settings.parse_provider_type("nvidia_nim/meta/llama") == "nvidia_nim"
assert Settings.parse_provider_type("open_router/deepseek/r1") == "open_router"
assert (
Settings.parse_provider_type("mistral/devstral-small-latest") == "mistral"
)
assert Settings.parse_provider_type("deepseek/deepseek-chat") == "deepseek"
assert Settings.parse_provider_type("lmstudio/qwen") == "lmstudio"
assert Settings.parse_provider_type("llamacpp/model") == "llamacpp"
@@ -769,6 +772,10 @@ class TestPerModelMapping:
from config.settings import Settings
assert Settings.parse_model_name("nvidia_nim/meta/llama") == "meta/llama"
assert (
Settings.parse_model_name("mistral/devstral-small-latest")
== "devstral-small-latest"
)
assert Settings.parse_model_name("deepseek/deepseek-chat") == "deepseek-chat"
assert Settings.parse_model_name("lmstudio/qwen") == "qwen"
assert Settings.parse_model_name("llamacpp/model") == "model"
+2
View File
@@ -8,6 +8,7 @@ from providers.base import BaseProvider
from providers.deepseek import DeepSeekProvider
from providers.llamacpp import LlamaCppProvider
from providers.lmstudio import LMStudioProvider
from providers.mistral import MistralProvider
from providers.nvidia_nim import NvidiaNimProvider
from providers.ollama import OllamaProvider
from providers.open_router import OpenRouterProvider
@@ -72,6 +73,7 @@ def test_provider_and_platform_registries_include_advertised_builtins() -> None:
provider_classes = {
"nvidia_nim": NvidiaNimProvider,
"open_router": OpenRouterProvider,
"mistral": MistralProvider,
"deepseek": DeepSeekProvider,
"lmstudio": LMStudioProvider,
"llamacpp": LlamaCppProvider,
+1
View File
@@ -25,6 +25,7 @@ def _settings(**overrides):
"model_haiku": None,
"nvidia_nim_api_key": "",
"open_router_api_key": "",
"mistral_api_key": "",
"deepseek_api_key": "",
"wafer_api_key": "",
"opencode_api_key": "",
+182
View File
@@ -0,0 +1,182 @@
"""Tests for Mistral La Plateforme provider."""
from contextlib import asynccontextmanager
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from providers.base import ProviderConfig
from providers.mistral import MISTRAL_DEFAULT_BASE, MistralProvider
class MockMessage:
def __init__(self, role, content):
self.role = role
self.content = content
class MockRequest:
def __init__(self, **kwargs):
self.model = "devstral-small-latest"
self.messages = [MockMessage("user", "Hello")]
self.max_tokens = 100
self.temperature = 0.5
self.top_p = 0.9
self.system = "System prompt"
self.stop_sequences = None
self.tools = []
self.thinking = MagicMock()
self.thinking.enabled = True
for key, value in kwargs.items():
setattr(self, key, value)
@pytest.fixture
def mistral_config():
return ProviderConfig(
api_key="test_mistral_key",
base_url=MISTRAL_DEFAULT_BASE,
rate_limit=10,
rate_window=60,
enable_thinking=True,
)
@pytest.fixture(autouse=True)
def mock_rate_limiter():
"""Mock the global rate limiter to prevent waiting."""
@asynccontextmanager
async def _slot():
yield
with patch("providers.openai_compat.GlobalRateLimiter") as mock:
instance = mock.get_scoped_instance.return_value
async def _passthrough(fn, *args, **kwargs):
return await fn(*args, **kwargs)
instance.execute_with_retry = AsyncMock(side_effect=_passthrough)
instance.concurrency_slot.side_effect = _slot
yield instance
@pytest.fixture
def mistral_provider(mistral_config):
return MistralProvider(mistral_config)
def test_init(mistral_config):
"""Test provider initialization."""
with patch("providers.openai_compat.AsyncOpenAI") as mock_openai:
provider = MistralProvider(mistral_config)
assert provider._api_key == "test_mistral_key"
assert provider._base_url == MISTRAL_DEFAULT_BASE
mock_openai.assert_called_once()
def test_default_base_url():
assert MISTRAL_DEFAULT_BASE == "https://api.mistral.ai/v1"
def test_build_request_body_basic(mistral_provider):
"""Basic request body conversion works for Mistral."""
req = MockRequest()
body = mistral_provider._build_request_body(req)
assert body["model"] == "devstral-small-latest"
assert body["messages"][0]["role"] == "system"
def test_build_request_body_global_disable_blocks_reasoning_mapping():
"""Global disable disables reasoning replay in the converter."""
provider = MistralProvider(
ProviderConfig(
api_key="test_mistral_key",
base_url=MISTRAL_DEFAULT_BASE,
rate_limit=10,
rate_window=60,
enable_thinking=False,
)
)
req = MockRequest()
body = provider._build_request_body(req)
roles = [m.get("role") for m in body.get("messages", [])]
assert "assistant_reasoning_content" not in roles
@pytest.mark.asyncio
async def test_stream_response_text(mistral_provider):
"""Text content deltas are emitted as text blocks."""
req = MockRequest()
mock_chunk = MagicMock()
mock_chunk.choices = [
MagicMock(
delta=MagicMock(
content="Hello back!",
reasoning_content=None,
tool_calls=None,
),
finish_reason="stop",
)
]
mock_chunk.usage = MagicMock(completion_tokens=5, prompt_tokens=10)
async def mock_stream():
yield mock_chunk
with patch.object(
mistral_provider._client.chat.completions, "create", new_callable=AsyncMock
) as mock_create:
mock_create.return_value = mock_stream()
events = [event async for event in mistral_provider.stream_response(req)]
assert any(
'"text_delta"' in event and "Hello back!" in event for event in events
)
@pytest.mark.asyncio
async def test_stream_response_reasoning_content(mistral_provider):
"""reasoning_content deltas are emitted as thinking blocks."""
req = MockRequest()
mock_chunk = MagicMock()
mock_chunk.choices = [
MagicMock(
delta=MagicMock(
content=None,
reasoning_content="Thinking...",
tool_calls=None,
),
finish_reason="stop",
)
]
mock_chunk.usage = MagicMock(completion_tokens=2, prompt_tokens=10)
async def mock_stream():
yield mock_chunk
with patch.object(
mistral_provider._client.chat.completions, "create", new_callable=AsyncMock
) as mock_create:
mock_create.return_value = mock_stream()
events = [event async for event in mistral_provider.stream_response(req)]
assert any(
'"thinking_delta"' in event and "Thinking..." in event for event in events
)
@pytest.mark.asyncio
async def test_cleanup(mistral_provider):
"""cleanup closes the OpenAI client."""
mistral_provider._client = AsyncMock()
await mistral_provider.cleanup()
mistral_provider._client.close.assert_called_once()
+4
View File
@@ -11,6 +11,7 @@ from providers.deepseek import DeepSeekProvider
from providers.exceptions import UnknownProviderTypeError
from providers.llamacpp import LlamaCppProvider
from providers.lmstudio import LMStudioProvider
from providers.mistral import MistralProvider
from providers.nvidia_nim import NvidiaNimProvider
from providers.ollama import OllamaProvider
from providers.open_router import OpenRouterProvider
@@ -31,6 +32,7 @@ def _make_settings(**overrides):
mock.provider_type = "nvidia_nim"
mock.nvidia_nim_api_key = "test_key"
mock.open_router_api_key = "test_openrouter_key"
mock.mistral_api_key = "test_mistral_key"
mock.deepseek_api_key = "test_deepseek_key"
mock.wafer_api_key = "test_wafer_key"
mock.opencode_api_key = "test_opencode_key"
@@ -42,6 +44,7 @@ def _make_settings(**overrides):
mock.open_router_proxy = ""
mock.lmstudio_proxy = ""
mock.llamacpp_proxy = ""
mock.mistral_proxy = ""
mock.kimi_proxy = ""
mock.wafer_proxy = ""
mock.opencode_proxy = ""
@@ -147,6 +150,7 @@ def test_create_provider_instantiates_each_builtin():
settings = _make_settings()
cases = {
"nvidia_nim": NvidiaNimProvider,
"mistral": MistralProvider,
"deepseek": DeepSeekProvider,
"lmstudio": LMStudioProvider,
"llamacpp": LlamaCppProvider,