diff --git a/docs/docs/providers/inference/remote_mistral.mdx b/docs/docs/providers/inference/remote_mistral.mdx new file mode 100644 index 00000000000..d86d997dbe3 --- /dev/null +++ b/docs/docs/providers/inference/remote_mistral.mdx @@ -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:=} +``` diff --git a/src/ogx/providers/registry/inference.py b/src/ogx/providers/registry/inference.py index a7362b9454d..bb6ac5e1d5b 100644 --- a/src/ogx/providers/registry/inference.py +++ b/src/ogx/providers/registry/inference.py @@ -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", diff --git a/src/ogx/providers/remote/inference/mistral/__init__.py b/src/ogx/providers/remote/inference/mistral/__init__.py new file mode 100644 index 00000000000..2c315652c89 --- /dev/null +++ b/src/ogx/providers/remote/inference/mistral/__init__.py @@ -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 diff --git a/src/ogx/providers/remote/inference/mistral/config.py b/src/ogx/providers/remote/inference/mistral/config.py new file mode 100644 index 00000000000..a9cebc5042a --- /dev/null +++ b/src/ogx/providers/remote/inference/mistral/config.py @@ -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, + } diff --git a/src/ogx/providers/remote/inference/mistral/mistral.py b/src/ogx/providers/remote/inference/mistral/mistral.py new file mode 100644 index 00000000000..9bcfc495d3b --- /dev/null +++ b/src/ogx/providers/remote/inference/mistral/mistral.py @@ -0,0 +1,29 @@ +# 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 ogx.providers.utils.inference.openai_mixin import OpenAIMixin + +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) diff --git a/tests/unit/providers/inference/test_mistral_config.py b/tests/unit/providers/inference/test_mistral_config.py new file mode 100644 index 00000000000..9a4d5f9c539 --- /dev/null +++ b/tests/unit/providers/inference/test_mistral_config.py @@ -0,0 +1,50 @@ +# 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 patch + +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 + + +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