fix(tts): resolve API keys from ~/.hermes/.env via get_env_value (#17140)

TTS provider tools (elevenlabs, xai, minimax, mistral, gemini) called
os.getenv("X_API_KEY") directly, which bypassed Hermes's dotenv bridge in
hermes_cli.config. Users who keep their TTS keys only in ~/.hermes/.env saw
"X_API_KEY not set" errors even though the rest of the stack
(agent/credential_pool, hermes_cli/auth) already resolves keys through
get_env_value() — same class of bug as #15914 fixed for those modules.

Switch every TTS env-var lookup (API keys, base URLs, and
check_tts_requirements gates) to get_env_value, which checks os.environ
first and then ~/.hermes/.env. Behaviour for users with keys exported in
the shell is unchanged; users with dotenv-only keys now succeed. The two
diagnostics prints in __main__ are migrated for consistency.

Regression test (tests/tools/test_tts_dotenv_fallback.py):
  - per-provider: each backend reads the dotenv key when only
    ~/.hermes/.env carries it (5 providers).
  - end-to-end: with hermes_cli.config.load_env returning the key and
    os.environ empty, _generate_minimax_tts and check_tts_requirements
    both succeed; reverting tools/tts_tool.py back to os.getenv makes all
    7 tests fail with "MINIMAX_API_KEY not set" / similar.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
briandevans
2026-04-28 15:16:05 -07:00
committed by Teknium
parent ff687c019e
commit 40d25e125b
2 changed files with 246 additions and 15 deletions

View File

@@ -44,6 +44,7 @@ from urllib.parse import urljoin
from hermes_constants import display_hermes_home
logger = logging.getLogger(__name__)
from hermes_cli.config import get_env_value
from tools.managed_tool_gateway import resolve_managed_tool_gateway
from tools.tool_backend_helpers import managed_nous_tools_enabled, prefers_gateway, resolve_openai_audio_api_key
from tools.xai_http import hermes_xai_user_agent
@@ -312,7 +313,7 @@ def _generate_elevenlabs(text: str, output_path: str, tts_config: Dict[str, Any]
Returns:
Path to the saved audio file.
"""
api_key = os.getenv("ELEVENLABS_API_KEY", "")
api_key = (get_env_value("ELEVENLABS_API_KEY") or "")
if not api_key:
raise ValueError("ELEVENLABS_API_KEY not set. Get one at https://elevenlabs.io/")
@@ -406,7 +407,7 @@ def _generate_xai_tts(text: str, output_path: str, tts_config: Dict[str, Any]) -
"""
import requests
api_key = os.getenv("XAI_API_KEY", "").strip()
api_key = (get_env_value("XAI_API_KEY") or "").strip()
if not api_key:
raise ValueError("XAI_API_KEY not set. Get one at https://console.x.ai/")
@@ -417,7 +418,7 @@ def _generate_xai_tts(text: str, output_path: str, tts_config: Dict[str, Any]) -
bit_rate = int(xai_config.get("bit_rate", DEFAULT_XAI_BIT_RATE))
base_url = str(
xai_config.get("base_url")
or os.getenv("XAI_BASE_URL")
or get_env_value("XAI_BASE_URL")
or DEFAULT_XAI_BASE_URL
).strip().rstrip("/")
@@ -479,7 +480,7 @@ def _generate_minimax_tts(text: str, output_path: str, tts_config: Dict[str, Any
"""
import requests
api_key = os.getenv("MINIMAX_API_KEY", "")
api_key = (get_env_value("MINIMAX_API_KEY") or "")
if not api_key:
raise ValueError("MINIMAX_API_KEY not set. Get one at https://platform.minimax.io/")
@@ -556,7 +557,7 @@ def _generate_mistral_tts(text: str, output_path: str, tts_config: Dict[str, Any
and writes the raw bytes to *output_path*.
Supports native Opus output for Telegram voice bubbles.
"""
api_key = os.getenv("MISTRAL_API_KEY", "")
api_key = (get_env_value("MISTRAL_API_KEY") or "")
if not api_key:
raise ValueError("MISTRAL_API_KEY not set. Get one at https://console.mistral.ai/")
@@ -651,7 +652,7 @@ def _generate_gemini_tts(text: str, output_path: str, tts_config: Dict[str, Any]
"""
import requests
api_key = (os.getenv("GEMINI_API_KEY") or os.getenv("GOOGLE_API_KEY") or "").strip()
api_key = (get_env_value("GEMINI_API_KEY") or get_env_value("GOOGLE_API_KEY") or "").strip()
if not api_key:
raise ValueError(
"GEMINI_API_KEY not set. Get one at https://aistudio.google.com/app/apikey"
@@ -662,7 +663,7 @@ def _generate_gemini_tts(text: str, output_path: str, tts_config: Dict[str, Any]
voice = str(gemini_config.get("voice", DEFAULT_GEMINI_TTS_VOICE)).strip() or DEFAULT_GEMINI_TTS_VOICE
base_url = str(
gemini_config.get("base_url")
or os.getenv("GEMINI_BASE_URL")
or get_env_value("GEMINI_BASE_URL")
or DEFAULT_GEMINI_TTS_BASE_URL
).strip().rstrip("/")
@@ -1148,7 +1149,7 @@ def check_tts_requirements() -> bool:
pass
try:
_import_elevenlabs()
if os.getenv("ELEVENLABS_API_KEY"):
if get_env_value("ELEVENLABS_API_KEY"):
return True
except ImportError:
pass
@@ -1158,15 +1159,15 @@ def check_tts_requirements() -> bool:
return True
except ImportError:
pass
if os.getenv("MINIMAX_API_KEY"):
if get_env_value("MINIMAX_API_KEY"):
return True
if os.getenv("XAI_API_KEY"):
if get_env_value("XAI_API_KEY"):
return True
if os.getenv("GEMINI_API_KEY") or os.getenv("GOOGLE_API_KEY"):
if get_env_value("GEMINI_API_KEY") or get_env_value("GOOGLE_API_KEY"):
return True
try:
_import_mistral_client()
if os.getenv("MISTRAL_API_KEY"):
if get_env_value("MISTRAL_API_KEY"):
return True
except ImportError:
pass
@@ -1278,7 +1279,7 @@ def stream_tts_to_speaker(
{**tts_config, "elevenlabs": {**el_config, "model_id": model_id}},
)
api_key = os.getenv("ELEVENLABS_API_KEY", "")
api_key = (get_env_value("ELEVENLABS_API_KEY") or "")
if not api_key:
logger.warning("ELEVENLABS_API_KEY not set; streaming TTS audio disabled")
else:
@@ -1464,13 +1465,13 @@ if __name__ == "__main__":
print("\nProvider availability:")
print(f" Edge TTS: {'installed' if _check(_import_edge_tts, 'edge') else 'not installed (pip install edge-tts)'}")
print(f" ElevenLabs: {'installed' if _check(_import_elevenlabs, 'el') else 'not installed (pip install elevenlabs)'}")
print(f" API Key: {'set' if os.getenv('ELEVENLABS_API_KEY') else 'not set'}")
print(f" API Key: {'set' if get_env_value('ELEVENLABS_API_KEY') else 'not set'}")
print(f" OpenAI: {'installed' if _check(_import_openai_client, 'oai') else 'not installed'}")
print(
" API Key: "
f"{'set' if resolve_openai_audio_api_key() else 'not set (VOICE_TOOLS_OPENAI_KEY or OPENAI_API_KEY)'}"
)
print(f" MiniMax: {'API key set' if os.getenv('MINIMAX_API_KEY') else 'not set (MINIMAX_API_KEY)'}")
print(f" MiniMax: {'API key set' if get_env_value('MINIMAX_API_KEY') else 'not set (MINIMAX_API_KEY)'}")
print(f" ffmpeg: {'✅ found' if _has_ffmpeg() else '❌ not found (needed for Telegram Opus)'}")
print(f"\n Output dir: {DEFAULT_OUTPUT_DIR}")