mirror of
https://github.com/Alishahryar1/free-claude-code.git
synced 2026-06-02 06:13:46 +02:00
Add Mistral Provider
This commit is contained in:
+7
-1
@@ -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=""
|
||||
|
||||
@@ -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`.
|
||||
|
||||
|
||||
@@ -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
@@ -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",
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"]
|
||||
@@ -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),
|
||||
)
|
||||
@@ -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
|
||||
@@ -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
@@ -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.
|
||||
|
||||
@@ -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":
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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": "",
|
||||
|
||||
@@ -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()
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user