mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-18 04:41:56 +00:00
fix(deps): unbreak [all] install — drop mistralai while PyPI quarantined (#24205)
The `mistralai` PyPI package was quarantined on 2026-05-12 after a malicious 2.4.6 release. Every fresh resolve (AUR makepkg, Docker build, CI run, install.sh first-run) currently fails on `mistralai>=2.3.0,<3` because PyPI returns zero candidates. Existing users running `hermes update` mostly didn't notice — `hermes update` falls back from `.[all]` to per-extra retries and silently skips mistral with a warning that scrolls past. But fresh installs hard-fail or lose every other extra. Changes: - pyproject.toml: drop `hermes-agent[mistral]` from `[all]` and `[termux-all]`. The `mistral` extra itself is preserved so users can opt back in once PyPI un-quarantines. - hermes_cli/tools_config.py: hide Mistral Voxtral TTS from the `hermes tools` provider picker until restored. - hermes_cli/web_server.py: drop "mistral" from dashboard STT options. - tools/transcription_tools.py: explicit `provider: mistral` returns "none" with a clear status message; auto-detect skips mistral. - tools/tts_tool.py: dispatcher returns a clear "temporarily disabled" error before any SDK import attempt (avoids cached-stale-package surprises). - tests/tools/: update three test files to assert the new disabled behavior. Each test docstring records why and points at the rollback trigger (PyPI un-quarantines mistralai). Restore plan: revert this commit once the package is available on PyPI again. The behavior change is intentional and documented in code comments + test docstrings to make the rollback trivial. Validation: - scripts/run_tests.sh tests/tools/ -k 'mistral or stt or tts' → 425/425 passing. Refs: https://pypi.org/simple/mistralai/ (currently "pypi:project-status: quarantined").
This commit is contained in:
parent
407683b72d
commit
99ad2d1372
8 changed files with 90 additions and 48 deletions
|
|
@ -205,15 +205,9 @@ TOOL_CATEGORIES = {
|
|||
],
|
||||
"tts_provider": "elevenlabs",
|
||||
},
|
||||
{
|
||||
"name": "Mistral (Voxtral TTS)",
|
||||
"badge": "paid",
|
||||
"tag": "Multilingual, native Opus",
|
||||
"env_vars": [
|
||||
{"key": "MISTRAL_API_KEY", "prompt": "Mistral API key", "url": "https://console.mistral.ai/"},
|
||||
],
|
||||
"tts_provider": "mistral",
|
||||
},
|
||||
# Mistral (Voxtral TTS) temporarily hidden — `mistralai` PyPI
|
||||
# package is currently quarantined (malicious 2.4.6 release on
|
||||
# 2026-05-12). Restore this entry once PyPI un-quarantines.
|
||||
{
|
||||
"name": "Google Gemini TTS",
|
||||
"badge": "preview",
|
||||
|
|
|
|||
|
|
@ -273,7 +273,9 @@ _SCHEMA_OVERRIDES: Dict[str, Dict[str, Any]] = {
|
|||
"stt.provider": {
|
||||
"type": "select",
|
||||
"description": "Speech-to-text provider",
|
||||
"options": ["local", "openai", "mistral"],
|
||||
# "mistral" temporarily removed — mistralai PyPI package quarantined
|
||||
# (malicious 2.4.6 release on 2026-05-12). Restore once available.
|
||||
"options": ["local", "openai"],
|
||||
},
|
||||
"display.skin": {
|
||||
"type": "select",
|
||||
|
|
|
|||
|
|
@ -111,7 +111,10 @@ termux-all = [
|
|||
"hermes-agent[dingtalk]",
|
||||
"hermes-agent[feishu]",
|
||||
"hermes-agent[google]",
|
||||
"hermes-agent[mistral]",
|
||||
# mistral: omitted from broad termux-all profile — `mistralai` PyPI package
|
||||
# is currently quarantined (malicious 2.4.6 release). Users who explicitly
|
||||
# want Voxtral STT/TTS can still `pip install hermes-agent[mistral]`
|
||||
# directly once PyPI un-quarantines.
|
||||
"hermes-agent[bedrock]",
|
||||
"hermes-agent[homeassistant]",
|
||||
"hermes-agent[sms]",
|
||||
|
|
@ -169,7 +172,11 @@ all = [
|
|||
"hermes-agent[dingtalk]",
|
||||
"hermes-agent[feishu]",
|
||||
"hermes-agent[google]",
|
||||
"hermes-agent[mistral]",
|
||||
# mistral: omitted from [all] — `mistralai` PyPI package is currently
|
||||
# quarantined (malicious 2.4.6 release on 2026-05-12). Pulling it from
|
||||
# [all] would break every fresh install / AUR build / Docker build / CI
|
||||
# run until PyPI un-quarantines. Users who explicitly want Voxtral STT/TTS
|
||||
# can still `pip install hermes-agent[mistral]` once it's available again.
|
||||
"hermes-agent[bedrock]",
|
||||
"hermes-agent[web]",
|
||||
"hermes-agent[youtube]",
|
||||
|
|
|
|||
|
|
@ -69,6 +69,12 @@ class TestProviderSelectionGate:
|
|||
assert tt._get_provider({"enabled": True, "provider": "groq"}) == "groq"
|
||||
|
||||
def test_explicit_mistral_sees_dotenv(self):
|
||||
"""Mistral STT is intentionally disabled (PyPI quarantine 2026-05-12).
|
||||
|
||||
Even with the dotenv key visible, explicit `provider: mistral` must
|
||||
return "none" with a warning. Restore the previous behavior once
|
||||
`mistralai` is un-quarantined on PyPI.
|
||||
"""
|
||||
from tools import transcription_tools as tt
|
||||
|
||||
with patch.object(tt, "_HAS_FASTER_WHISPER", False), \
|
||||
|
|
@ -76,7 +82,7 @@ class TestProviderSelectionGate:
|
|||
patch.object(tt, "_has_local_command", return_value=False), \
|
||||
patch("hermes_cli.config.load_env",
|
||||
return_value={"MISTRAL_API_KEY": "dotenv-secret"}):
|
||||
assert tt._get_provider({"enabled": True, "provider": "mistral"}) == "mistral"
|
||||
assert tt._get_provider({"enabled": True, "provider": "mistral"}) == "none"
|
||||
|
||||
def test_explicit_xai_sees_dotenv(self):
|
||||
from tools import transcription_tools as tt
|
||||
|
|
|
|||
|
|
@ -978,16 +978,23 @@ class TestTranscribeMistral:
|
|||
# ============================================================================
|
||||
|
||||
class TestGetProviderMistral:
|
||||
"""Mistral-specific provider selection tests."""
|
||||
"""Mistral-specific provider selection tests.
|
||||
|
||||
Mistral STT is intentionally disabled in 2026-05-12+ while the
|
||||
`mistralai` PyPI package is quarantined. These tests document that
|
||||
explicit `provider: mistral` always returns "none" with a warning, and
|
||||
that auto-detect skips mistral entirely.
|
||||
"""
|
||||
|
||||
def test_mistral_when_key_and_sdk_available(self, monkeypatch):
|
||||
"""Even with key + SDK, explicit mistral returns 'none' (disabled)."""
|
||||
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"
|
||||
assert _get_provider({"provider": "mistral"}) == "none"
|
||||
|
||||
def test_mistral_explicit_no_key_returns_none(self, monkeypatch):
|
||||
"""Explicit mistral with no key returns none — no cross-provider fallback."""
|
||||
"""Explicit mistral with no key returns none."""
|
||||
monkeypatch.delenv("MISTRAL_API_KEY", raising=False)
|
||||
with patch("tools.transcription_tools._HAS_MISTRAL", True):
|
||||
from tools.transcription_tools import _get_provider
|
||||
|
|
@ -1000,18 +1007,23 @@ class TestGetProviderMistral:
|
|||
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."""
|
||||
def test_auto_detect_skips_mistral(self, monkeypatch):
|
||||
"""Auto-detect intentionally skips mistral (quarantine workaround).
|
||||
|
||||
With no other provider available but MISTRAL_API_KEY set, the result
|
||||
must be 'none' — mistral is no longer in the auto-detect chain.
|
||||
"""
|
||||
monkeypatch.delenv("GROQ_API_KEY", raising=False)
|
||||
monkeypatch.delenv("VOICE_TOOLS_OPENAI_KEY", raising=False)
|
||||
monkeypatch.delenv("OPENAI_API_KEY", raising=False)
|
||||
monkeypatch.delenv("XAI_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"
|
||||
assert _get_provider({}) == "none"
|
||||
|
||||
def test_auto_detect_openai_preferred_over_mistral(self, monkeypatch):
|
||||
"""Auto-detect: openai is preferred over mistral (both paid, openai more common)."""
|
||||
|
|
@ -1285,8 +1297,13 @@ class TestGetProviderXAI:
|
|||
from tools.transcription_tools import _get_provider
|
||||
assert _get_provider({}) == "xai"
|
||||
|
||||
def test_auto_detect_mistral_preferred_over_xai(self, monkeypatch):
|
||||
"""Auto-detect: mistral is preferred over xai."""
|
||||
def test_auto_detect_mistral_skipped_xai_wins(self, monkeypatch):
|
||||
"""Auto-detect skips mistral entirely (quarantine) — xai wins.
|
||||
|
||||
Even with MISTRAL_API_KEY set, mistral is no longer in the
|
||||
auto-detect chain. xai is the next-best fallback when the
|
||||
local/groq/openai chain is unavailable.
|
||||
"""
|
||||
monkeypatch.setenv("MISTRAL_API_KEY", "test-key")
|
||||
monkeypatch.setenv("XAI_API_KEY", "xai-test")
|
||||
monkeypatch.delenv("GROQ_API_KEY", raising=False)
|
||||
|
|
@ -1297,7 +1314,7 @@ class TestGetProviderXAI:
|
|||
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"
|
||||
assert _get_provider({}) == "xai"
|
||||
|
||||
def test_auto_detect_no_key_returns_none(self, monkeypatch):
|
||||
"""Auto-detect: xai skipped when no key is set."""
|
||||
|
|
|
|||
|
|
@ -162,27 +162,34 @@ class TestGenerateMistralTts:
|
|||
|
||||
|
||||
class TestTtsDispatcherMistral:
|
||||
def test_dispatcher_routes_to_mistral(
|
||||
def test_dispatcher_returns_disabled_error(
|
||||
self, tmp_path, mock_mistral_module, monkeypatch
|
||||
):
|
||||
"""Mistral TTS is intentionally disabled (PyPI quarantine 2026-05-12).
|
||||
|
||||
The dispatcher must short-circuit with a clear status message before
|
||||
attempting any SDK import, even when MISTRAL_API_KEY is set and a
|
||||
mock SDK is wired in. Restore routing once `mistralai` is
|
||||
un-quarantined on PyPI.
|
||||
"""
|
||||
import json
|
||||
|
||||
from tools.tts_tool import text_to_speech_tool
|
||||
|
||||
monkeypatch.setenv("MISTRAL_API_KEY", "test-key")
|
||||
mock_mistral_module.audio.speech.complete.return_value = MagicMock(
|
||||
audio_data=base64.b64encode(b"audio").decode()
|
||||
)
|
||||
|
||||
output_path = str(tmp_path / "out.mp3")
|
||||
with patch("tools.tts_tool._load_tts_config", return_value={"provider": "mistral"}):
|
||||
result = json.loads(text_to_speech_tool("Hello", output_path=output_path))
|
||||
|
||||
assert result["success"] is True
|
||||
assert result["provider"] == "mistral"
|
||||
mock_mistral_module.audio.speech.complete.assert_called_once()
|
||||
assert result["success"] is False
|
||||
assert "temporarily disabled" in result["error"]
|
||||
assert "quarantined" in result["error"]
|
||||
# SDK must not have been called.
|
||||
mock_mistral_module.audio.speech.complete.assert_not_called()
|
||||
|
||||
def test_dispatcher_returns_error_when_sdk_not_installed(self, tmp_path, monkeypatch):
|
||||
"""Same disabled message regardless of SDK presence."""
|
||||
import json
|
||||
|
||||
from tools.tts_tool import text_to_speech_tool
|
||||
|
|
@ -196,7 +203,7 @@ class TestTtsDispatcherMistral:
|
|||
)
|
||||
|
||||
assert result["success"] is False
|
||||
assert "mistralai" in result["error"]
|
||||
assert "temporarily disabled" in result["error"]
|
||||
|
||||
|
||||
class TestCheckTtsRequirementsMistral:
|
||||
|
|
|
|||
|
|
@ -252,11 +252,16 @@ def _get_provider(stt_config: dict) -> str:
|
|||
return "none"
|
||||
|
||||
if provider == "mistral":
|
||||
if _HAS_MISTRAL and get_env_value("MISTRAL_API_KEY"):
|
||||
return "mistral"
|
||||
# `mistralai` PyPI package was quarantined on 2026-05-12 after a
|
||||
# malicious 2.4.6 release. Refuse to use this provider until it's
|
||||
# available again so we surface a clear message instead of an
|
||||
# opaque ImportError mid-call.
|
||||
logger.warning(
|
||||
"STT provider 'mistral' configured but mistralai package "
|
||||
"not installed or MISTRAL_API_KEY not set"
|
||||
"STT provider 'mistral' (Voxtral Transcribe) is temporarily "
|
||||
"disabled — `mistralai` PyPI package is quarantined "
|
||||
"(malicious 2.4.6 release on 2026-05-12). Falling back to "
|
||||
"another provider. Set stt.provider in config.yaml to 'local' "
|
||||
"or 'openai' to silence this warning."
|
||||
)
|
||||
return "none"
|
||||
|
||||
|
|
@ -270,7 +275,9 @@ def _get_provider(stt_config: dict) -> str:
|
|||
|
||||
return provider # Unknown — let it fail downstream
|
||||
|
||||
# --- Auto-detect (no explicit provider): local > groq > openai > mistral > xai -
|
||||
# --- Auto-detect (no explicit provider): local > groq > openai > xai ---
|
||||
# mistral is intentionally skipped while `mistralai` is quarantined on
|
||||
# PyPI (malicious 2.4.6 release on 2026-05-12).
|
||||
|
||||
if _HAS_FASTER_WHISPER:
|
||||
return "local"
|
||||
|
|
@ -282,9 +289,6 @@ def _get_provider(stt_config: dict) -> str:
|
|||
if _HAS_OPENAI and _has_openai_audio_backend():
|
||||
logger.info("No local STT available, using OpenAI Whisper API")
|
||||
return "openai"
|
||||
if _HAS_MISTRAL and get_env_value("MISTRAL_API_KEY"):
|
||||
logger.info("No local STT available, using Mistral Voxtral Transcribe API")
|
||||
return "mistral"
|
||||
if get_env_value("XAI_API_KEY"):
|
||||
logger.info("No local STT available, using xAI Grok STT API")
|
||||
return "xai"
|
||||
|
|
|
|||
|
|
@ -1662,16 +1662,21 @@ def text_to_speech_tool(
|
|||
_generate_xai_tts(text, file_str, tts_config)
|
||||
|
||||
elif provider == "mistral":
|
||||
try:
|
||||
_import_mistral_client()
|
||||
except ImportError:
|
||||
return json.dumps({
|
||||
"success": False,
|
||||
"error": "Mistral provider selected but 'mistralai' package not installed. "
|
||||
"Run: pip install 'hermes-agent[mistral]'"
|
||||
}, ensure_ascii=False)
|
||||
logger.info("Generating speech with Mistral Voxtral TTS...")
|
||||
_generate_mistral_tts(text, file_str, tts_config)
|
||||
# `mistralai` PyPI package was quarantined on 2026-05-12 after a
|
||||
# malicious 2.4.6 release. Surface a clear status message instead
|
||||
# of attempting an import that would either fail or pull a stale
|
||||
# cached package.
|
||||
return json.dumps({
|
||||
"success": False,
|
||||
"error": (
|
||||
"Mistral Voxtral TTS is temporarily disabled. The "
|
||||
"`mistralai` PyPI package was quarantined on 2026-05-12 "
|
||||
"after a malicious 2.4.6 release. Switch tts.provider in "
|
||||
"config.yaml to 'edge', 'elevenlabs', 'openai', 'minimax', "
|
||||
"'gemini', 'xai', 'neutts', or 'kittentts'. Mistral "
|
||||
"support will return once PyPI un-quarantines the package."
|
||||
),
|
||||
}, ensure_ascii=False)
|
||||
|
||||
elif provider == "gemini":
|
||||
logger.info("Generating speech with Google Gemini TTS...")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue