feat(providers): add Mistral embedding provider, route registry through dynaconf
Adds a hosted Mistral embedding option (mistral-embed, 1024-dim) alongside the existing Bedrock / OpenAI / Ollama / Simple providers. Implementation mirrors OpenAIProvider: lazy dimension detection with a known-models lookup, chunked batch requests, defensive index sort, and a 429-aware retry decorator. In the same change, ProviderRegistry switches from os.getenv to the dynaconf-backed Settings dataclass so all five providers share a single configuration path. config.py gains the previously-uncovered Bedrock keys, the new Mistral keys, the missing OPENAI_GENERATION_MODEL / OLLAMA_GENERATION_MODEL, and SIMPLE_EMBEDDING_DIMENSION. Auto-detection priority: Bedrock → OpenAI → Mistral → Ollama → Simple. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
61cadf7935
commit
3268a13d11
@@ -0,0 +1,130 @@
|
||||
"""Unit tests for ProviderRegistry — dynaconf-driven auto-detection."""
|
||||
|
||||
import pytest
|
||||
|
||||
from nextcloud_mcp_server.config import _reload_config
|
||||
from nextcloud_mcp_server.providers import (
|
||||
BedrockProvider,
|
||||
MistralProvider,
|
||||
OllamaProvider,
|
||||
OpenAIProvider,
|
||||
SimpleProvider,
|
||||
get_provider,
|
||||
reset_provider,
|
||||
)
|
||||
from nextcloud_mcp_server.providers.bedrock import BOTO3_AVAILABLE
|
||||
|
||||
|
||||
def _clear_provider_envs(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
"""Strip every provider-selection env var so each test starts clean."""
|
||||
for name in (
|
||||
"AWS_REGION",
|
||||
"AWS_ACCESS_KEY_ID",
|
||||
"AWS_SECRET_ACCESS_KEY",
|
||||
"BEDROCK_EMBEDDING_MODEL",
|
||||
"BEDROCK_GENERATION_MODEL",
|
||||
"OPENAI_API_KEY",
|
||||
"OPENAI_BASE_URL",
|
||||
"OPENAI_EMBEDDING_MODEL",
|
||||
"OPENAI_GENERATION_MODEL",
|
||||
"MISTRAL_API_KEY",
|
||||
"MISTRAL_BASE_URL",
|
||||
"MISTRAL_EMBEDDING_MODEL",
|
||||
"OLLAMA_BASE_URL",
|
||||
"OLLAMA_EMBEDDING_MODEL",
|
||||
"OLLAMA_GENERATION_MODEL",
|
||||
"OLLAMA_VERIFY_SSL",
|
||||
"SIMPLE_EMBEDDING_DIMENSION",
|
||||
):
|
||||
monkeypatch.delenv(name, raising=False)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def clean_provider_env(monkeypatch):
|
||||
"""Reset provider singleton + dynaconf cache around each test."""
|
||||
_clear_provider_envs(monkeypatch)
|
||||
reset_provider()
|
||||
_reload_config()
|
||||
yield monkeypatch
|
||||
reset_provider()
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_registry_falls_back_to_simple(clean_provider_env):
|
||||
"""No provider env set → SimpleProvider (with default dimension)."""
|
||||
provider = get_provider()
|
||||
assert isinstance(provider, SimpleProvider)
|
||||
assert provider.get_dimension() == 384
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_registry_picks_simple_with_custom_dimension(clean_provider_env):
|
||||
"""SIMPLE_EMBEDDING_DIMENSION flows through dynaconf to SimpleProvider."""
|
||||
clean_provider_env.setenv("SIMPLE_EMBEDDING_DIMENSION", "512")
|
||||
_reload_config()
|
||||
|
||||
provider = get_provider()
|
||||
assert isinstance(provider, SimpleProvider)
|
||||
assert provider.get_dimension() == 512
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_registry_picks_mistral_when_api_key_set(clean_provider_env):
|
||||
"""MISTRAL_API_KEY alone is enough to select MistralProvider."""
|
||||
clean_provider_env.setenv("MISTRAL_API_KEY", "test-key")
|
||||
_reload_config()
|
||||
|
||||
provider = get_provider()
|
||||
assert isinstance(provider, MistralProvider)
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_registry_picks_ollama_when_base_url_set(clean_provider_env, mocker):
|
||||
"""OLLAMA_BASE_URL selects OllamaProvider."""
|
||||
# OllamaProvider eagerly probes /api/tags in __init__; stub it out.
|
||||
mocker.patch(
|
||||
"nextcloud_mcp_server.providers.ollama.OllamaProvider._check_model_is_loaded"
|
||||
)
|
||||
clean_provider_env.setenv("OLLAMA_BASE_URL", "http://localhost:11434")
|
||||
_reload_config()
|
||||
|
||||
provider = get_provider()
|
||||
assert isinstance(provider, OllamaProvider)
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_registry_openai_wins_over_mistral_and_ollama(clean_provider_env):
|
||||
"""OpenAI takes priority when multiple provider env vars are set."""
|
||||
clean_provider_env.setenv("OPENAI_API_KEY", "openai-key")
|
||||
clean_provider_env.setenv("MISTRAL_API_KEY", "mistral-key")
|
||||
clean_provider_env.setenv("OLLAMA_BASE_URL", "http://localhost:11434")
|
||||
_reload_config()
|
||||
|
||||
provider = get_provider()
|
||||
assert isinstance(provider, OpenAIProvider)
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_registry_mistral_wins_over_ollama(clean_provider_env):
|
||||
"""Mistral takes priority over Ollama when both are configured."""
|
||||
clean_provider_env.setenv("MISTRAL_API_KEY", "mistral-key")
|
||||
clean_provider_env.setenv("OLLAMA_BASE_URL", "http://localhost:11434")
|
||||
_reload_config()
|
||||
|
||||
provider = get_provider()
|
||||
assert isinstance(provider, MistralProvider)
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_registry_bedrock_wins_when_aws_region_set(clean_provider_env):
|
||||
"""AWS_REGION alone routes to Bedrock, even with other providers configured."""
|
||||
if not BOTO3_AVAILABLE:
|
||||
pytest.skip("boto3 not installed")
|
||||
|
||||
clean_provider_env.setenv("AWS_REGION", "us-east-1")
|
||||
clean_provider_env.setenv("OPENAI_API_KEY", "openai-key")
|
||||
clean_provider_env.setenv("MISTRAL_API_KEY", "mistral-key")
|
||||
_reload_config()
|
||||
|
||||
provider = get_provider()
|
||||
assert isinstance(provider, BedrockProvider)
|
||||
Reference in New Issue
Block a user