feat(tools): add Voxtral Transcribe STT provider (Mistral AI)

This commit is contained in:
jjovalle99
2026-04-06 17:38:25 +01:00
committed by Teknium
parent 5d2fc6d928
commit 5f4b93c20f
7 changed files with 331 additions and 34 deletions

View File

@@ -48,6 +48,7 @@ def clean_env(monkeypatch):
monkeypatch.delenv("VOICE_TOOLS_OPENAI_KEY", raising=False)
monkeypatch.delenv("OPENAI_API_KEY", raising=False)
monkeypatch.delenv("GROQ_API_KEY", raising=False)
monkeypatch.delenv("MISTRAL_API_KEY", raising=False)
monkeypatch.delenv("HERMES_LOCAL_STT_COMMAND", raising=False)
monkeypatch.delenv("HERMES_LOCAL_STT_LANGUAGE", raising=False)
@@ -858,3 +859,183 @@ class TestGetSttModelFromConfig:
from tools.transcription_tools import get_stt_model_from_config
assert get_stt_model_from_config() is None
# ============================================================================
# _transcribe_mistral
# ============================================================================
@pytest.fixture
def mock_mistral_module():
"""Inject a fake mistralai module into sys.modules for testing."""
mock_client = MagicMock()
mock_client.__enter__ = MagicMock(return_value=mock_client)
mock_client.__exit__ = MagicMock(return_value=False)
mock_mistral_cls = MagicMock(return_value=mock_client)
fake_module = MagicMock()
fake_module.Mistral = mock_mistral_cls
with patch.dict("sys.modules", {"mistralai": fake_module}):
yield mock_client
class TestTranscribeMistral:
def test_no_key(self, monkeypatch):
monkeypatch.delenv("MISTRAL_API_KEY", raising=False)
from tools.transcription_tools import _transcribe_mistral
result = _transcribe_mistral("/tmp/test.ogg", "voxtral-mini-latest")
assert result["success"] is False
assert "MISTRAL_API_KEY" in result["error"]
def test_successful_transcription(self, monkeypatch, sample_ogg, mock_mistral_module):
monkeypatch.setenv("MISTRAL_API_KEY", "test-key")
mock_result = MagicMock()
mock_result.text = "hello from mistral"
mock_mistral_module.audio.transcriptions.complete.return_value = mock_result
from tools.transcription_tools import _transcribe_mistral
result = _transcribe_mistral(sample_ogg, "voxtral-mini-latest")
assert result["success"] is True
assert result["transcript"] == "hello from mistral"
assert result["provider"] == "mistral"
mock_mistral_module.audio.transcriptions.complete.assert_called_once()
mock_mistral_module.__exit__.assert_called_once()
def test_api_error_returns_failure(self, monkeypatch, sample_ogg, mock_mistral_module):
monkeypatch.setenv("MISTRAL_API_KEY", "test-key")
mock_mistral_module.audio.transcriptions.complete.side_effect = RuntimeError("secret-key-leaked")
from tools.transcription_tools import _transcribe_mistral
result = _transcribe_mistral(sample_ogg, "voxtral-mini-latest")
assert result["success"] is False
assert "RuntimeError" in result["error"]
assert "secret-key-leaked" not in result["error"]
def test_permission_error(self, monkeypatch, sample_ogg, mock_mistral_module):
monkeypatch.setenv("MISTRAL_API_KEY", "test-key")
mock_mistral_module.audio.transcriptions.complete.side_effect = PermissionError("denied")
from tools.transcription_tools import _transcribe_mistral
result = _transcribe_mistral(sample_ogg, "voxtral-mini-latest")
assert result["success"] is False
assert "Permission denied" in result["error"]
# ============================================================================
# _get_provider — Mistral
# ============================================================================
class TestGetProviderMistral:
"""Mistral-specific provider selection tests."""
def test_mistral_when_key_and_sdk_available(self, monkeypatch):
monkeypatch.setenv("MISTRAL_API_KEY", "test-key")
with patch("tools.transcription_tools._HAS_MISTRAL", True):
from tools.transcription_tools import _get_provider
assert _get_provider({"provider": "mistral"}) == "mistral"
def test_mistral_explicit_no_key_returns_none(self, monkeypatch):
"""Explicit mistral with no key returns none — no cross-provider fallback."""
monkeypatch.delenv("MISTRAL_API_KEY", raising=False)
with patch("tools.transcription_tools._HAS_MISTRAL", True):
from tools.transcription_tools import _get_provider
assert _get_provider({"provider": "mistral"}) == "none"
def test_mistral_explicit_no_sdk_returns_none(self, monkeypatch):
"""Explicit mistral with key but no SDK returns none."""
monkeypatch.setenv("MISTRAL_API_KEY", "test-key")
with patch("tools.transcription_tools._HAS_MISTRAL", False):
from tools.transcription_tools import _get_provider
assert _get_provider({"provider": "mistral"}) == "none"
def test_auto_detect_mistral_after_openai(self, monkeypatch):
"""Auto-detect: mistral is tried after openai when both are unavailable."""
monkeypatch.delenv("GROQ_API_KEY", raising=False)
monkeypatch.delenv("VOICE_TOOLS_OPENAI_KEY", raising=False)
monkeypatch.delenv("OPENAI_API_KEY", raising=False)
monkeypatch.setenv("MISTRAL_API_KEY", "test-key")
with patch("tools.transcription_tools._HAS_FASTER_WHISPER", False), \
patch("tools.transcription_tools._has_local_command", return_value=False), \
patch("tools.transcription_tools._HAS_OPENAI", False), \
patch("tools.transcription_tools._HAS_MISTRAL", True):
from tools.transcription_tools import _get_provider
assert _get_provider({}) == "mistral"
def test_auto_detect_openai_preferred_over_mistral(self, monkeypatch):
"""Auto-detect: openai is preferred over mistral (both paid, openai more common)."""
monkeypatch.setenv("VOICE_TOOLS_OPENAI_KEY", "sk-test")
monkeypatch.setenv("MISTRAL_API_KEY", "test-key")
monkeypatch.delenv("GROQ_API_KEY", raising=False)
with patch("tools.transcription_tools._HAS_FASTER_WHISPER", False), \
patch("tools.transcription_tools._has_local_command", return_value=False), \
patch("tools.transcription_tools._HAS_OPENAI", True), \
patch("tools.transcription_tools._HAS_MISTRAL", True):
from tools.transcription_tools import _get_provider
assert _get_provider({}) == "openai"
def test_auto_detect_groq_preferred_over_mistral(self, monkeypatch):
"""Auto-detect: groq (free) is preferred over mistral (paid)."""
monkeypatch.setenv("GROQ_API_KEY", "gsk-test")
monkeypatch.setenv("MISTRAL_API_KEY", "test-key")
with patch("tools.transcription_tools._HAS_FASTER_WHISPER", False), \
patch("tools.transcription_tools._has_local_command", return_value=False), \
patch("tools.transcription_tools._HAS_OPENAI", True), \
patch("tools.transcription_tools._HAS_MISTRAL", True):
from tools.transcription_tools import _get_provider
assert _get_provider({}) == "groq"
def test_auto_detect_skips_mistral_without_sdk(self, monkeypatch):
"""Auto-detect: mistral skipped when key is set but SDK is not installed."""
monkeypatch.delenv("GROQ_API_KEY", raising=False)
monkeypatch.delenv("VOICE_TOOLS_OPENAI_KEY", raising=False)
monkeypatch.delenv("OPENAI_API_KEY", raising=False)
monkeypatch.setenv("MISTRAL_API_KEY", "test-key")
with patch("tools.transcription_tools._HAS_FASTER_WHISPER", False), \
patch("tools.transcription_tools._has_local_command", return_value=False), \
patch("tools.transcription_tools._HAS_OPENAI", False), \
patch("tools.transcription_tools._HAS_MISTRAL", False):
from tools.transcription_tools import _get_provider
assert _get_provider({}) == "none"
# ============================================================================
# transcribe_audio — Mistral dispatch
# ============================================================================
class TestTranscribeAudioMistralDispatch:
def test_dispatches_to_mistral(self, sample_ogg):
with patch("tools.transcription_tools._load_stt_config", return_value={"provider": "mistral"}), \
patch("tools.transcription_tools._get_provider", return_value="mistral"), \
patch("tools.transcription_tools._transcribe_mistral",
return_value={"success": True, "transcript": "hi", "provider": "mistral"}) as mock_mistral:
from tools.transcription_tools import transcribe_audio
result = transcribe_audio(sample_ogg)
assert result["success"] is True
assert result["provider"] == "mistral"
mock_mistral.assert_called_once()
def test_config_mistral_model_used(self, sample_ogg):
config = {"provider": "mistral", "mistral": {"model": "voxtral-mini-2602"}}
with patch("tools.transcription_tools._load_stt_config", return_value=config), \
patch("tools.transcription_tools._get_provider", return_value="mistral"), \
patch("tools.transcription_tools._transcribe_mistral",
return_value={"success": True, "transcript": "hi"}) as mock_mistral:
from tools.transcription_tools import transcribe_audio
transcribe_audio(sample_ogg, model=None)
assert mock_mistral.call_args[0][1] == "voxtral-mini-2602"
def test_model_override_passed_to_mistral(self, sample_ogg):
with patch("tools.transcription_tools._load_stt_config", return_value={}), \
patch("tools.transcription_tools._get_provider", return_value="mistral"), \
patch("tools.transcription_tools._transcribe_mistral",
return_value={"success": True, "transcript": "hi"}) as mock_mistral:
from tools.transcription_tools import transcribe_audio
transcribe_audio(sample_ogg, model="voxtral-mini-2602")
assert mock_mistral.call_args[0][1] == "voxtral-mini-2602"