mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-19 15:18:03 +00:00
fix(deepinfra): align refresh and TTS availability
Forward explicit catalog refreshes and make the TTS availability gate follow the configured provider instead of unrelated credentials.
This commit is contained in:
parent
2fc3f9c1ff
commit
10dc1571bc
9 changed files with 122 additions and 54 deletions
|
|
@ -2392,7 +2392,7 @@ def provider_model_ids(provider: Optional[str], *, force_refresh: bool = False)
|
|||
# DeepInfra's generic /models endpoint mixes chat, image, video,
|
||||
# speech, and embedding models. The tagged catalog helper is the only
|
||||
# safe source for the chat picker, including its empty/failure result.
|
||||
return _fetch_deepinfra_models() or []
|
||||
return _fetch_deepinfra_models(force_refresh=force_refresh) or []
|
||||
if normalized == "ollama-cloud":
|
||||
live = fetch_ollama_cloud_models(force_refresh=force_refresh)
|
||||
if live:
|
||||
|
|
|
|||
|
|
@ -1454,7 +1454,9 @@ class TestFetchDeepInfraModels:
|
|||
|
||||
profile = get_provider_profile("deepinfra")
|
||||
assert profile is not None
|
||||
monkeypatch.setattr(models, "_fetch_deepinfra_models", lambda: None)
|
||||
monkeypatch.setattr(
|
||||
models, "_fetch_deepinfra_models", lambda **kwargs: None
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
profile,
|
||||
"fetch_models",
|
||||
|
|
@ -1464,6 +1466,22 @@ class TestFetchDeepInfraModels:
|
|||
|
||||
assert models.provider_model_ids("deepinfra") == []
|
||||
|
||||
def test_force_refresh_reaches_deepinfra_catalog(self, monkeypatch):
|
||||
import hermes_cli.models as models
|
||||
|
||||
seen = []
|
||||
|
||||
def _fetch(*, force_refresh=False, **kwargs):
|
||||
seen.append(force_refresh)
|
||||
return ["vendor/chat"]
|
||||
|
||||
monkeypatch.setattr(models, "_fetch_deepinfra_models", _fetch)
|
||||
|
||||
assert models.provider_model_ids("deepinfra", force_refresh=True) == [
|
||||
"vendor/chat"
|
||||
]
|
||||
assert seen == [True]
|
||||
|
||||
def test_excludes_non_chat_models(self, monkeypatch):
|
||||
monkeypatch.setenv("DEEPINFRA_API_KEY", "test-key")
|
||||
|
||||
|
|
|
|||
|
|
@ -493,6 +493,9 @@ class TestTextToSpeechToolWithCommandProvider:
|
|||
|
||||
class TestCheckTtsRequirements:
|
||||
def test_configured_command_provider_satisfies_requirement(self):
|
||||
cfg = {"providers": {"x": {"type": "command", "command": "echo x"}}}
|
||||
cfg = {
|
||||
"provider": "x",
|
||||
"providers": {"x": {"type": "command", "command": "echo x"}},
|
||||
}
|
||||
with patch("tools.tts_tool._load_tts_config", return_value=cfg):
|
||||
assert check_tts_requirements() is True
|
||||
|
|
|
|||
|
|
@ -57,3 +57,26 @@ def test_delegates_to_openai_handler_with_deepinfra_creds(monkeypatch, tmp_path)
|
|||
|
||||
assert "deepinfra" in captured["base_url"]
|
||||
assert captured["api_key"] == "test-key"
|
||||
|
||||
|
||||
def test_requirements_follow_explicit_deepinfra_provider(monkeypatch):
|
||||
from tools import tts_tool
|
||||
|
||||
monkeypatch.setattr(
|
||||
tts_tool,
|
||||
"_load_tts_config",
|
||||
lambda: {"provider": "deepinfra", "deepinfra": {}},
|
||||
)
|
||||
monkeypatch.setattr(tts_tool, "_import_openai_client", lambda: object)
|
||||
|
||||
assert tts_tool.check_tts_requirements() is True
|
||||
|
||||
|
||||
def test_unselected_cloud_credentials_do_not_expose_edge_tool(monkeypatch):
|
||||
from tools import tts_tool
|
||||
|
||||
monkeypatch.setattr(tts_tool, "_load_tts_config", lambda: {})
|
||||
monkeypatch.setattr(tts_tool, "_import_edge_tts", MagicMock(side_effect=ImportError))
|
||||
monkeypatch.setenv("OPENAI_API_KEY", "unselected-key")
|
||||
|
||||
assert tts_tool.check_tts_requirements() is False
|
||||
|
|
|
|||
|
|
@ -269,6 +269,8 @@ class TestRegressionGuard:
|
|||
with patch(
|
||||
"hermes_cli.config.load_env",
|
||||
return_value={"MINIMAX_API_KEY": "dotenv-secret"},
|
||||
), patch.object(
|
||||
tts_tool, "_load_tts_config", return_value={"provider": "minimax"}
|
||||
), patch.object(tts_tool, "_import_edge_tts", side_effect=ImportError), \
|
||||
patch.object(tts_tool, "_import_elevenlabs", side_effect=ImportError), \
|
||||
patch.object(tts_tool, "_import_openai_client", side_effect=ImportError), \
|
||||
|
|
|
|||
|
|
@ -447,5 +447,8 @@ class TestGeminiInCheckRequirements:
|
|||
raise ImportError("simulated")
|
||||
return real_import(name, *args, **kwargs)
|
||||
|
||||
with patch("builtins.__import__", side_effect=fake_import):
|
||||
with patch(
|
||||
"tools.tts_tool._load_tts_config",
|
||||
return_value={"provider": "gemini"},
|
||||
), patch("builtins.__import__", side_effect=fake_import):
|
||||
assert check_tts_requirements() is True
|
||||
|
|
|
|||
|
|
@ -204,7 +204,10 @@ class TestCheckTtsRequirementsMistral:
|
|||
from tools.tts_tool import check_tts_requirements
|
||||
|
||||
monkeypatch.setenv("MISTRAL_API_KEY", "test-key")
|
||||
with patch("tools.tts_tool._import_edge_tts", side_effect=ImportError), \
|
||||
with patch(
|
||||
"tools.tts_tool._load_tts_config",
|
||||
return_value={"provider": "mistral"},
|
||||
), patch("tools.tts_tool._import_edge_tts", side_effect=ImportError), \
|
||||
patch("tools.tts_tool._import_elevenlabs", side_effect=ImportError), \
|
||||
patch("tools.tts_tool._import_openai_client", side_effect=ImportError), \
|
||||
patch("tools.tts_tool._check_neutts_available", return_value=False):
|
||||
|
|
|
|||
|
|
@ -376,6 +376,7 @@ class TestTextToSpeechToolWithPiper:
|
|||
class TestCheckTtsRequirementsPiper:
|
||||
def test_piper_install_satisfies_requirements(self, monkeypatch):
|
||||
# Drop every other provider so we can isolate the piper signal.
|
||||
monkeypatch.setattr(tts_tool, "_load_tts_config", lambda: {"provider": "piper"})
|
||||
monkeypatch.setattr(tts_tool, "_import_edge_tts", lambda: (_ for _ in ()).throw(ImportError()))
|
||||
monkeypatch.setattr(tts_tool, "_import_elevenlabs", lambda: (_ for _ in ()).throw(ImportError()))
|
||||
monkeypatch.setattr(tts_tool, "_import_openai_client", lambda: (_ for _ in ()).throw(ImportError()))
|
||||
|
|
|
|||
|
|
@ -2595,60 +2595,75 @@ def text_to_speech_tool(
|
|||
# Requirements check
|
||||
# ===========================================================================
|
||||
def check_tts_requirements() -> bool:
|
||||
"""Return whether the explicitly resolved TTS provider can run.
|
||||
|
||||
Availability must mirror :func:`text_to_speech_tool` dispatch. Unrelated
|
||||
cloud credentials do not make the default Edge backend usable, and an
|
||||
explicitly selected backend is checked on its own requirements.
|
||||
"""
|
||||
Check if at least one TTS provider is available.
|
||||
|
||||
Edge TTS needs no API key and is the default, so if the package
|
||||
is installed, TTS is available. A user-declared command provider
|
||||
also satisfies the requirement.
|
||||
|
||||
Returns:
|
||||
bool: True if at least one provider can work.
|
||||
"""
|
||||
# Any configured command provider counts as available.
|
||||
if _has_any_command_tts_provider():
|
||||
tts_config = _load_tts_config()
|
||||
provider = _get_provider(tts_config)
|
||||
command_config = _resolve_command_provider_config(provider, tts_config)
|
||||
if command_config is not None:
|
||||
return True
|
||||
try:
|
||||
_import_edge_tts()
|
||||
return True
|
||||
except ImportError:
|
||||
pass
|
||||
try:
|
||||
_import_elevenlabs()
|
||||
if get_env_value("ELEVENLABS_API_KEY"):
|
||||
return True
|
||||
except ImportError:
|
||||
pass
|
||||
try:
|
||||
_import_openai_client()
|
||||
if _has_openai_audio_backend():
|
||||
return True
|
||||
except ImportError:
|
||||
pass
|
||||
if get_env_value("MINIMAX_API_KEY"):
|
||||
return True
|
||||
try:
|
||||
from tools.xai_http import resolve_xai_http_credentials
|
||||
|
||||
if resolve_xai_http_credentials().get("api_key"):
|
||||
if provider == "edge":
|
||||
try:
|
||||
_import_edge_tts()
|
||||
return True
|
||||
except ImportError:
|
||||
return _check_neutts_available()
|
||||
if provider == "elevenlabs":
|
||||
try:
|
||||
_import_elevenlabs()
|
||||
except ImportError:
|
||||
return False
|
||||
return bool(get_env_value("ELEVENLABS_API_KEY"))
|
||||
if provider == "openai":
|
||||
try:
|
||||
_import_openai_client()
|
||||
except ImportError:
|
||||
return False
|
||||
return _has_openai_audio_backend()
|
||||
if provider == "deepinfra":
|
||||
try:
|
||||
_import_openai_client()
|
||||
except ImportError:
|
||||
return False
|
||||
return bool(get_env_value("DEEPINFRA_API_KEY"))
|
||||
if provider == "minimax":
|
||||
return bool(get_env_value("MINIMAX_API_KEY"))
|
||||
if provider == "xai":
|
||||
try:
|
||||
from tools.xai_http import resolve_xai_http_credentials
|
||||
|
||||
return bool(resolve_xai_http_credentials().get("api_key"))
|
||||
except Exception:
|
||||
return False
|
||||
if provider == "gemini":
|
||||
return bool(get_env_value("GEMINI_API_KEY") or get_env_value("GOOGLE_API_KEY"))
|
||||
if provider == "mistral":
|
||||
try:
|
||||
_import_mistral_client()
|
||||
except ImportError:
|
||||
return False
|
||||
return bool(get_env_value("MISTRAL_API_KEY"))
|
||||
if provider == "neutts":
|
||||
return _check_neutts_available()
|
||||
if provider == "kittentts":
|
||||
return _check_kittentts_available()
|
||||
if provider == "piper":
|
||||
return _check_piper_available()
|
||||
|
||||
try:
|
||||
from agent.tts_registry import get_provider
|
||||
from hermes_cli.plugins import _ensure_plugins_discovered
|
||||
|
||||
_ensure_plugins_discovered()
|
||||
plugin = get_provider(provider)
|
||||
return bool(plugin and plugin.is_available())
|
||||
except Exception:
|
||||
pass
|
||||
if get_env_value("GEMINI_API_KEY") or get_env_value("GOOGLE_API_KEY"):
|
||||
return True
|
||||
try:
|
||||
_import_mistral_client()
|
||||
if get_env_value("MISTRAL_API_KEY"):
|
||||
return True
|
||||
except ImportError:
|
||||
pass
|
||||
if _check_neutts_available():
|
||||
return True
|
||||
if _check_kittentts_available():
|
||||
return True
|
||||
if _check_piper_available():
|
||||
return True
|
||||
return False
|
||||
return False
|
||||
|
||||
|
||||
def _resolve_openai_audio_client_config() -> tuple[str, str, bool]:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue