Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,15 @@
# Optional base URL override:
# UPSTAGE_BASE_URL=https://api.upstage.ai/v1

# =============================================================================
# LLM PROVIDER (Mistral AI)
# =============================================================================
# Mistral, Codestral, and Devstral models via Mistral's OpenAI-compatible API.
# Get your key at: https://console.mistral.ai/
# MISTRAL_API_KEY=your_key_here
# Optional base URL override:
# MISTRAL_BASE_URL=https://api.mistral.ai/v1

# =============================================================================
# LLM PROVIDER (Ramp Router)
# =============================================================================
Expand Down
2 changes: 2 additions & 0 deletions hermes_cli/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -1545,6 +1545,8 @@ def group_providers(slugs):
"novitaai": "novita",
"mimo": "xiaomi",
"xiaomi-mimo": "xiaomi",
"mistral-ai": "mistral",
"mistralai": "mistral",
"tencent": "tencent-tokenhub",
"tokenhub": "tencent-tokenhub",
"tencent-cloud": "tencent-tokenhub",
Expand Down
11 changes: 11 additions & 0 deletions hermes_cli/providers.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,11 @@ class HermesOverlay:
transport="openai_chat",
base_url_env_var="DEEPSEEK_BASE_URL",
),
"mistral": HermesOverlay(
transport="openai_chat",
base_url_override="https://api.mistral.ai/v1",
base_url_env_var="MISTRAL_BASE_URL",
),
"alibaba": HermesOverlay(
transport="openai_chat",
base_url_env_var="DASHSCOPE_BASE_URL",
Expand Down Expand Up @@ -362,6 +367,10 @@ class ProviderDef:
# deepseek
"deep-seek": "deepseek",

# mistral
"mistral-ai": "mistral",
"mistralai": "mistral",

# alibaba
"dashscope": "alibaba",
"aliyun": "alibaba",
Expand Down Expand Up @@ -997,6 +1006,8 @@ def resolve_provider_full(
transport="openai_chat",
api_key_env_vars=tuple(_pcfg.api_key_env_vars or ()),
base_url=_pcfg.inference_base_url or "",
base_url_env_var=_pcfg.base_url_env_var or "",
auth_type=_pcfg.auth_type or "api_key",
source="hermes-auth-registry",
)
except Exception:
Expand Down
31 changes: 31 additions & 0 deletions plugins/model-providers/mistral/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
"""Mistral AI model-provider profile."""

from providers import register_provider
from providers.base import ProviderProfile


mistral = ProviderProfile(
name="mistral",
aliases=("mistral-ai", "mistralai"),
display_name="Mistral AI",
description="Mistral AI direct API (Mistral, Codestral, and Devstral models)",
signup_url="https://console.mistral.ai/",
env_vars=("MISTRAL_API_KEY", "MISTRAL_BASE_URL"),
base_url="https://api.mistral.ai/v1",
auth_type="api_key",
api_mode="chat_completions",
supports_vision=True,
supports_prompt_cache_key=True,
default_aux_model="mistral-small-latest",
# Keep this as a deliberately small fallback set for offline picker/setup
# paths. Live catalog/model metadata discovery supplies fresher entries.
fallback_models=(
"mistral-small-latest",
"mistral-medium-latest",
"mistral-large-latest",
"codestral-latest",
"devstral-medium-latest",
),
)

register_provider(mistral)
5 changes: 5 additions & 0 deletions plugins/model-providers/mistral/plugin.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
name: mistral-provider
kind: model-provider
version: 1.0.0
description: Mistral AI direct API provider
author: Nous Research
147 changes: 147 additions & 0 deletions tests/plugins/model_providers/test_mistral_profile.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
"""Behavioral tests for the bundled Mistral model-provider profile."""

from __future__ import annotations

from types import SimpleNamespace


def test_mistral_profile_core_contract():
import providers

profile = providers.get_provider_profile("mistral")

assert profile is not None
assert profile.name == "mistral"
assert profile.auth_type == "api_key"
assert profile.api_mode == "chat_completions"
assert profile.base_url == "https://api.mistral.ai/v1"
assert profile.env_vars == ("MISTRAL_API_KEY", "MISTRAL_BASE_URL")
assert profile.default_aux_model == "mistral-small-latest"
assert profile.supports_prompt_cache_key is True
assert profile.display_name == "Mistral AI"
assert profile.description
assert profile.signup_url.startswith("https://")


def test_mistral_aliases_resolve_through_profile_registry():
import providers

for alias in ("mistral-ai", "mistralai"):
resolved = providers.get_provider_profile(alias)
assert resolved is not None
assert resolved.name == "mistral"


def test_mistral_profile_opts_into_prompt_cache_key_transport_gate():
import agent.transports.chat_completions # noqa: F401 -- registers transport
import providers
from agent.transports import get_transport

profile = providers.get_provider_profile("mistral")
assert profile is not None

transport = get_transport("chat_completions")
assert transport is not None

kwargs = transport.build_kwargs(
model="mistral-large-latest",
messages=[{"role": "system", "content": "stable"}, {"role": "user", "content": "hi"}],
tools=[],
session_id="mistral-cache-session",
provider_profile=profile,
)

assert kwargs["prompt_cache_key"].startswith("pck_")


def test_mistral_fallback_catalog_is_small_valid_and_deduped():
import providers

profile = providers.get_provider_profile("mistral")
assert profile is not None
models = list(profile.fallback_models)

assert models
assert len(models) == len(set(models))
assert len(models) <= 8 # intentional fallback set, not an exhaustive snapshot
assert profile.default_aux_model in models
assert all(isinstance(model, str) and model.strip() == model and model for model in models)


def test_mistral_generic_api_key_registration_and_catalog_surface():
from hermes_cli.auth import PROVIDER_REGISTRY
from hermes_cli.models import CANONICAL_PROVIDERS
from hermes_cli.provider_catalog import provider_catalog_by_slug

pconfig = PROVIDER_REGISTRY["mistral"]
assert pconfig.auth_type == "api_key"
assert pconfig.inference_base_url == "https://api.mistral.ai/v1"
assert pconfig.api_key_env_vars == ("MISTRAL_API_KEY",)
assert pconfig.base_url_env_var == "MISTRAL_BASE_URL"

assert any(entry.slug == "mistral" for entry in CANONICAL_PROVIDERS)

descriptor = provider_catalog_by_slug()["mistral"]
assert descriptor.label == "Mistral AI"
assert descriptor.tab == "keys"
assert descriptor.api_key_env_vars == ("MISTRAL_API_KEY",)
assert descriptor.base_url_env_var == "MISTRAL_BASE_URL"
assert descriptor.signup_url == "https://console.mistral.ai/"


def test_mistral_aliases_resolve_for_provider_surfaces():
from hermes_cli.models import normalize_provider, provider_model_ids
from hermes_cli.providers import resolve_provider_full

for alias in ("mistral", "mistral-ai", "mistralai"):
assert normalize_provider(alias) == "mistral"
pdef = resolve_provider_full(alias)
assert pdef is not None
assert pdef.id == "mistral"
assert pdef.base_url == "https://api.mistral.ai/v1"
assert pdef.base_url_env_var == "MISTRAL_BASE_URL"
assert "MISTRAL_API_KEY" in pdef.api_key_env_vars
ids = provider_model_ids(alias)
assert ids
assert len(ids) == len(set(ids))
assert all(isinstance(model, str) and model for model in ids)


def test_mistral_config_driven_runtime_uses_default_endpoint(monkeypatch):
from hermes_cli import runtime_provider as rp

monkeypatch.setenv("MISTRAL_API_KEY", "test-mistral-key")
monkeypatch.delenv("MISTRAL_BASE_URL", raising=False)
monkeypatch.setattr(rp, "load_pool", lambda _provider: SimpleNamespace(has_credentials=lambda: False))
monkeypatch.setattr(
rp,
"_get_model_config",
lambda: {"provider": "mistral", "default": "mistral-small-latest"},
)

resolved = rp.resolve_runtime_provider()

assert resolved["provider"] == "mistral"
assert resolved["requested_provider"] == "mistral"
assert resolved["api_mode"] == "chat_completions"
assert resolved["base_url"] == "https://api.mistral.ai/v1"
assert resolved["api_key"] == "test-mistral-key"


def test_mistral_base_url_env_override(monkeypatch):
from hermes_cli import runtime_provider as rp

monkeypatch.setenv("MISTRAL_API_KEY", "test-mistral-key")
monkeypatch.setenv("MISTRAL_BASE_URL", "https://mistral-proxy.example/v1/")
monkeypatch.setattr(rp, "load_pool", lambda _provider: SimpleNamespace(has_credentials=lambda: False))
monkeypatch.setattr(
rp,
"_get_model_config",
lambda: {"provider": "mistral", "default": "mistral-small-latest"},
)

resolved = rp.resolve_runtime_provider()

assert resolved["provider"] == "mistral"
assert resolved["base_url"] == "https://mistral-proxy.example/v1"
assert resolved["api_key"] == "test-mistral-key"
9 changes: 8 additions & 1 deletion website/docs/integrations/providers.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ You need at least one way to connect to an LLM. Use `hermes model` to switch pro
| **OpenRouter** | `OPENROUTER_API_KEY` in `~/.hermes/.env` |
| **Ramp Router** | `RAMP_ROUTER_API_KEY` in `~/.hermes/.env` (provider: `router`; aliases: `ramp-router`, `ramp`, `router.com`; Responses-native gateway, live account-scoped catalog) |
| **Fireworks AI** | `FIREWORKS_API_KEY` in `~/.hermes/.env` (provider: `fireworks`; aliases: `fireworks-ai`, `fw`) |
| **Mistral AI** | `MISTRAL_API_KEY` in `~/.hermes/.env` (provider: `mistral`; aliases: `mistral-ai`, `mistralai`; optional `MISTRAL_BASE_URL`) |
| **NovitaAI** | `NOVITA_API_KEY` in `~/.hermes/.env` (provider: `novita`, 200+ models, Model API, Agent Sandbox, GPU Cloud) |
| **AI Gateway** | `AI_GATEWAY_API_KEY` in `~/.hermes/.env` (provider: `ai-gateway`) |
| **z.ai / GLM** | `GLM_API_KEY` in `~/.hermes/.env` (provider: `zai`) |
Expand Down Expand Up @@ -253,6 +254,10 @@ These providers have built-in support with dedicated provider IDs. Set the API k
hermes chat --provider fireworks --model accounts/fireworks/models/kimi-k2p6
# Requires: FIREWORKS_API_KEY in ~/.hermes/.env

# Mistral AI
hermes chat --provider mistral --model mistral-small-latest
# Requires: MISTRAL_API_KEY in ~/.hermes/.env

# NovitaAI Model API
hermes chat --provider novita --model moonshotai/kimi-k2.5
# Requires: NOVITA_API_KEY in ~/.hermes/.env
Expand Down Expand Up @@ -317,14 +322,16 @@ hermes chat --provider nebius --model deepseek-ai/DeepSeek-V4-Pro

Fireworks uses its native slash-form catalog IDs, such as `accounts/fireworks/models/kimi-k2p6`. Run `hermes model`, choose **Fireworks AI**, and select from the live catalog or enter another Fireworks model ID. The default endpoint is `https://api.fireworks.ai/inference/v1`; configure a different endpoint through `model.base_url` in `config.yaml`, not `.env`.

Mistral AI uses the OpenAI-compatible Chat Completions API. Set `MISTRAL_API_KEY`, choose provider `mistral`, and use aliases such as `mistral-ai` or `mistralai` if needed. The default endpoint is `https://api.mistral.ai/v1`; override it with `MISTRAL_BASE_URL` only for compatible proxies or enterprise endpoints.

Or set the provider permanently in `config.yaml`:
```yaml
model:
provider: "gmi"
default: "zai-org/GLM-5.1-FP8"
```

Base URLs can be overridden with `NOVITA_BASE_URL`, `GLM_BASE_URL`, `KIMI_BASE_URL`, `MINIMAX_BASE_URL`, `MINIMAX_CN_BASE_URL`, `DASHSCOPE_BASE_URL`, `XIAOMI_BASE_URL`, `GMI_BASE_URL`, `META_BASE_URL`, or `TOKENHUB_BASE_URL` environment variables.
Base URLs can be overridden with `NOVITA_BASE_URL`, `MISTRAL_BASE_URL`, `GLM_BASE_URL`, `KIMI_BASE_URL`, `MINIMAX_BASE_URL`, `MINIMAX_CN_BASE_URL`, `DASHSCOPE_BASE_URL`, `XIAOMI_BASE_URL`, `GMI_BASE_URL`, `META_BASE_URL`, or `TOKENHUB_BASE_URL` environment variables.

:::note Meta contributor tier
`muse-spark-1.2-contributor` is Meta's discounted tier — Meta may train on your prompts and completions, so [interactive model selection asks for confirmation](../user-guide/configuring-models.md) before using it. Use `muse-spark-1.2` (standard pricing, no training) for confidential work.
Expand Down
3 changes: 2 additions & 1 deletion website/docs/reference/environment-variables.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,8 @@ Hermes reads environment variables from the process environment and, for user-ma
| `OLLAMA_BASE_URL` | Override Ollama Cloud base URL (default: `https://ollama.com/v1`) |
| `XAI_API_KEY` | xAI (Grok) API key for chat + TTS + web search ([console.x.ai](https://console.x.ai/)) |
| `XAI_BASE_URL` | Override xAI base URL (default: `https://api.x.ai/v1`) |
| `MISTRAL_API_KEY` | Mistral API key for Voxtral TTS and Voxtral STT ([console.mistral.ai](https://console.mistral.ai)) |
| `MISTRAL_API_KEY` | Mistral API key for direct LLM inference plus Voxtral TTS/STT ([console.mistral.ai](https://console.mistral.ai)) |
| `MISTRAL_BASE_URL` | Override Mistral base URL (default: `https://api.mistral.ai/v1`) |
| `AWS_REGION` | AWS region for Bedrock inference (e.g. `us-east-1`, `eu-central-1`). Read by boto3. |
| `AWS_PROFILE` | AWS named profile for Bedrock authentication (reads `~/.aws/credentials`). Leave unset to use default boto3 credential chain. |
| `BEDROCK_BASE_URL` | Override Bedrock runtime base URL (default: `https://bedrock-runtime.us-east-1.amazonaws.com`; usually leave unset and use `AWS_REGION` instead) |
Expand Down