feat(web): send Hermes identity headers on Perplexity requests

Add HTTP-Referer, X-Title, User-Agent (HermesAgent/<version>) and
X-Pplx-Integration: hermes-agent to the Perplexity web provider's
/search and /sdk/content/snippets requests. Same static headers Hermes
already sends Kimi and OpenCode; nothing per-user, no extra requests.
This commit is contained in:
Andrew Madson
2026-09-10 20:31:12 +00:00
committed by Teknium
parent 1ab32b212b
commit 1ad89ac018
2 changed files with 26 additions and 0 deletions

View File

@@ -40,12 +40,24 @@ from urllib.parse import urlparse
import httpx
from agent.web_search_provider import WebSearchProvider
from hermes_cli import __version__ as _HERMES_VERSION
logger = logging.getLogger(__name__)
_DEFAULT_BASE_URL = "https://api.perplexity.ai"
_KEY_URL = "https://www.perplexity.ai/account/api"
# Identify Hermes to Perplexity: the same static harness identity Hermes sends Kimi and
# OpenCode, plus Perplexity's integration header. No per-user identifier and no separate
# request; the call already carries the user's own API key.
_HEADERS = {
"HTTP-Referer": "https://hermes-agent.nousresearch.com",
"X-Title": "Hermes Agent",
"User-Agent": f"HermesAgent/{_HERMES_VERSION}",
"X-Pplx-Integration": "hermes-agent",
}
# Search API hard cap for search_type=web.
_MAX_SEARCH_RESULTS = 20
# Snippet budgets (backend limits: max_tokens 1-16384, per page 1-4096).
@@ -80,6 +92,7 @@ def _perplexity_request(endpoint: str, payload: Dict[str, Any]) -> Dict[str, Any
headers={
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
**_HEADERS,
},
)
if response.status_code >= 400:

View File

@@ -16,6 +16,17 @@ def _ok(payload):
return resp
def _assert_hermes_identity_headers(headers):
"""Both Perplexity endpoints carry the Hermes identity headers (same set as Kimi/OpenCode)
plus Perplexity's integration header."""
from hermes_cli import __version__
assert headers["HTTP-Referer"] == "https://hermes-agent.nousresearch.com"
assert headers["X-Title"] == "Hermes Agent"
assert headers["User-Agent"] == f"HermesAgent/{__version__}"
assert headers["X-Pplx-Integration"] == "hermes-agent"
def test_search_dispatch_maps_search_api_shape():
"""web_search on backend=perplexity hits /search with Bearer auth and maps snippet→description."""
import tools.web_tools as wt
@@ -35,6 +46,7 @@ def test_search_dispatch_maps_search_api_shape():
assert post.call_args.args[0] == "https://api.perplexity.ai/search"
assert post.call_args.kwargs["headers"]["Authorization"] == "Bearer pplx-test"
_assert_hermes_identity_headers(post.call_args.kwargs["headers"])
body = post.call_args.kwargs["json"]
assert body["query"] == "bloom filter"
assert 1 <= body["max_results"] <= 20 # dispatcher bucket-rounds the fetch limit
@@ -65,6 +77,7 @@ def test_extract_dispatch_snippets_per_url_and_missing_key():
out = json.loads(asyncio.run(wt.web_extract_tool(urls)))
assert post.call_args.args[0] == "https://api.perplexity.ai/sdk/content/snippets"
_assert_hermes_identity_headers(post.call_args.kwargs["headers"])
body = post.call_args.kwargs["json"]
assert body["urls"] == urls
assert body["query"] == "tokio tutorial smol"