mirror of
https://github.com/Alishahryar1/free-claude-code.git
synced 2026-07-10 00:14:16 +00:00
Add Codestral Provider
This commit is contained in:
parent
fbb1d6586d
commit
a4d7d76040
18 changed files with 369 additions and 23 deletions
|
|
@ -10,6 +10,10 @@ OPENROUTER_API_KEY=""
|
|||
MISTRAL_API_KEY=""
|
||||
|
||||
|
||||
# Mistral Codestral (separate key from La Plateforme; OpenAI-compatible at codestral.mistral.ai/v1)
|
||||
CODESTRAL_API_KEY=""
|
||||
|
||||
|
||||
# DeepSeek Config (uses native Anthropic Messages at api.deepseek.com/anthropic)
|
||||
DEEPSEEK_API_KEY=""
|
||||
|
||||
|
|
@ -60,7 +64,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" | "mistral" | "deepseek" | "kimi" | "wafer" | "lmstudio" | "llamacpp" | "ollama" | "opencode" | "opencode_go" | "zai" | "fireworks" | "gemini" | "groq" | "cerebras"
|
||||
# Valid providers: "nvidia_nim" | "open_router" | "mistral" | "mistral_codestral" | "deepseek" | "kimi" | "wafer" | "lmstudio" | "llamacpp" | "ollama" | "opencode" | "opencode_go" | "zai" | "fireworks" | "gemini" | "groq" | "cerebras"
|
||||
MODEL_OPUS=
|
||||
MODEL_SONNET=
|
||||
MODEL_HAIKU=
|
||||
|
|
@ -72,6 +76,7 @@ MODEL="nvidia_nim/nvidia/nemotron-3-super-120b-a12b"
|
|||
FCC_SMOKE_MODEL_NVIDIA_NIM=
|
||||
FCC_SMOKE_MODEL_OPEN_ROUTER=
|
||||
FCC_SMOKE_MODEL_MISTRAL=
|
||||
FCC_SMOKE_MODEL_MISTRAL_CODESTRAL=
|
||||
FCC_SMOKE_MODEL_DEEPSEEK=
|
||||
FCC_SMOKE_MODEL_LMSTUDIO=
|
||||
FCC_SMOKE_MODEL_LLAMACPP=
|
||||
|
|
@ -105,6 +110,7 @@ ENABLE_MODEL_THINKING=true
|
|||
NVIDIA_NIM_PROXY=""
|
||||
OPENROUTER_PROXY=""
|
||||
MISTRAL_PROXY=""
|
||||
CODESTRAL_PROXY=""
|
||||
LMSTUDIO_PROXY=""
|
||||
LLAMACPP_PROXY=""
|
||||
KIMI_PROXY=""
|
||||
|
|
|
|||
38
README.md
38
README.md
|
|
@ -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.
|
||||
- Sixteen provider backends: NVIDIA NIM, OpenRouter, Mistral La Plateforme, DeepSeek, Kimi, Wafer, LM Studio, llama.cpp, Ollama, OpenCode Zen, OpenCode Go, Z.ai, Fireworks AI, Google AI Studio (Gemini), Groq, and Cerebras Inference.
|
||||
- Seventeen provider backends: NVIDIA NIM, OpenRouter, Mistral La Plateforme, Mistral Codestral, DeepSeek, Kimi, Wafer, LM Studio, llama.cpp, Ollama, OpenCode Zen, OpenCode Go, Z.ai, Fireworks AI, Google AI Studio (Gemini), Groq, and Cerebras Inference.
|
||||
- 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.
|
||||
|
|
@ -144,7 +144,15 @@ Popular examples:
|
|||
|
||||
Browse models at [Mistral documentation](https://docs.mistral.ai/).
|
||||
|
||||
### 4. [DeepSeek](https://platform.deepseek.com/)
|
||||
### 4. [Mistral Codestral](https://console.mistral.ai/)
|
||||
|
||||
Mistral’s **Codestral** gateway uses a **separate API key** from La Plateforme: provision `CODESTRAL_API_KEY`, then route with the `mistral_codestral/` prefix. The default upstream is **`https://codestral.mistral.ai/v1`** (OpenAI-compatible Chat Completions; same request shaping as the `mistral` provider). See Mistral’s [coding / FIM domains](https://docs.mistral.ai/mistral-vibe/using-fim-api); the curated [free LLM API list](https://github.com/cheahjs/free-llm-api-resources#mistral-codestral) summarizes typical Codestral access terms.
|
||||
|
||||
Popular examples:
|
||||
|
||||
- `mistral_codestral/codestral-latest`
|
||||
|
||||
### 5. [DeepSeek](https://platform.deepseek.com/)
|
||||
|
||||
Get a key at [platform.deepseek.com/api_keys](https://platform.deepseek.com/api_keys).
|
||||
|
||||
|
|
@ -152,7 +160,7 @@ In the Admin UI, paste it into `DEEPSEEK_API_KEY`, then set `MODEL` to a DeepSee
|
|||
|
||||
This provider uses DeepSeek's Anthropic-compatible endpoint, not the OpenAI chat-completions endpoint.
|
||||
|
||||
### 5. [Kimi](https://platform.moonshot.ai/)
|
||||
### 6. [Kimi](https://platform.moonshot.ai/)
|
||||
|
||||
Get a key at [platform.moonshot.ai/console/api-keys](https://platform.moonshot.ai/console/api-keys).
|
||||
|
||||
|
|
@ -162,7 +170,7 @@ This provider calls Kimi's **Anthropic-compatible** Messages API (`https://api.m
|
|||
|
||||
Browse models at [platform.moonshot.ai](https://platform.moonshot.ai).
|
||||
|
||||
### 6. [Wafer](https://wafer.ai/)
|
||||
### 7. [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`.
|
||||
|
||||
|
|
@ -175,13 +183,13 @@ Popular examples:
|
|||
|
||||
This provider uses Wafer's Anthropic-compatible endpoint at `https://pass.wafer.ai/v1/messages`.
|
||||
|
||||
### 7. [LM Studio](https://lmstudio.ai/)
|
||||
### 8. [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.
|
||||
|
||||
### 8. [llama.cpp](https://github.com/ggml-org/llama.cpp)
|
||||
### 9. [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.
|
||||
|
||||
|
|
@ -189,7 +197,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.
|
||||
|
||||
### 9. [Ollama](https://ollama.com/)
|
||||
### 10. [Ollama](https://ollama.com/)
|
||||
|
||||
Run Ollama and pull a model:
|
||||
|
||||
|
|
@ -202,7 +210,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`.
|
||||
|
||||
### 10. [OpenCode Zen](https://opencode.ai/)
|
||||
### 11. [OpenCode Zen](https://opencode.ai/)
|
||||
|
||||
Get an API key at [opencode.ai/auth](https://opencode.ai/auth).
|
||||
|
||||
|
|
@ -221,7 +229,7 @@ Popular examples:
|
|||
|
||||
Browse available models at [opencode.ai](https://opencode.ai).
|
||||
|
||||
### 11. [OpenCode Go](https://opencode.ai/)
|
||||
### 12. [OpenCode Go](https://opencode.ai/)
|
||||
|
||||
Get an API key at [opencode.ai/auth](https://opencode.ai/auth) (same as OpenCode Zen).
|
||||
|
||||
|
|
@ -235,7 +243,7 @@ Popular examples:
|
|||
|
||||
Browse available models at [opencode.ai](https://opencode.ai).
|
||||
|
||||
### 12. [Z.ai](https://z.ai/)
|
||||
### 13. [Z.ai](https://z.ai/)
|
||||
|
||||
Get an API key at [Z.ai/manage-apikey/apikey-list](https://z.ai/manage-apikey/apikey-list).
|
||||
|
||||
|
|
@ -250,7 +258,7 @@ Popular examples:
|
|||
|
||||
Browse models at [Z.ai](https://z.ai).
|
||||
|
||||
### 13. [Fireworks AI](https://fireworks.ai/)
|
||||
### 14. [Fireworks AI](https://fireworks.ai/)
|
||||
|
||||
Get an API key at [fireworks.ai/account/api-keys](https://fireworks.ai/account/api-keys).
|
||||
|
||||
|
|
@ -260,7 +268,7 @@ Fireworks exposes an **Anthropic-compatible** Messages API at `https://api.firew
|
|||
|
||||
Browse models at [fireworks.ai/models](https://fireworks.ai/models).
|
||||
|
||||
### 14. [Google AI Studio (Gemini)](https://aistudio.google.com/)
|
||||
### 15. [Google AI Studio (Gemini)](https://aistudio.google.com/)
|
||||
|
||||
Get a Gemini API key at [Google AI Studio](https://aistudio.google.com/apikey) (see Google's [Gemini OpenAI compatibility](https://ai.google.dev/gemini-api/docs/openai) docs).
|
||||
|
||||
|
|
@ -273,7 +281,7 @@ Popular examples:
|
|||
- `gemini/gemini-2.5-flash`
|
||||
- `gemini/gemini-3.1-flash-lite`
|
||||
|
||||
### 15. [Groq](https://console.groq.com/)
|
||||
### 16. [Groq](https://console.groq.com/)
|
||||
|
||||
Get an API key at [console.groq.com/keys](https://console.groq.com/keys).
|
||||
|
||||
|
|
@ -285,7 +293,7 @@ Reasoning-heavy models expose extra knobs documented under [Groq reasoning](http
|
|||
|
||||
Browse models at [console.groq.com/docs/models](https://console.groq.com/docs/models).
|
||||
|
||||
### 16. [Cerebras Inference](https://inference-docs.cerebras.ai/quickstart)
|
||||
### 17. [Cerebras Inference](https://inference-docs.cerebras.ai/quickstart)
|
||||
|
||||
Sign up and create an API key in the [Cerebras Cloud Console](https://cloud.cerebras.ai) (see [Quickstart](https://inference-docs.cerebras.ai/quickstart)).
|
||||
|
||||
|
|
@ -293,7 +301,7 @@ In the Admin UI, set `CEREBRAS_API_KEY`, then route with `MODEL` such as `cerebr
|
|||
|
||||
Cerebras exposes an OpenAI-compatible API at `https://api.cerebras.ai/v1` ([OpenAI compatibility](https://inference-docs.cerebras.ai/resources/openai)). Non-standard request fields should go in `extra_body` when using the OpenAI client; see the same page. For reasoning models and parameters, see [Reasoning](https://inference-docs.cerebras.ai/capabilities/reasoning). This proxy follows other OpenAI-compat adapters for thinking via `reasoning_content` when Claude-style thinking is enabled.
|
||||
|
||||
### 17. Mix Providers By Model Tier
|
||||
### 18. 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`.
|
||||
|
||||
|
|
|
|||
|
|
@ -145,6 +145,18 @@ FIELDS: tuple[ConfigFieldSpec, ...] = (
|
|||
"Mistral La Plateforme (api.mistral.ai); Experiment plan is free tier with rate limits."
|
||||
),
|
||||
),
|
||||
ConfigFieldSpec(
|
||||
"CODESTRAL_API_KEY",
|
||||
"Codestral API Key",
|
||||
"providers",
|
||||
"secret",
|
||||
settings_attr="codestral_api_key",
|
||||
secret=True,
|
||||
description=(
|
||||
"Mistral Codestral endpoint (codestral.mistral.ai); distinct from Mistral "
|
||||
"La Plateforme ``MISTRAL_API_KEY``. See Mistral docs for coding/FIM domains."
|
||||
),
|
||||
),
|
||||
ConfigFieldSpec(
|
||||
"DEEPSEEK_API_KEY",
|
||||
"DeepSeek API Key",
|
||||
|
|
@ -287,6 +299,15 @@ FIELDS: tuple[ConfigFieldSpec, ...] = (
|
|||
secret=True,
|
||||
advanced=True,
|
||||
),
|
||||
ConfigFieldSpec(
|
||||
"CODESTRAL_PROXY",
|
||||
"Codestral Proxy",
|
||||
"providers",
|
||||
"secret",
|
||||
settings_attr="codestral_proxy",
|
||||
secret=True,
|
||||
advanced=True,
|
||||
),
|
||||
ConfigFieldSpec(
|
||||
"LMSTUDIO_PROXY",
|
||||
"LM Studio Proxy",
|
||||
|
|
@ -792,6 +813,12 @@ FIELDS: tuple[ConfigFieldSpec, ...] = (
|
|||
"smoke",
|
||||
advanced=True,
|
||||
),
|
||||
ConfigFieldSpec(
|
||||
"FCC_SMOKE_MODEL_MISTRAL_CODESTRAL",
|
||||
"Smoke Mistral Codestral Model",
|
||||
"smoke",
|
||||
advanced=True,
|
||||
),
|
||||
ConfigFieldSpec(
|
||||
"FCC_SMOKE_MODEL_DEEPSEEK",
|
||||
"Smoke DeepSeek Model",
|
||||
|
|
|
|||
|
|
@ -61,6 +61,7 @@ function providerName(providerId) {
|
|||
const names = {
|
||||
nvidia_nim: "NVIDIA NIM",
|
||||
open_router: "OpenRouter",
|
||||
mistral_codestral: "Mistral Codestral",
|
||||
deepseek: "DeepSeek",
|
||||
lmstudio: "LM Studio",
|
||||
llamacpp: "llama.cpp",
|
||||
|
|
|
|||
|
|
@ -23,6 +23,8 @@ 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"
|
||||
# Codestral IDE/personal endpoint (distinct from La Plateforme ``api.mistral.ai`` keys).
|
||||
CODESTRAL_DEFAULT_BASE = "https://codestral.mistral.ai/v1"
|
||||
LMSTUDIO_DEFAULT_BASE = "http://localhost:1234/v1"
|
||||
LLAMACPP_DEFAULT_BASE = "http://localhost:8080/v1"
|
||||
OLLAMA_DEFAULT_BASE = "http://localhost:11434"
|
||||
|
|
@ -83,6 +85,16 @@ PROVIDER_CATALOG: dict[str, ProviderDescriptor] = {
|
|||
proxy_attr="mistral_proxy",
|
||||
capabilities=("chat", "streaming", "tools", "thinking", "rate_limit"),
|
||||
),
|
||||
"mistral_codestral": ProviderDescriptor(
|
||||
provider_id="mistral_codestral",
|
||||
transport_type="openai_chat",
|
||||
credential_env="CODESTRAL_API_KEY",
|
||||
credential_url="https://console.mistral.ai/",
|
||||
credential_attr="codestral_api_key",
|
||||
default_base_url=CODESTRAL_DEFAULT_BASE,
|
||||
proxy_attr="codestral_proxy",
|
||||
capabilities=("chat", "streaming", "tools", "thinking", "rate_limit"),
|
||||
),
|
||||
"deepseek": ProviderDescriptor(
|
||||
provider_id="deepseek",
|
||||
transport_type="anthropic_messages",
|
||||
|
|
|
|||
|
|
@ -113,6 +113,9 @@ class Settings(BaseSettings):
|
|||
# ==================== Mistral La Plateforme ====================
|
||||
mistral_api_key: str = Field(default="", validation_alias="MISTRAL_API_KEY")
|
||||
|
||||
# ==================== Mistral Codestral (codestral.mistral.ai) ====================
|
||||
codestral_api_key: str = Field(default="", validation_alias="CODESTRAL_API_KEY")
|
||||
|
||||
# ==================== DeepSeek Config ====================
|
||||
deepseek_api_key: str = Field(default="", validation_alias="DEEPSEEK_API_KEY")
|
||||
|
||||
|
|
@ -189,6 +192,7 @@ class Settings(BaseSettings):
|
|||
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")
|
||||
codestral_proxy: str = Field(default="", validation_alias="CODESTRAL_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")
|
||||
|
|
|
|||
7
providers/codestral/__init__.py
Normal file
7
providers/codestral/__init__.py
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
"""Mistral Codestral provider (codestral.mistral.ai) exports."""
|
||||
|
||||
from providers.defaults import CODESTRAL_DEFAULT_BASE
|
||||
|
||||
from .client import CodestralProvider
|
||||
|
||||
__all__ = ["CODESTRAL_DEFAULT_BASE", "CodestralProvider"]
|
||||
34
providers/codestral/client.py
Normal file
34
providers/codestral/client.py
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
"""Mistral Codestral provider (OpenAI-compatible chat on codestral.mistral.ai)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from providers.base import ProviderConfig
|
||||
from providers.defaults import CODESTRAL_DEFAULT_BASE
|
||||
from providers.mistral.request import build_request_body
|
||||
from providers.openai_compat import OpenAIChatTransport
|
||||
|
||||
|
||||
class CodestralProvider(OpenAIChatTransport):
|
||||
"""Codestral host using ``https://codestral.mistral.ai/v1/chat/completions``.
|
||||
|
||||
Uses a separate Codestral API key from La Plateforme (``MISTRAL_API_KEY``).
|
||||
Request shaping matches Mistral La Plateforme (shared ``build_request_body``).
|
||||
"""
|
||||
|
||||
def __init__(self, config: ProviderConfig):
|
||||
super().__init__(
|
||||
config,
|
||||
provider_name="CODESTRAL",
|
||||
base_url=config.base_url or CODESTRAL_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),
|
||||
)
|
||||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
from config.provider_catalog import (
|
||||
CEREBRAS_DEFAULT_BASE,
|
||||
CODESTRAL_DEFAULT_BASE,
|
||||
DEEPSEEK_ANTHROPIC_DEFAULT_BASE,
|
||||
DEEPSEEK_DEFAULT_BASE,
|
||||
GEMINI_DEFAULT_BASE,
|
||||
|
|
@ -21,6 +22,7 @@ from config.provider_catalog import (
|
|||
|
||||
__all__ = (
|
||||
"CEREBRAS_DEFAULT_BASE",
|
||||
"CODESTRAL_DEFAULT_BASE",
|
||||
"DEEPSEEK_ANTHROPIC_DEFAULT_BASE",
|
||||
"DEEPSEEK_DEFAULT_BASE",
|
||||
"GEMINI_DEFAULT_BASE",
|
||||
|
|
|
|||
|
|
@ -44,18 +44,26 @@ def _create_open_router(config: ProviderConfig, _settings: Settings) -> BaseProv
|
|||
return OpenRouterProvider(config)
|
||||
|
||||
|
||||
def _create_deepseek(config: ProviderConfig, _settings: Settings) -> BaseProvider:
|
||||
from providers.deepseek import DeepSeekProvider
|
||||
|
||||
return DeepSeekProvider(config)
|
||||
|
||||
|
||||
def _create_mistral(config: ProviderConfig, _settings: Settings) -> BaseProvider:
|
||||
from providers.mistral import MistralProvider
|
||||
|
||||
return MistralProvider(config)
|
||||
|
||||
|
||||
def _create_mistral_codestral(
|
||||
config: ProviderConfig, _settings: Settings
|
||||
) -> BaseProvider:
|
||||
from providers.codestral import CodestralProvider
|
||||
|
||||
return CodestralProvider(config)
|
||||
|
||||
|
||||
def _create_deepseek(config: ProviderConfig, _settings: Settings) -> BaseProvider:
|
||||
from providers.deepseek import DeepSeekProvider
|
||||
|
||||
return DeepSeekProvider(config)
|
||||
|
||||
|
||||
def _create_lmstudio(config: ProviderConfig, _settings: Settings) -> BaseProvider:
|
||||
from providers.lmstudio import LMStudioProvider
|
||||
|
||||
|
|
@ -132,6 +140,7 @@ PROVIDER_FACTORIES: dict[str, ProviderFactory] = {
|
|||
"nvidia_nim": _create_nvidia_nim,
|
||||
"open_router": _create_open_router,
|
||||
"mistral": _create_mistral,
|
||||
"mistral_codestral": _create_mistral_codestral,
|
||||
"deepseek": _create_deepseek,
|
||||
"lmstudio": _create_lmstudio,
|
||||
"llamacpp": _create_llamacpp,
|
||||
|
|
|
|||
|
|
@ -118,7 +118,8 @@ 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_MISTRAL`, `FCC_SMOKE_MODEL_DEEPSEEK`, `FCC_SMOKE_MODEL_KIMI`,
|
||||
`FCC_SMOKE_MODEL_MISTRAL`, `FCC_SMOKE_MODEL_MISTRAL_CODESTRAL`,
|
||||
`FCC_SMOKE_MODEL_DEEPSEEK`, `FCC_SMOKE_MODEL_KIMI`,
|
||||
`FCC_SMOKE_MODEL_WAFER`, `FCC_SMOKE_MODEL_OPENCODE`, `FCC_SMOKE_MODEL_OPENCODE_GO`,
|
||||
`FCC_SMOKE_MODEL_ZAI`, `FCC_SMOKE_MODEL_FIREWORKS`, `FCC_SMOKE_MODEL_GEMINI`,
|
||||
`FCC_SMOKE_MODEL_GROQ`, `FCC_SMOKE_MODEL_CEREBRAS`,
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ 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",
|
||||
"mistral_codestral": "mistral_codestral/codestral-latest",
|
||||
"deepseek": "deepseek/deepseek-v4-pro",
|
||||
"lmstudio": "lmstudio/local-model",
|
||||
"llamacpp": "llamacpp/local-model",
|
||||
|
|
@ -230,6 +231,8 @@ class SmokeConfig:
|
|||
return bool(self.settings.open_router_api_key.strip())
|
||||
if provider == "mistral":
|
||||
return bool(self.settings.mistral_api_key.strip())
|
||||
if provider == "mistral_codestral":
|
||||
return bool(self.settings.codestral_api_key.strip())
|
||||
if provider == "deepseek":
|
||||
return bool(self.settings.deepseek_api_key.strip())
|
||||
if provider == "kimi":
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ from api.dependencies import (
|
|||
)
|
||||
from config.nim import NimSettings
|
||||
from providers.cerebras import CerebrasProvider
|
||||
from providers.codestral import CodestralProvider
|
||||
from providers.deepseek import DeepSeekProvider
|
||||
from providers.exceptions import ServiceUnavailableError, UnknownProviderTypeError
|
||||
from providers.gemini import GeminiProvider
|
||||
|
|
@ -40,6 +41,7 @@ def _make_mock_settings(**overrides):
|
|||
mock.provider_max_concurrency = 5
|
||||
mock.open_router_api_key = "test_openrouter_key"
|
||||
mock.mistral_api_key = "test_mistral_key"
|
||||
mock.codestral_api_key = "test_codestral_key"
|
||||
mock.deepseek_api_key = "test_deepseek_key"
|
||||
mock.wafer_api_key = "test_wafer_key"
|
||||
mock.opencode_api_key = "test_opencode_key"
|
||||
|
|
@ -50,6 +52,7 @@ def _make_mock_settings(**overrides):
|
|||
mock.nvidia_nim_proxy = ""
|
||||
mock.open_router_proxy = ""
|
||||
mock.mistral_proxy = ""
|
||||
mock.codestral_proxy = ""
|
||||
mock.lmstudio_proxy = ""
|
||||
mock.llamacpp_proxy = ""
|
||||
mock.kimi_proxy = ""
|
||||
|
|
@ -229,6 +232,21 @@ async def test_get_provider_mistral():
|
|||
assert provider._api_key == "test_mistral_key"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_provider_mistral_codestral():
|
||||
"""provider_type=mistral_codestral returns CodestralProvider."""
|
||||
with patch("api.dependencies.get_settings") as mock_settings:
|
||||
mock_settings.return_value = _make_mock_settings(
|
||||
provider_type="mistral_codestral",
|
||||
)
|
||||
|
||||
provider = get_provider()
|
||||
|
||||
assert isinstance(provider, CodestralProvider)
|
||||
assert provider._base_url == "https://codestral.mistral.ai/v1"
|
||||
assert provider._api_key == "test_codestral_key"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_provider_gemini():
|
||||
"""Test that provider_type=gemini returns GeminiProvider."""
|
||||
|
|
@ -479,6 +497,23 @@ async def test_get_provider_mistral_missing_api_key():
|
|||
assert "console.mistral.ai" in exc_info.value.detail
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_provider_mistral_codestral_missing_api_key():
|
||||
"""Mistral Codestral 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_codestral",
|
||||
codestral_api_key="",
|
||||
)
|
||||
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
get_provider()
|
||||
|
||||
assert exc_info.value.status_code == 503
|
||||
assert "CODESTRAL_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."""
|
||||
|
|
|
|||
|
|
@ -761,6 +761,10 @@ class TestPerModelMapping:
|
|||
assert (
|
||||
Settings.parse_provider_type("mistral/devstral-small-latest") == "mistral"
|
||||
)
|
||||
assert (
|
||||
Settings.parse_provider_type("mistral_codestral/codestral-latest")
|
||||
== "mistral_codestral"
|
||||
)
|
||||
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"
|
||||
|
|
@ -779,6 +783,10 @@ class TestPerModelMapping:
|
|||
Settings.parse_model_name("mistral/devstral-small-latest")
|
||||
== "devstral-small-latest"
|
||||
)
|
||||
assert (
|
||||
Settings.parse_model_name("mistral_codestral/codestral-latest")
|
||||
== "codestral-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"
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ from pathlib import Path
|
|||
from messaging.platforms.factory import create_messaging_platform
|
||||
from providers.base import BaseProvider
|
||||
from providers.cerebras import CerebrasProvider
|
||||
from providers.codestral import CodestralProvider
|
||||
from providers.deepseek import DeepSeekProvider
|
||||
from providers.fireworks import FireworksProvider
|
||||
from providers.gemini import GeminiProvider
|
||||
|
|
@ -79,6 +80,7 @@ def test_provider_and_platform_registries_include_advertised_builtins() -> None:
|
|||
"nvidia_nim": NvidiaNimProvider,
|
||||
"open_router": OpenRouterProvider,
|
||||
"mistral": MistralProvider,
|
||||
"mistral_codestral": CodestralProvider,
|
||||
"deepseek": DeepSeekProvider,
|
||||
"kimi": KimiProvider,
|
||||
"fireworks": FireworksProvider,
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ def _settings(**overrides):
|
|||
"nvidia_nim_api_key": "",
|
||||
"open_router_api_key": "",
|
||||
"mistral_api_key": "",
|
||||
"codestral_api_key": "",
|
||||
"deepseek_api_key": "",
|
||||
"kimi_api_key": "",
|
||||
"wafer_api_key": "",
|
||||
|
|
|
|||
182
tests/providers/test_codestral.py
Normal file
182
tests/providers/test_codestral.py
Normal file
|
|
@ -0,0 +1,182 @@
|
|||
"""Tests for Mistral Codestral provider."""
|
||||
|
||||
from contextlib import asynccontextmanager
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from providers.base import ProviderConfig
|
||||
from providers.codestral import CODESTRAL_DEFAULT_BASE, CodestralProvider
|
||||
|
||||
|
||||
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 codestral_config():
|
||||
return ProviderConfig(
|
||||
api_key="test_codestral_key",
|
||||
base_url=CODESTRAL_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 codestral_provider(codestral_config):
|
||||
return CodestralProvider(codestral_config)
|
||||
|
||||
|
||||
def test_init(codestral_config):
|
||||
"""Test provider initialization."""
|
||||
with patch("providers.openai_compat.AsyncOpenAI") as mock_openai:
|
||||
provider = CodestralProvider(codestral_config)
|
||||
assert provider._api_key == "test_codestral_key"
|
||||
assert provider._base_url == CODESTRAL_DEFAULT_BASE
|
||||
mock_openai.assert_called_once()
|
||||
|
||||
|
||||
def test_default_base_url():
|
||||
assert CODESTRAL_DEFAULT_BASE == "https://codestral.mistral.ai/v1"
|
||||
|
||||
|
||||
def test_build_request_body_basic(codestral_provider):
|
||||
"""Basic request body conversion works for Codestral."""
|
||||
req = MockRequest()
|
||||
body = codestral_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 = CodestralProvider(
|
||||
ProviderConfig(
|
||||
api_key="test_codestral_key",
|
||||
base_url=CODESTRAL_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(codestral_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(
|
||||
codestral_provider._client.chat.completions, "create", new_callable=AsyncMock
|
||||
) as mock_create:
|
||||
mock_create.return_value = mock_stream()
|
||||
|
||||
events = [event async for event in codestral_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(codestral_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(
|
||||
codestral_provider._client.chat.completions, "create", new_callable=AsyncMock
|
||||
) as mock_create:
|
||||
mock_create.return_value = mock_stream()
|
||||
|
||||
events = [event async for event in codestral_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(codestral_provider):
|
||||
"""cleanup closes the OpenAI client."""
|
||||
codestral_provider._client = AsyncMock()
|
||||
|
||||
await codestral_provider.cleanup()
|
||||
|
||||
codestral_provider._client.close.assert_called_once()
|
||||
|
|
@ -8,6 +8,7 @@ from config.nim import NimSettings
|
|||
from config.provider_catalog import PROVIDER_CATALOG, ZAI_DEFAULT_BASE
|
||||
from config.provider_ids import SUPPORTED_PROVIDER_IDS
|
||||
from providers.cerebras import CerebrasProvider
|
||||
from providers.codestral import CodestralProvider
|
||||
from providers.deepseek import DeepSeekProvider
|
||||
from providers.exceptions import UnknownProviderTypeError
|
||||
from providers.fireworks import FireworksProvider
|
||||
|
|
@ -38,6 +39,7 @@ def _make_settings(**overrides):
|
|||
mock.nvidia_nim_api_key = "test_key"
|
||||
mock.open_router_api_key = "test_openrouter_key"
|
||||
mock.mistral_api_key = "test_mistral_key"
|
||||
mock.codestral_api_key = "test_codestral_key"
|
||||
mock.deepseek_api_key = "test_deepseek_key"
|
||||
mock.wafer_api_key = "test_wafer_key"
|
||||
mock.opencode_api_key = "test_opencode_key"
|
||||
|
|
@ -50,6 +52,7 @@ def _make_settings(**overrides):
|
|||
mock.lmstudio_proxy = ""
|
||||
mock.llamacpp_proxy = ""
|
||||
mock.mistral_proxy = ""
|
||||
mock.codestral_proxy = ""
|
||||
mock.kimi_proxy = ""
|
||||
mock.kimi_api_key = "test_kimi_key"
|
||||
mock.wafer_proxy = ""
|
||||
|
|
@ -171,6 +174,7 @@ def test_create_provider_instantiates_each_builtin():
|
|||
cases = {
|
||||
"nvidia_nim": NvidiaNimProvider,
|
||||
"mistral": MistralProvider,
|
||||
"mistral_codestral": CodestralProvider,
|
||||
"deepseek": DeepSeekProvider,
|
||||
"kimi": KimiProvider,
|
||||
"fireworks": FireworksProvider,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue