From 226e8de827a669e8ffa7035b27d70c19e44b1208 Mon Sep 17 00:00:00 2001 From: Vishal Dharmadhikari Date: Fri, 10 Jul 2026 16:05:35 -0700 Subject: [PATCH] fix(gemini): restrict TTS client context to official host --- tests/tools/test_tts_gemini.py | 20 ++++++++++++++++++-- tools/tts_tool.py | 26 +++++++++++++------------- 2 files changed, 31 insertions(+), 15 deletions(-) diff --git a/tests/tools/test_tts_gemini.py b/tests/tools/test_tts_gemini.py index da744693b588..1a8bde7cc8a9 100644 --- a/tests/tools/test_tts_gemini.py +++ b/tests/tools/test_tts_gemini.py @@ -117,6 +117,7 @@ class TestGenerateGeminiTts: def test_x_goog_api_client_header_is_set(self, tmp_path, monkeypatch, mock_gemini_response): """Gemini TTS requests should include Hermes client context.""" + from hermes_cli import __version__ from tools.tts_tool import _generate_gemini_tts monkeypatch.setenv("GEMINI_API_KEY", "test-key") @@ -125,8 +126,7 @@ class TestGenerateGeminiTts: _generate_gemini_tts("Hi", str(tmp_path / "test.wav"), {}) headers = mock_post.call_args[1]["headers"] - assert "X-Goog-Api-Client" in headers - assert headers["X-Goog-Api-Client"].startswith("hermes-agent/") + assert headers["X-Goog-Api-Client"] == f"hermes-agent/{__version__}" def test_default_voice_and_model(self, tmp_path, monkeypatch, mock_gemini_response): from tools.tts_tool import ( @@ -268,6 +268,22 @@ class TestGenerateGeminiTts: _generate_gemini_tts("Hi", str(tmp_path / "test.wav"), {}) assert mock_post.call_args[0][0].startswith("https://custom-gemini.example.com/v1beta/") + assert "X-Goog-Api-Client" not in mock_post.call_args[1]["headers"] + + def test_lookalike_base_url_omits_client_context( + self, tmp_path, monkeypatch, mock_gemini_response + ): + from tools.tts_tool import _generate_gemini_tts + + lookalike = "https://generativelanguage.googleapis.com.evil.example/v1beta" + monkeypatch.setenv("GEMINI_API_KEY", "test-key") + monkeypatch.setenv("GEMINI_BASE_URL", lookalike) + + with patch("requests.post", return_value=mock_gemini_response) as mock_post: + _generate_gemini_tts("Hi", str(tmp_path / "test.wav"), {}) + + assert mock_post.call_args[0][0].startswith(f"{lookalike}/") + assert "X-Goog-Api-Client" not in mock_post.call_args[1]["headers"] def test_persona_prompt_file_appends_labeled_transcript( self, tmp_path, monkeypatch, mock_gemini_response diff --git a/tools/tts_tool.py b/tools/tts_tool.py index 374042192414..545d72bb6907 100644 --- a/tools/tts_tool.py +++ b/tools/tts_tool.py @@ -50,7 +50,7 @@ import threading import uuid from pathlib import Path from typing import Callable, Dict, Any, Optional -from urllib.parse import urljoin +from urllib.parse import urljoin, urlparse from hermes_cli._subprocess_compat import windows_hide_flags from hermes_constants import display_hermes_home @@ -1850,24 +1850,24 @@ def _generate_gemini_tts(text: str, output_path: str, tts_config: Dict[str, Any] }, } - try: - import hermes_cli as _hermes_cli + headers = {"Content-Type": "application/json"} + if urlparse(base_url).hostname == "generativelanguage.googleapis.com": + try: + import hermes_cli as _hermes_cli - _hermes_version = str(_hermes_cli.__version__) - except Exception: - _hermes_version = "0.0.0" + _hermes_version = str(_hermes_cli.__version__) + except Exception: + _hermes_version = "0.0.0" + # Include Hermes client context following Gemini's partner + # integration guidance: + # https://ai.google.dev/gemini-api/docs/partner-integration + headers["X-Goog-Api-Client"] = f"hermes-agent/{_hermes_version}" endpoint = f"{base_url}/models/{model}:generateContent" response = requests.post( endpoint, params={"key": api_key}, - headers={ - "Content-Type": "application/json", - # Include Hermes client context following Gemini's partner - # integration guidance: - # https://ai.google.dev/gemini-api/docs/partner-integration - "X-Goog-Api-Client": f"hermes-agent/{_hermes_version}", - }, + headers=headers, json=payload, timeout=60, )