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:
kshitijk4poor 2026-07-14 02:04:06 +05:30 committed by kshitij
parent 2fc3f9c1ff
commit 10dc1571bc
9 changed files with 122 additions and 54 deletions

View file

@ -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:

View file

@ -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")

View file

@ -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

View file

@ -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

View file

@ -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), \

View file

@ -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

View file

@ -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):

View file

@ -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()))

View file

@ -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]: