Skip to content
Merged
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
44 changes: 44 additions & 0 deletions docs/docs/providers/inference/remote_mistral.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
---
description: "Mistral AI inference provider for accessing Mistral models via the Mistral API."
sidebar_label: Remote - Mistral
title: remote::mistral
---

# remote::mistral

## Description

Mistral AI inference provider for accessing Mistral models via the Mistral API.

## Configuration

| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `allowed_models` | `list[str] \| None` | No | | List of models that should be registered with the model registry. If None, all models are allowed. |
| `refresh_models` | `bool` | No | False | Whether to refresh models periodically from the provider |
| `api_key` | `SecretStr \| None` | No | | Authentication credential for the provider |
| `network` | `NetworkConfig \| None` | No | | Network configuration including TLS, proxy, and timeout settings. |
| `network.tls` | `TLSConfig \| None` | No | | TLS/SSL configuration for secure connections. |
| `network.tls.verify` | `bool \| Path` | No | True | Whether to verify TLS certificates. Can be a boolean or a path to a CA certificate file. |
| `network.tls.min_version` | `Literal[TLSv1.2, TLSv1.3] \| None` | No | | Minimum TLS version to use. Defaults to system default if not specified. |
| `network.tls.ciphers` | `list[str] \| None` | No | | List of allowed cipher suites (e.g., ['ECDHE+AESGCM', 'DHE+AESGCM']). |
| `network.tls.client_cert` | `Path \| None` | No | | Path to client certificate file for mTLS authentication. |
| `network.tls.client_key` | `Path \| None` | No | | Path to client private key file for mTLS authentication. |
| `network.proxy` | `ProxyConfig \| None` | No | | Proxy configuration for HTTP connections. |
| `network.proxy.url` | `HttpUrl \| None` | No | | Single proxy URL for all connections (e.g., 'http://proxy.example.com:8080'). |
| `network.proxy.http` | `HttpUrl \| None` | No | | Proxy URL for HTTP connections. |
| `network.proxy.https` | `HttpUrl \| None` | No | | Proxy URL for HTTPS connections. |
| `network.proxy.cacert` | `Path \| None` | No | | Path to CA certificate file for verifying the proxy's certificate. Required for proxies in interception mode. |
| `network.proxy.no_proxy` | `list[str] \| None` | No | | List of hosts that should bypass the proxy (e.g., ['localhost', '127.0.0.1', '.internal.corp']). |
| `network.timeout` | `float \| TimeoutConfig \| None` | No | | Timeout configuration. Can be a float (for both connect and read) or a TimeoutConfig object with separate connect and read timeouts. |
| `network.timeout.connect` | `float \| None` | No | | Connection timeout in seconds. |
| `network.timeout.read` | `float \| None` | No | | Read timeout in seconds. |
| `network.headers` | `dict[str, str] \| None` | No | | Additional HTTP headers to include in all requests. |
| `base_url` | `HttpUrl \| None` | No | https://api.mistral.ai/v1 | Base URL for the Mistral API |

## Sample Configuration

```yaml
base_url: https://api.mistral.ai/v1
api_key: ${env.MISTRAL_API_KEY:=}
```
1 change: 1 addition & 0 deletions scripts/generate_target_models_docs.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@
# These inference providers exist in the registry but do not have named
# integration-test setups yet, so they are intentionally excluded from this doc.
INTENTIONALLY_UNMAPPED_REGISTRY_PROVIDERS = {
"mistral",
"meta",
"nvidia",
"oci",
Expand Down
10 changes: 10 additions & 0 deletions src/ogx/providers/registry/inference.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,16 @@ def available_providers() -> list[ProviderSpec]:
provider_data_validator="ogx.providers.remote.inference.cerebras.config.CerebrasProviderDataValidator",
description="Cerebras inference provider for running models on Cerebras Cloud platform.",
),
RemoteProviderSpec(
api=Api.inference,
adapter_type="mistral",
provider_type="remote::mistral",
pip_packages=[],
module="ogx.providers.remote.inference.mistral",
config_class="ogx.providers.remote.inference.mistral.MistralImplConfig",
provider_data_validator="ogx.providers.remote.inference.mistral.config.MistralProviderDataValidator",
description="Mistral AI inference provider for accessing Mistral models via the Mistral API.",
),
RemoteProviderSpec(
api=Api.inference,
adapter_type="ollama",
Expand Down
19 changes: 19 additions & 0 deletions src/ogx/providers/remote/inference/mistral/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Copyright (c) The OGX Contributors.
# All rights reserved.
#
# This source code is licensed under the terms described in the LICENSE file in
# the root directory of this source tree.

from .config import MistralImplConfig


async def get_adapter_impl(config: MistralImplConfig, _deps):
from .mistral import MistralInferenceAdapter

assert isinstance(config, MistralImplConfig), f"Unexpected config type: {type(config)}"

impl = MistralInferenceAdapter(config=config)

await impl.initialize()

return impl
41 changes: 41 additions & 0 deletions src/ogx/providers/remote/inference/mistral/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Copyright (c) The OGX Contributors.
# All rights reserved.
#
# This source code is licensed under the terms described in the LICENSE file in
# the root directory of this source tree.

import os
from typing import Any

from pydantic import BaseModel, Field, HttpUrl, SecretStr

from ogx.providers.utils.inference.model_registry import RemoteInferenceProviderConfig
from ogx_api import json_schema_type

DEFAULT_BASE_URL = "https://api.mistral.ai/v1"


class MistralProviderDataValidator(BaseModel):
"""Validates provider-specific request data for Mistral inference."""

mistral_api_key: SecretStr | None = Field(
default=None,
description="API key for Mistral models",
)


@json_schema_type
class MistralImplConfig(RemoteInferenceProviderConfig):
"""Configuration for the Mistral inference provider."""

base_url: HttpUrl | None = Field(
default=HttpUrl(os.environ.get("MISTRAL_BASE_URL", DEFAULT_BASE_URL)),
description="Base URL for the Mistral API",
)

@classmethod
def sample_run_config(cls, api_key: str = "${env.MISTRAL_API_KEY:=}", **kwargs) -> dict[str, Any]:
return {
"base_url": DEFAULT_BASE_URL,
"api_key": api_key,
}
61 changes: 61 additions & 0 deletions src/ogx/providers/remote/inference/mistral/mistral.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# Copyright (c) The OGX Contributors.
# All rights reserved.
#
# This source code is licensed under the terms described in the LICENSE file in
# the root directory of this source tree.

from collections.abc import AsyncIterator

from ogx.providers.utils.inference.openai_mixin import OpenAIMixin
from ogx_api import (
OpenAICompletion,
OpenAICompletionRequestWithExtraBody,
OpenAIEmbeddingsRequestWithExtraBody,
OpenAIEmbeddingsResponse,
)

from .config import MistralImplConfig


class MistralInferenceAdapter(OpenAIMixin):
"""Inference adapter for the Mistral AI platform.

Mistral exposes an OpenAI-compatible API (chat completions and embeddings),
so the shared `OpenAIMixin` handles requests once pointed at Mistral's base
URL. See https://docs.mistral.ai/api/.
"""

config: MistralImplConfig

provider_data_api_key_field: str = "mistral_api_key"

embedding_model_metadata: dict[str, dict[str, int]] = {
"mistral-embed": {"embedding_dimension": 1024, "context_length": 8192},
}

def get_base_url(self) -> str:
return str(self.config.base_url)

async def openai_embeddings(
self,
params: OpenAIEmbeddingsRequestWithExtraBody,
) -> OpenAIEmbeddingsResponse:
"""Mistral's embeddings endpoint does not support encoding_format.

Mistral accepts the param but ignores it, always returning a list of
floats. Rather than silently accepting a misleading parameter, we
reject base64 requests explicitly so users get a clear error.
"""
if params.encoding_format == "base64":
raise ValueError("Mistral's embeddings endpoint does not support encoding_format='base64'.")

return await super().openai_embeddings(params)

async def openai_completion(
self,
params: OpenAICompletionRequestWithExtraBody,
) -> OpenAICompletion | AsyncIterator[OpenAICompletion]:
"""Mistral does not support the legacy /v1/completions endpoint."""
raise NotImplementedError(
"Mistral does not support /v1/completions endpoint. Only /v1/chat/completions is supported. "
)
84 changes: 84 additions & 0 deletions tests/unit/providers/inference/test_mistral_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# Copyright (c) The OGX Contributors.
# All rights reserved.
#
# This source code is licensed under the terms described in the LICENSE file in
# the root directory of this source tree.

import os
from unittest.mock import AsyncMock, patch

import pytest

from ogx.core.stack import replace_env_vars
from ogx.providers.remote.inference.mistral.config import MistralImplConfig
from ogx.providers.remote.inference.mistral.mistral import MistralInferenceAdapter
from ogx_api import (
OpenAICompletionRequestWithExtraBody,
OpenAIEmbeddingsRequestWithExtraBody,
)


class TestMistralConfig:
"""Tests for the Mistral inference provider config and adapter wiring."""

def test_default_base_url(self):
config = MistralImplConfig(api_key="test-key")
adapter = MistralInferenceAdapter(config=config)
adapter.provider_data_api_key_field = None

assert adapter.get_base_url() == "https://api.mistral.ai/v1"

def test_custom_base_url_from_config(self):
custom_url = "https://custom.mistral.ai/v1"
config = MistralImplConfig(api_key="test-key", base_url=custom_url)
adapter = MistralInferenceAdapter(config=config)
adapter.provider_data_api_key_field = None

assert adapter.get_base_url() == custom_url

@patch.dict(os.environ, {"MISTRAL_BASE_URL": "https://env.mistral.ai/v1"})
def test_base_url_from_environment_variable(self):
config_data = MistralImplConfig.sample_run_config(api_key="test-key")
processed_config = replace_env_vars(config_data)
config = MistralImplConfig.model_validate(processed_config)

assert str(config.base_url) == "https://api.mistral.ai/v1"

def test_sample_run_config_uses_env_placeholder(self):
cfg = MistralImplConfig.sample_run_config()
assert cfg["base_url"] == "https://api.mistral.ai/v1"
assert cfg["api_key"] == "${env.MISTRAL_API_KEY:=}"

def test_provider_data_api_key_field(self):
config = MistralImplConfig(api_key="test-key")
adapter = MistralInferenceAdapter(config=config)
assert adapter.provider_data_api_key_field == "mistral_api_key"
assert "mistral-embed" in adapter.embedding_model_metadata

async def test_base64_encoding_format_raises_value_error(self):
"""Mistral's embeddings endpoint does not support encoding_format='base64'."""
config = MistralImplConfig(api_key="test-key", base_url="https://api.mistral.ai/v1")
adapter = MistralInferenceAdapter(config=config)
adapter.provider_data_api_key_field = None
adapter.model_store = AsyncMock()
adapter.model_store.has_model = AsyncMock(return_value=True)
adapter.model_store.get_model = AsyncMock()

params = OpenAIEmbeddingsRequestWithExtraBody(
model="mistral-embed",
input="hello world",
encoding_format="base64",
)

with pytest.raises(ValueError, match="does not support encoding_format='base64'"):
await adapter.openai_embeddings(params)

async def test_legacy_completions_endpoint_not_supported(self):
"""Mistral does not support the legacy /v1/completions endpoint."""
config = MistralImplConfig(api_key="test-key", base_url="https://api.mistral.ai/v1")
adapter = MistralInferenceAdapter(config=config)

params = OpenAICompletionRequestWithExtraBody(model="mistral-small", prompt="Hello")

with pytest.raises(NotImplementedError, match="does not support /v1/completions endpoint"):
await adapter.openai_completion(params)
Loading