mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-20 15:33:54 +00:00
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:
parent
c7e09f2571
commit
b8eb89f5c9
7 changed files with 126 additions and 3 deletions
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue