diff --git a/plugins/web/perplexity/provider.py b/plugins/web/perplexity/provider.py index 187addb47f..c29699d2b5 100644 --- a/plugins/web/perplexity/provider.py +++ b/plugins/web/perplexity/provider.py @@ -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: diff --git a/tests/tools/test_web_tools_perplexity.py b/tests/tools/test_web_tools_perplexity.py index 529e3aa7a6..40bbdeb0fb 100644 --- a/tests/tools/test_web_tools_perplexity.py +++ b/tests/tools/test_web_tools_perplexity.py @@ -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"