From 6b7e72d9ce2a7a23af87faa57b97da6e1e9136ce Mon Sep 17 00:00:00 2001 From: Adolanium <94890352+Adolanium@users.noreply.github.com> Date: Sun, 21 Jun 2026 19:28:31 +0300 Subject: [PATCH] feat: add Mistral AI as first-class LLM provider --- .env.example | 9 + agent/model_metadata.py | 4 +- hermes_cli/config.py | 2 +- hermes_cli/models.py | 11 ++ hermes_cli/providers.py | 9 + plugins/model-providers/mistral/__init__.py | 42 +++++ plugins/model-providers/mistral/plugin.yaml | 5 + .../model_providers/test_mistral_profile.py | 164 ++++++++++++++++++ website/docs/integrations/providers.md | 32 +++- website/docs/reference/cli-commands.md | 2 +- .../docs/reference/environment-variables.md | 3 +- 11 files changed, 277 insertions(+), 6 deletions(-) create mode 100644 plugins/model-providers/mistral/__init__.py create mode 100644 plugins/model-providers/mistral/plugin.yaml create mode 100644 tests/plugins/model_providers/test_mistral_profile.py diff --git a/.env.example b/.env.example index 4c83db1f3b48d..02b270a079f51 100644 --- a/.env.example +++ b/.env.example @@ -22,6 +22,15 @@ # NOVITA_API_KEY= # NOVITA_BASE_URL=https://api.novita.ai/openai/v1 # Override default base URL +# ============================================================================= +# LLM PROVIDER (Mistral AI) +# ============================================================================= +# Mistral AI: Mistral, Codestral, and Devstral models (direct API). +# The same key also powers Voxtral TTS and transcription. +# Get your key at: https://console.mistral.ai/ +# MISTRAL_API_KEY= +# MISTRAL_BASE_URL=https://api.mistral.ai/v1 # Override default base URL + # ============================================================================= # LLM PROVIDER (Google AI Studio / Gemini) # ============================================================================= diff --git a/agent/model_metadata.py b/agent/model_metadata.py index 9430a98bfb13c..0fb73cdf76c46 100644 --- a/agent/model_metadata.py +++ b/agent/model_metadata.py @@ -48,7 +48,7 @@ def _resolve_requests_verify() -> bool | str: _PROVIDER_PREFIXES: frozenset[str] = frozenset({ "openrouter", "nous", "openai-codex", "copilot", "copilot-acp", "gemini", "ollama-cloud", "zai", "kimi-coding", "kimi-coding-cn", "stepfun", "minimax", "minimax-oauth", "minimax-cn", "anthropic", "deepseek", - "opencode-zen", "opencode-go", "kilocode", "alibaba", "novita", + "opencode-zen", "opencode-go", "kilocode", "alibaba", "novita", "mistral", "qwen-oauth", "xiaomi", "arcee", @@ -68,6 +68,7 @@ def _resolve_requests_verify() -> bool | str: "xai", "x-ai", "x.ai", "grok", "nvidia", "nim", "nvidia-nim", "nemotron", "qwen-portal", "novita-ai", "novitaai", + "mistral-ai", "mistralai", }) @@ -428,6 +429,7 @@ def _is_custom_endpoint(base_url: str) -> bool: "generativelanguage.googleapis.com": "gemini", "inference-api.nousresearch.com": "nous", "api.deepseek.com": "deepseek", + "api.mistral.ai": "mistral", "api.githubcopilot.com": "copilot", "models.github.ai": "copilot", # GitHub Models free tier (Azure-hosted prototyping endpoint) — same diff --git a/hermes_cli/config.py b/hermes_cli/config.py index 3474ee35a0ed9..09352ca775cbe 100644 --- a/hermes_cli/config.py +++ b/hermes_cli/config.py @@ -3592,7 +3592,7 @@ def _ensure_hermes_home_managed(home: Path): "category": "tool", }, "MISTRAL_API_KEY": { - "description": "Mistral API key for Voxtral TTS and transcription (STT)", + "description": "Mistral API key for LLM inference (provider: mistral), Voxtral TTS, and transcription (STT)", "prompt": "Mistral API key", "url": "https://console.mistral.ai/", "password": True, diff --git a/hermes_cli/models.py b/hermes_cli/models.py index cf3eb40edaaf8..ea6937efa8d65 100644 --- a/hermes_cli/models.py +++ b/hermes_cli/models.py @@ -356,6 +356,14 @@ def _xai_curated_models() -> list[str]: "deepseek-chat", "deepseek-reasoner", ], + "mistral": [ + "mistral-large-latest", + "mistral-medium-latest", + "mistral-small-latest", + "codestral-latest", + "devstral-medium-latest", + "ministral-8b-latest", + ], "xiaomi": [ "mimo-v2.5-pro", "mimo-v2.5", @@ -1033,6 +1041,7 @@ class ProviderEntry(NamedTuple): ProviderEntry("huggingface", "Hugging Face", "Hugging Face Inference Providers"), ProviderEntry("gemini", "Google AI Studio", "Google AI Studio (Native Gemini API)"), ProviderEntry("deepseek", "DeepSeek", "DeepSeek (V3, R1, coder, direct API)"), + ProviderEntry("mistral", "Mistral AI", "Mistral AI (Large, Medium, Small, Codestral, Devstral; direct API)"), ProviderEntry("xai", "xAI", "xAI Grok (Direct API)"), ProviderEntry("zai", "Z.AI / GLM", "Z.AI / GLM (Zhipu direct API)"), ProviderEntry("kimi-coding", "Kimi / Kimi Coding Plan", "Kimi Coding Plan (api.kimi.com & Moonshot API)"), @@ -1211,6 +1220,8 @@ def group_providers(slugs): "claude": "anthropic", "claude-code": "anthropic", "deep-seek": "deepseek", + "mistral-ai": "mistral", + "mistralai": "mistral", "opencode": "opencode-zen", "zen": "opencode-zen", "go": "opencode-go", diff --git a/hermes_cli/providers.py b/hermes_cli/providers.py index 0c2a451831519..83a79851d69c2 100644 --- a/hermes_cli/providers.py +++ b/hermes_cli/providers.py @@ -134,6 +134,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", @@ -301,6 +306,10 @@ class ProviderDef: # deepseek "deep-seek": "deepseek", + # mistral + "mistral-ai": "mistral", + "mistralai": "mistral", + # alibaba "dashscope": "alibaba", "aliyun": "alibaba", diff --git a/plugins/model-providers/mistral/__init__.py b/plugins/model-providers/mistral/__init__.py new file mode 100644 index 0000000000000..2c4a4c7aac829 --- /dev/null +++ b/plugins/model-providers/mistral/__init__.py @@ -0,0 +1,42 @@ +"""Mistral AI provider profile. + +Mistral's platform speaks the OpenAI-compatible Chat Completions API at +``https://api.mistral.ai/v1``, so the default ``openai_chat`` transport carries +tool calling and streaming without special handling. The endpoint is strict and +rejects unknown message fields, but the transport already strips Hermes-internal +keys (reasoning carriers, codex fields, finish_reason, scaffolding markers) +before the request leaves, so no extra message handling is needed here. + +Adjustable reasoning (``reasoning_effort``) is intentionally not wired: Mistral +streams reasoning as structured content blocks (``[{"type": "thinking"}, ...]``) +rather than a plain string, which the streaming path does not flatten yet, so +enabling it truncates responses. That belongs in the streaming layer, not this +profile. +""" + +from providers import register_provider +from providers.base import ProviderProfile + + +mistral = ProviderProfile( + name="mistral", + aliases=("mistral-ai", "mistralai"), + env_vars=("MISTRAL_API_KEY", "MISTRAL_BASE_URL"), + display_name="Mistral AI", + description="Mistral AI: Mistral, Codestral, and Devstral models (direct API)", + signup_url="https://console.mistral.ai/", + base_url="https://api.mistral.ai/v1", + auth_type="api_key", + supports_vision=True, + default_aux_model="mistral-small-latest", + fallback_models=( + "mistral-large-latest", + "mistral-medium-latest", + "mistral-small-latest", + "codestral-latest", + "devstral-medium-latest", + "ministral-8b-latest", + ), +) + +register_provider(mistral) diff --git a/plugins/model-providers/mistral/plugin.yaml b/plugins/model-providers/mistral/plugin.yaml new file mode 100644 index 0000000000000..bf3dbf6f1957e --- /dev/null +++ b/plugins/model-providers/mistral/plugin.yaml @@ -0,0 +1,5 @@ +name: mistral-provider +kind: model-provider +version: 1.0.0 +description: Mistral AI direct API (Mistral, Codestral, Devstral) +author: Adolanium diff --git a/tests/plugins/model_providers/test_mistral_profile.py b/tests/plugins/model_providers/test_mistral_profile.py new file mode 100644 index 0000000000000..a8c88e98db53d --- /dev/null +++ b/tests/plugins/model_providers/test_mistral_profile.py @@ -0,0 +1,164 @@ +"""Unit tests for the Mistral provider profile. + +Mistral is a plain OpenAI-compatible provider: chat and tool calling ride the +default ``openai_chat`` transport. These tests pin the profile's identity, +endpoint, aliases, auxiliary model, and curated fallback catalog so the +first-class wiring stays intact. +""" + +from __future__ import annotations + +import pytest + + +@pytest.fixture +def mistral_profile(): + """Resolve the registered Mistral profile through the public registry.""" + # Importing ``model_tools`` triggers plugin discovery, which registers the + # Mistral profile in the global provider registry. + import model_tools # noqa: F401 + import providers + + profile = providers.get_provider_profile("mistral") + assert profile is not None, "mistral provider profile must be registered" + return profile + + +class TestMistralProfileMetadata: + """Identity, endpoint, and aux model are wired as a first-class provider.""" + + def test_identity_and_endpoint(self, mistral_profile): + assert mistral_profile.name == "mistral" + assert mistral_profile.base_url == "https://api.mistral.ai/v1" + assert mistral_profile.auth_type == "api_key" + assert "MISTRAL_API_KEY" in mistral_profile.env_vars + assert mistral_profile.display_name == "Mistral AI" + + def test_aliases_resolve(self): + import model_tools # noqa: F401 + import providers + + assert providers.get_provider_profile("mistral-ai").name == "mistral" + assert providers.get_provider_profile("mistralai").name == "mistral" + + def test_default_aux_model(self, mistral_profile): + assert mistral_profile.default_aux_model == "mistral-small-latest" + + def test_consumer_api_returns_aux_model(self): + from agent.auxiliary_client import _get_aux_model_for_provider + + assert _get_aux_model_for_provider("mistral") == "mistral-small-latest" + + def test_fallback_catalog_is_latest_aliases(self, mistral_profile): + models = mistral_profile.fallback_models + assert "mistral-large-latest" in models + assert "codestral-latest" in models + # Curated fallbacks use stable -latest aliases so they don't go stale. + assert all(m.endswith("-latest") for m in models) + + def test_no_reasoning_kwargs_emitted(self, mistral_profile): + """The plain profile sends no provider-specific reasoning kwargs.""" + extra_body, top_level = mistral_profile.build_api_kwargs_extras( + reasoning_config={"enabled": True, "effort": "high"}, + model="mistral-small-latest", + ) + assert extra_body == {} + assert top_level == {} + + +class TestMistralConfigResolution: + """Selecting Mistral via ``config.yaml`` reaches the first-class path. + + ``hermes chat`` with no ``--provider`` flag resolves the provider from + ``model.provider`` in ``config.yaml`` through the same ``normalize_provider`` + and api-key dispatch the flag uses, so the config-driven flow lands on the + first-class provider, not a custom-endpoint fallback. + """ + + @pytest.mark.parametrize("configured", ["mistral", "mistral-ai", "mistralai"]) + def test_config_provider_value_normalizes(self, configured): + from hermes_cli.models import normalize_provider + + assert normalize_provider(configured) == "mistral" + + def test_auto_resolves_provider_from_config_yaml(self, monkeypatch): + # The exact flow from the reviewer's example: ``config.yaml`` sets + # ``model.provider: mistral`` and ``hermes chat`` runs without + # ``--provider`` (requested == "auto"). resolve_provider() must read the + # config provider and land on the first-class ``mistral``, not fall + # through to an OpenRouter/env default. + import model_tools # noqa: F401 + import hermes_cli.config as config_mod + from hermes_cli.auth import resolve_provider + + monkeypatch.setattr( + config_mod, + "load_config", + lambda: {"model": {"provider": "mistral", "default": "mistral-large-latest"}}, + ) + assert resolve_provider("auto") == "mistral" + + def test_config_provider_takes_api_key_path(self): + # The agent treats this provider as a profile-backed api-key provider, + # which is the branch a config-set ``provider: mistral`` flows through. + import model_tools # noqa: F401 + from hermes_cli.main import _is_profile_api_key_provider + + assert _is_profile_api_key_provider("mistral") is True + + +class TestMistralStrictTransport: + """Mistral relies on the generic transport sanitization, not its own. + + ``api.mistral.ai`` is a strict OpenAI-compatible endpoint that rejects + unknown message fields with HTTP 400. The Mistral profile carries no + provider-specific message cleanup (``prepare_messages`` is pass-through); + the shared ``chat_completions`` transport strips the Hermes-internal carriers + (``timestamp``, ``tool_name``, ``_``-scaffolding, Gemini ``extra_content``) + before the request leaves. These tests pin that contract so a future change + that reintroduces a leak fails here. + """ + + def test_profile_prepare_messages_is_passthrough(self, mistral_profile): + messages = [ + {"role": "user", "content": "hi"}, + {"role": "assistant", "content": "ok"}, + ] + assert mistral_profile.prepare_messages(messages) == messages + + def test_transport_strips_fields_mistral_rejects(self): + from agent.transports import get_transport + import agent.transports.chat_completions # noqa: F401 + + transport = get_transport("chat_completions") + messages = [ + { + "role": "assistant", + "content": "ok", + "timestamp": "2026-06-21T00:00:00Z", + "tool_name": "terminal", + "_empty_recovery_synthetic": True, + "tool_calls": [ + { + "id": "call_1", + "type": "function", + "extra_content": {"google": {"thought_signature": "SIG"}}, + "function": {"name": "t", "arguments": "{}"}, + } + ], + } + ] + result = transport.convert_messages(messages, model="mistral-large-latest") + sent = result[0] + # Internal carriers that api.mistral.ai rejects are gone. + assert "timestamp" not in sent + assert "tool_name" not in sent + assert "_empty_recovery_synthetic" not in sent + assert "extra_content" not in sent["tool_calls"][0] + # Real chat-completions fields survive untouched. + assert sent["role"] == "assistant" + assert sent["content"] == "ok" + assert sent["tool_calls"][0]["id"] == "call_1" + assert sent["tool_calls"][0]["function"]["name"] == "t" + # The caller's list is not mutated (sanitization deep-copies on demand). + assert "timestamp" in messages[0] diff --git a/website/docs/integrations/providers.md b/website/docs/integrations/providers.md index 1378762f346ff..48e9a5d3bc2ee 100644 --- a/website/docs/integrations/providers.md +++ b/website/docs/integrations/providers.md @@ -38,6 +38,7 @@ You need at least one way to connect to an LLM. Use `hermes model` to switch pro | **OpenCode Zen** | `OPENCODE_ZEN_API_KEY` in `~/.hermes/.env` (provider: `opencode-zen`) | | **OpenCode Go** | `OPENCODE_GO_API_KEY` in `~/.hermes/.env` (provider: `opencode-go`) | | **DeepSeek** | `DEEPSEEK_API_KEY` in `~/.hermes/.env` (provider: `deepseek`) | +| **Mistral AI** | `MISTRAL_API_KEY` in `~/.hermes/.env` (provider: `mistral`; aliases: `mistral-ai`, `mistralai`) | | **Hugging Face** | `HF_TOKEN` in `~/.hermes/.env` (provider: `huggingface`, aliases: `hf`) | | **Google / Gemini** | `GOOGLE_API_KEY` (or `GEMINI_API_KEY`) in `~/.hermes/.env` (provider: `gemini`) | | **OpenAI API (direct)** | `OPENAI_API_KEY` in `~/.hermes/.env` (provider: `openai-api`, optional `OPENAI_BASE_URL`) | @@ -257,6 +258,10 @@ hermes chat --provider arcee --model trinity-large-thinking # Use the exact model ID returned by GMI's /v1/models endpoint. hermes chat --provider gmi --model zai-org/GLM-5.1-FP8 # Requires: GMI_API_KEY in ~/.hermes/.env + +# Mistral AI (Mistral, Codestral, Devstral) +hermes chat --provider mistral --model mistral-large-latest +# Requires: MISTRAL_API_KEY in ~/.hermes/.env ``` Or set the provider permanently in `config.yaml`: @@ -266,7 +271,7 @@ model: 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`, or `TOKENHUB_BASE_URL` environment variables. +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`, `MISTRAL_BASE_URL`, or `TOKENHUB_BASE_URL` environment variables. :::note Z.AI Endpoint Auto-Detection When using the Z.AI / GLM provider, Hermes automatically probes multiple endpoints (global, China, coding variants) to find one that accepts your API key. You don't need to set `GLM_BASE_URL` manually — the working endpoint is detected and cached automatically. @@ -316,6 +321,29 @@ model: Get your API key at [novita.ai/settings/key-management](https://novita.ai/settings/key-management). The base URL can be overridden with `NOVITA_BASE_URL`. +### Mistral AI + +[Mistral AI](https://mistral.ai) serves its models over an OpenAI-compatible Chat Completions API, so tool calling and streaming work through the standard provider path. Set `MISTRAL_API_KEY` in `~/.hermes/.env` and pick **Mistral AI** in `hermes model`, or pass `--provider mistral`. + +```bash +hermes chat --provider mistral --model mistral-large-latest +# Requires: MISTRAL_API_KEY in ~/.hermes/.env + +# Short alias +hermes chat --provider mistralai --model codestral-latest +``` + +Or set it permanently in `config.yaml`, with no endpoint needed since the provider is first-class: +```yaml +model: + provider: "mistral" + default: "mistral-large-latest" +``` + +The picker reads the live catalog from [models.dev](https://models.dev) and merges in a curated fallback (`mistral-large-latest`, `mistral-medium-latest`, `mistral-small-latest`, `codestral-latest`, `devstral-medium-latest`, `ministral-8b-latest`) when the catalog is unreachable. That fallback is a deliberately small set of current general chat and coding models, not the full lineup; vision-capable and other Mistral model IDs stay reachable through the live catalog or by passing the ID directly. Use any model ID Mistral exposes; the `-latest` aliases always track the current release. + +This is LLM inference over Mistral's REST API; it does not use the `mistralai` Python SDK. Get your API key at [console.mistral.ai](https://console.mistral.ai/). The base URL can be overridden with `MISTRAL_BASE_URL`. + ### Ollama Cloud — Managed Ollama Models, OAuth + API Key [Ollama Cloud](https://ollama.com/cloud) hosts the same open-weight catalog as local Ollama but without the GPU requirement. Pick it in `hermes model` as **Ollama Cloud**, paste your API key from [ollama.com/settings/keys](https://ollama.com/settings/keys), and Hermes auto-discovers the available models. @@ -1074,7 +1102,7 @@ Any service with an OpenAI-compatible API works. Some popular options: | [Fireworks AI](https://fireworks.ai) | `https://api.fireworks.ai/inference/v1` | Fast open model hosting | | [GMI Cloud](https://www.gmicloud.ai/) | `https://api.gmi-serving.com/v1` | Managed OpenAI-compatible inference | | [Cerebras](https://cerebras.ai) | `https://api.cerebras.ai/v1` | Wafer-scale chip inference | -| [Mistral AI](https://mistral.ai) | `https://api.mistral.ai/v1` | Mistral models | +| [Mistral AI](https://mistral.ai) | `https://api.mistral.ai/v1` | First-class provider: use `--provider mistral` with `MISTRAL_API_KEY` (see above) | | [OpenAI](https://openai.com) | `https://api.openai.com/v1` | Direct OpenAI access | | [Azure OpenAI](https://azure.microsoft.com) | `https://YOUR.openai.azure.com/` | Enterprise OpenAI | | [LocalAI](https://localai.io) | `http://localhost:8080/v1` | Self-hosted, multi-model | diff --git a/website/docs/reference/cli-commands.md b/website/docs/reference/cli-commands.md index 2f35858ef04d1..18d07dd65484f 100644 --- a/website/docs/reference/cli-commands.md +++ b/website/docs/reference/cli-commands.md @@ -105,7 +105,7 @@ Common options: | `-q`, `--query "..."` | One-shot, non-interactive prompt. | | `-m`, `--model ` | Override the model for this run. | | `-t`, `--toolsets ` | Enable a comma-separated set of toolsets. | -| `--provider ` | Force a provider: `auto`, `openrouter`, `nous`, `openai-codex`, `copilot-acp`, `copilot`, `anthropic`, `gemini`, `huggingface`, `novita` (aliases `novita-ai`, `novitaai`), `openai-api`, `zai`, `kimi-coding`, `kimi-coding-cn`, `minimax`, `minimax-cn`, `minimax-oauth`, `kilocode`, `xiaomi`, `arcee`, `gmi`, `alibaba`, `alibaba-coding-plan` (alias `alibaba_coding`), `deepseek`, `nvidia`, `ollama-cloud`, `xai` (alias `grok`), `xai-oauth` (alias `grok-oauth`), `qwen-oauth`, `bedrock`, `opencode-zen`, `opencode-go`, `azure-foundry`, `lmstudio`, `stepfun`, `tencent-tokenhub` (alias `tencent`, `tokenhub`). | +| `--provider ` | Force a provider: `auto`, `openrouter`, `nous`, `openai-codex`, `copilot-acp`, `copilot`, `anthropic`, `gemini`, `google-gemini-cli`, `huggingface`, `novita` (aliases `novita-ai`, `novitaai`), `openai-api`, `zai`, `kimi-coding`, `kimi-coding-cn`, `minimax`, `minimax-cn`, `minimax-oauth`, `kilocode`, `xiaomi`, `arcee`, `gmi`, `alibaba`, `alibaba-coding-plan` (alias `alibaba_coding`), `deepseek`, `mistral` (aliases `mistral-ai`, `mistralai`), `nvidia`, `ollama-cloud`, `xai` (alias `grok`), `xai-oauth` (alias `grok-oauth`), `qwen-oauth`, `bedrock`, `opencode-zen`, `opencode-go`, `azure-foundry`, `lmstudio`, `stepfun`, `tencent-tokenhub` (alias `tencent`, `tokenhub`). | | `-s`, `--skills ` | Preload one or more skills for the session (can be repeated or comma-separated). | | `-v`, `--verbose` | Verbose output. | | `-Q`, `--quiet` | Programmatic mode: suppress banner/spinner/tool previews. | diff --git a/website/docs/reference/environment-variables.md b/website/docs/reference/environment-variables.md index 1b45de6c175e4..8022b030200ec 100644 --- a/website/docs/reference/environment-variables.md +++ b/website/docs/reference/environment-variables.md @@ -86,7 +86,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 LLM inference (`mistral` provider), plus Voxtral TTS and Voxtral 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) |