feat(gemini): improve request context for support and compatibility

Include the Hermes client name and version with Gemini inference, model and tier checks, and TTS requests. Add focused coverage for the request headers and keep the Gemini-specific context scoped to Google Gemini endpoints.
This commit is contained in:
Vishal Dharmadhikari 2026-07-09 21:38:18 -07:00 committed by Teknium
parent c7e09f2571
commit b8eb89f5c9
7 changed files with 126 additions and 3 deletions

View file

@ -32,6 +32,13 @@ from agent.gemini_schema import sanitize_gemini_tool_parameters
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
try:
import hermes_cli as _hermes_cli
_HERMES_VERSION = str(_hermes_cli.__version__)
except Exception:
_HERMES_VERSION = "0.0.0"
DEFAULT_GEMINI_BASE_URL = "https://generativelanguage.googleapis.com/v1beta" DEFAULT_GEMINI_BASE_URL = "https://generativelanguage.googleapis.com/v1beta"
# Published max output-token ceiling shared by every current Gemini text model # Published max output-token ceiling shared by every current Gemini text model
@ -99,7 +106,10 @@ def probe_gemini_tier(
url, url,
params={"key": key}, params={"key": key},
json=payload, json=payload,
headers={"Content-Type": "application/json"}, headers={
"Content-Type": "application/json",
"X-Goog-Api-Client": f"hermes-agent/{_HERMES_VERSION}",
},
) )
except Exception as exc: except Exception as exc:
logger.debug("probe_gemini_tier: network error: %s", exc) logger.debug("probe_gemini_tier: network error: %s", exc)
@ -901,7 +911,11 @@ class GeminiNativeClient:
"Content-Type": "application/json", "Content-Type": "application/json",
"Accept": "application/json", "Accept": "application/json",
"x-goog-api-key": self.api_key, "x-goog-api-key": self.api_key,
"User-Agent": "hermes-agent (gemini-native)", # Include Hermes client context following Gemini's partner
# integration guidance.
# See https://ai.google.dev/gemini-api/docs/partner-integration
"User-Agent": f"hermes-agent/{_HERMES_VERSION} (gemini-native)",
"X-Goog-Api-Client": f"hermes-agent/{_HERMES_VERSION}",
} }
headers.update(self._default_headers) headers.update(self._default_headers)
return headers return headers

View file

@ -3594,6 +3594,8 @@ def probe_api_models(
tried: list[str] = [] tried: list[str] = []
headers: dict[str, str] = {"User-Agent": _HERMES_USER_AGENT} headers: dict[str, str] = {"User-Agent": _HERMES_USER_AGENT}
if urllib.parse.urlparse(normalized).hostname == "generativelanguage.googleapis.com":
headers["X-Goog-Api-Client"] = f"hermes-agent/{_HERMES_VERSION}"
if api_key and api_mode == "anthropic_messages": if api_key and api_mode == "anthropic_messages":
headers["x-api-key"] = api_key headers["x-api-key"] = api_key
headers["anthropic-version"] = "2023-06-01" headers["anthropic-version"] = "2023-06-01"

View file

@ -776,6 +776,7 @@ AUTHOR_MAP = {
"bzarnitz13@gmail.com": "Beandon13", "bzarnitz13@gmail.com": "Beandon13",
"tony@tonysimons.dev": "asimons81", "tony@tonysimons.dev": "asimons81",
"jetha@google.com": "jethac", "jetha@google.com": "jethac",
"vishal.dharm@gmail.com": "vishal-dharm",
"jani@0xhoneyjar.xyz": "deep-name", "jani@0xhoneyjar.xyz": "deep-name",
# LINE messaging plugin (synthesis PR) # LINE messaging plugin (synthesis PR)
"32443648+leepoweii@users.noreply.github.com": "leepoweii", "32443648+leepoweii@users.noreply.github.com": "leepoweii",

View file

@ -461,3 +461,53 @@ def test_explicit_max_tokens_is_respected():
req = build_gemini_request(messages=[{"role": "user", "content": "hi"}], max_tokens=4096) req = build_gemini_request(messages=[{"role": "user", "content": "hi"}], max_tokens=4096)
assert req["generationConfig"]["maxOutputTokens"] == 4096 assert req["generationConfig"]["maxOutputTokens"] == 4096
# ---------------------------------------------------------------------------
# X-Goog-Api-Client header tests
# ---------------------------------------------------------------------------
def test_x_goog_api_client_header_is_set():
"""The X-Goog-Api-Client header should be set on inference requests."""
from agent.gemini_native_adapter import GeminiNativeClient
client = GeminiNativeClient(api_key="fake-key", model="gemini-2.0-flash")
headers = client._headers()
assert "X-Goog-Api-Client" in headers, "X-Goog-Api-Client header missing"
assert "hermes-agent/" in headers["X-Goog-Api-Client"], (
"hermes-agent not found in X-Goog-Api-Client header"
)
def test_x_goog_api_client_header_format():
"""Header value should be 'hermes-agent/<version>' matching the package version."""
from agent.gemini_native_adapter import GeminiNativeClient, _HERMES_VERSION
client = GeminiNativeClient(api_key="fake-key", model="gemini-2.0-flash")
headers = client._headers()
expected = f"hermes-agent/{_HERMES_VERSION}"
assert headers["X-Goog-Api-Client"] == expected
def test_user_agent_contains_version():
"""User-Agent should include the hermes-agent version."""
from agent.gemini_native_adapter import GeminiNativeClient, _HERMES_VERSION
client = GeminiNativeClient(api_key="fake-key", model="gemini-2.0-flash")
headers = client._headers()
assert f"hermes-agent/{_HERMES_VERSION}" in headers["User-Agent"]
def test_hermes_version_is_valid():
"""_HERMES_VERSION should be a non-empty string."""
from agent.gemini_native_adapter import _HERMES_VERSION
assert isinstance(_HERMES_VERSION, str)
assert len(_HERMES_VERSION) > 0
assert _HERMES_VERSION != "0.0.0", (
"Version should resolve from hermes_cli.__version__, not the fallback"
)

View file

@ -942,3 +942,33 @@ class TestProbeApiModelsUserAgent:
assert ua and ua.startswith("hermes-cli/") assert ua and ua.startswith("hermes-cli/")
# No Authorization was set, but UA must still be present. # No Authorization was set, but UA must still be present.
assert req.get_header("Authorization") is None assert req.get_header("Authorization") is None
def test_probe_sends_client_context_to_gemini(self):
from unittest.mock import patch
from hermes_cli.models import _HERMES_VERSION
body = b'{"data":[]}'
with patch(
"hermes_cli.models.urllib.request.urlopen",
return_value=self._make_mock_response(body),
) as mock_urlopen:
probe_api_models(
"gemini-key",
"https://generativelanguage.googleapis.com/v1beta/openai",
)
req = mock_urlopen.call_args[0][0]
assert req.get_header("X-goog-api-client") == f"hermes-agent/{_HERMES_VERSION}"
def test_probe_omits_gemini_client_context_for_other_providers(self):
from unittest.mock import patch
body = b'{"data":[]}'
with patch(
"hermes_cli.models.urllib.request.urlopen",
return_value=self._make_mock_response(body),
) as mock_urlopen:
probe_api_models("provider-key", "https://api.example.com/v1")
req = mock_urlopen.call_args[0][0]
assert req.get_header("X-goog-api-client") is None

View file

@ -115,6 +115,19 @@ class TestGenerateGeminiTts:
# Audio payload should match the PCM we put in # Audio payload should match the PCM we put in
assert data[44:] == fake_pcm_bytes assert data[44:] == fake_pcm_bytes
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 tools.tts_tool import _generate_gemini_tts
monkeypatch.setenv("GEMINI_API_KEY", "test-key")
with patch("requests.post", return_value=mock_gemini_response) as mock_post:
_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/")
def test_default_voice_and_model(self, tmp_path, monkeypatch, mock_gemini_response): def test_default_voice_and_model(self, tmp_path, monkeypatch, mock_gemini_response):
from tools.tts_tool import ( from tools.tts_tool import (
DEFAULT_GEMINI_TTS_MODEL, DEFAULT_GEMINI_TTS_MODEL,

View file

@ -1850,11 +1850,24 @@ def _generate_gemini_tts(text: str, output_path: str, tts_config: Dict[str, Any]
}, },
} }
try:
import hermes_cli as _hermes_cli
_hermes_version = str(_hermes_cli.__version__)
except Exception:
_hermes_version = "0.0.0"
endpoint = f"{base_url}/models/{model}:generateContent" endpoint = f"{base_url}/models/{model}:generateContent"
response = requests.post( response = requests.post(
endpoint, endpoint,
params={"key": api_key}, params={"key": api_key},
headers={"Content-Type": "application/json"}, 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}",
},
json=payload, json=payload,
timeout=60, timeout=60,
) )