mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-15 04:12:25 +00:00
refactor(gmi): move User-Agent to profile.default_headers
The previous revision of this PR added six GMI-specific branches
(`elif base_url_host_matches(..., 'api.gmi-serving.com')`) across
run_agent.py and agent/auxiliary_client.py, plus a _HERMES_UA_HEADERS
constant in auxiliary_client.py.
ProviderProfile already has a `default_headers: dict[str, str]` field
commented as 'Client-level quirks (set once at client construction)'.
Other plugins (ai-gateway, kimi-coding) already use it. Two of the four
auxiliary_client sites we previously patched already had a generic
`else: profile.default_headers` fallback that picked it up (so did
both run_agent sites).
This revision:
* Sets `default_headers={'User-Agent': 'HermesAgent/<ver>'}` on the
GMI profile in plugins/model-providers/gmi/__init__.py.
* Reverts all six GMI-specific branches in run_agent.py and
auxiliary_client.py.
* Adds the generic profile-fallback `else` block to the two
auxiliary_client sites (`_to_async_client`, `resolve_provider_client`)
that didn't have it yet. This benefits every provider whose profile
declares default_headers, not just GMI — e.g. Vercel AI Gateway's
HTTP-Referer/X-Title now flow through the async client path too.
* Replaces the GMI-specific URL-branch tests with a profile-level
assertion and keeps the run_agent integration test (with
`provider='gmi'` so the fallback picks up the profile).
Net diff vs main: +82/-0 across 5 files, touching only the GMI plugin,
two generic fallback blocks in auxiliary_client.py, AUTHOR_MAP, and
tests. No core files change.
Based on #20907 by @isaachuangGMICLOUD.
This commit is contained in:
parent
5d1bdf11b6
commit
81928f03ab
4 changed files with 82 additions and 0 deletions
|
|
@ -2141,6 +2141,20 @@ def _to_async_client(sync_client, model: str, is_vision: bool = False):
|
||||||
)
|
)
|
||||||
elif base_url_host_matches(sync_base_url, "api.kimi.com"):
|
elif base_url_host_matches(sync_base_url, "api.kimi.com"):
|
||||||
async_kwargs["default_headers"] = {"User-Agent": "claude-code/0.1.0"}
|
async_kwargs["default_headers"] = {"User-Agent": "claude-code/0.1.0"}
|
||||||
|
else:
|
||||||
|
# Fall back to profile.default_headers for providers that declare
|
||||||
|
# client-level headers on their ProviderProfile (e.g. attribution
|
||||||
|
# User-Agent strings). Provider is inferred from the hostname.
|
||||||
|
try:
|
||||||
|
from agent.model_metadata import _infer_provider_from_url
|
||||||
|
from providers import get_provider_profile as _gpf_async
|
||||||
|
_inferred = _infer_provider_from_url(sync_base_url)
|
||||||
|
if _inferred:
|
||||||
|
_ph_async = _gpf_async(_inferred)
|
||||||
|
if _ph_async and _ph_async.default_headers:
|
||||||
|
async_kwargs["default_headers"] = dict(_ph_async.default_headers)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
return AsyncOpenAI(**async_kwargs), model
|
return AsyncOpenAI(**async_kwargs), model
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -2368,6 +2382,16 @@ def resolve_provider_client(
|
||||||
extra["default_headers"] = copilot_request_headers(
|
extra["default_headers"] = copilot_request_headers(
|
||||||
is_agent_turn=True, is_vision=is_vision
|
is_agent_turn=True, is_vision=is_vision
|
||||||
)
|
)
|
||||||
|
else:
|
||||||
|
# Fall back to profile.default_headers for providers that
|
||||||
|
# declare client-level attribution headers on their profile.
|
||||||
|
try:
|
||||||
|
from providers import get_provider_profile as _gpf_custom
|
||||||
|
_ph_custom = _gpf_custom(provider)
|
||||||
|
if _ph_custom and _ph_custom.default_headers:
|
||||||
|
extra["default_headers"] = dict(_ph_custom.default_headers)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
client = OpenAI(api_key=custom_key, base_url=_clean_base, **extra)
|
client = OpenAI(api_key=custom_key, base_url=_clean_base, **extra)
|
||||||
client = _wrap_if_needed(client, final_model, custom_base, custom_key)
|
client = _wrap_if_needed(client, final_model, custom_base, custom_key)
|
||||||
return (_to_async_client(client, final_model, is_vision=is_vision) if async_mode
|
return (_to_async_client(client, final_model, is_vision=is_vision) if async_mode
|
||||||
|
|
@ -2556,6 +2580,18 @@ def resolve_provider_client(
|
||||||
headers.update(copilot_request_headers(
|
headers.update(copilot_request_headers(
|
||||||
is_agent_turn=True, is_vision=is_vision
|
is_agent_turn=True, is_vision=is_vision
|
||||||
))
|
))
|
||||||
|
else:
|
||||||
|
# Fall back to profile.default_headers for providers that declare
|
||||||
|
# client-level attribution headers on their profile (e.g. GMI
|
||||||
|
# User-Agent for traffic identification, Vercel AI Gateway
|
||||||
|
# Referer/Title for analytics).
|
||||||
|
try:
|
||||||
|
from providers import get_provider_profile as _gpf_main
|
||||||
|
_ph_main = _gpf_main(provider)
|
||||||
|
if _ph_main and _ph_main.default_headers:
|
||||||
|
headers.update(_ph_main.default_headers)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
client = OpenAI(api_key=api_key, base_url=base_url,
|
client = OpenAI(api_key=api_key, base_url=base_url,
|
||||||
**({"default_headers": headers} if headers else {}))
|
**({"default_headers": headers} if headers else {}))
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
"""GMI Cloud provider profile."""
|
"""GMI Cloud provider profile."""
|
||||||
|
|
||||||
|
from hermes_cli import __version__ as _HERMES_VERSION
|
||||||
from providers import register_provider
|
from providers import register_provider
|
||||||
from providers.base import ProviderProfile
|
from providers.base import ProviderProfile
|
||||||
|
|
||||||
|
|
@ -12,6 +13,10 @@ gmi = ProviderProfile(
|
||||||
env_vars=("GMI_API_KEY", "GMI_BASE_URL"),
|
env_vars=("GMI_API_KEY", "GMI_BASE_URL"),
|
||||||
base_url="https://api.gmi-serving.com/v1",
|
base_url="https://api.gmi-serving.com/v1",
|
||||||
auth_type="api_key",
|
auth_type="api_key",
|
||||||
|
# Attribution so GMI can identify traffic from Hermes Agent.
|
||||||
|
# The generic profile.default_headers fallback in run_agent.py and
|
||||||
|
# agent/auxiliary_client.py picks this up at client construction time.
|
||||||
|
default_headers={"User-Agent": f"HermesAgent/{_HERMES_VERSION}"},
|
||||||
default_aux_model="google/gemini-3.1-flash-lite-preview",
|
default_aux_model="google/gemini-3.1-flash-lite-preview",
|
||||||
fallback_models=(
|
fallback_models=(
|
||||||
"zai-org/GLM-5.1-FP8",
|
"zai-org/GLM-5.1-FP8",
|
||||||
|
|
|
||||||
|
|
@ -284,6 +284,22 @@ class TestGmiAuxiliary:
|
||||||
assert model == "google/gemini-3.1-flash-lite-preview"
|
assert model == "google/gemini-3.1-flash-lite-preview"
|
||||||
assert mock_openai.call_args.kwargs["api_key"] == "gmi-test-key"
|
assert mock_openai.call_args.kwargs["api_key"] == "gmi-test-key"
|
||||||
assert mock_openai.call_args.kwargs["base_url"] == "https://api.gmi-serving.com/v1"
|
assert mock_openai.call_args.kwargs["base_url"] == "https://api.gmi-serving.com/v1"
|
||||||
|
# GMI profile declares default_headers with a HermesAgent User-Agent
|
||||||
|
# for traffic attribution. The generic profile-fallback branch in
|
||||||
|
# resolve_provider_client should carry it through to the OpenAI client.
|
||||||
|
headers = mock_openai.call_args.kwargs.get("default_headers", {})
|
||||||
|
assert headers.get("User-Agent", "").startswith("HermesAgent/")
|
||||||
|
|
||||||
|
def test_gmi_profile_declares_hermes_user_agent(self):
|
||||||
|
"""The GMI plugin sets a HermesAgent/<ver> User-Agent on its profile."""
|
||||||
|
from providers import get_provider_profile
|
||||||
|
|
||||||
|
profile = get_provider_profile("gmi")
|
||||||
|
assert profile is not None
|
||||||
|
ua = profile.default_headers.get("User-Agent", "")
|
||||||
|
assert ua.startswith("HermesAgent/"), (
|
||||||
|
f"expected GMI profile User-Agent to start with 'HermesAgent/', got {ua!r}"
|
||||||
|
)
|
||||||
|
|
||||||
def test_resolve_provider_client_accepts_gmi_alias(self, monkeypatch):
|
def test_resolve_provider_client_accepts_gmi_alias(self, monkeypatch):
|
||||||
monkeypatch.setenv("GMI_API_KEY", "gmi-test-key")
|
monkeypatch.setenv("GMI_API_KEY", "gmi-test-key")
|
||||||
|
|
|
||||||
|
|
@ -65,6 +65,31 @@ def test_routermint_base_url_applies_user_agent_header(mock_openai):
|
||||||
assert headers["User-Agent"].startswith("HermesAgent/")
|
assert headers["User-Agent"].startswith("HermesAgent/")
|
||||||
|
|
||||||
|
|
||||||
|
@patch("run_agent.OpenAI")
|
||||||
|
def test_gmi_base_url_picks_up_profile_user_agent(mock_openai):
|
||||||
|
"""GMI declares User-Agent on its ProviderProfile.default_headers.
|
||||||
|
|
||||||
|
The ``_apply_client_headers_for_base_url`` else-branch looks up the
|
||||||
|
provider profile and applies its default_headers, so no GMI-specific
|
||||||
|
branch is needed in run_agent.
|
||||||
|
"""
|
||||||
|
mock_openai.return_value = MagicMock()
|
||||||
|
agent = AIAgent(
|
||||||
|
api_key="test-key",
|
||||||
|
base_url="https://api.gmi-serving.com/v1",
|
||||||
|
model="test/model",
|
||||||
|
provider="gmi",
|
||||||
|
quiet_mode=True,
|
||||||
|
skip_context_files=True,
|
||||||
|
skip_memory=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
agent._apply_client_headers_for_base_url("https://api.gmi-serving.com/v1")
|
||||||
|
|
||||||
|
headers = agent._client_kwargs["default_headers"]
|
||||||
|
assert headers["User-Agent"].startswith("HermesAgent/")
|
||||||
|
|
||||||
|
|
||||||
@patch("run_agent.OpenAI")
|
@patch("run_agent.OpenAI")
|
||||||
def test_unknown_base_url_clears_default_headers(mock_openai):
|
def test_unknown_base_url_clears_default_headers(mock_openai):
|
||||||
mock_openai.return_value = MagicMock()
|
mock_openai.return_value = MagicMock()
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue