Skip to content

feat: add Perplexity as default web search backend + LLM provider plugin#23981

Open
jliounis wants to merge 4 commits into
NousResearch:mainfrom
jliounis:feat/perplexity-default-search
Open

feat: add Perplexity as default web search backend + LLM provider plugin#23981
jliounis wants to merge 4 commits into
NousResearch:mainfrom
jliounis:feat/perplexity-default-search

Conversation

@jliounis

@jliounis jliounis commented May 11, 2026

Copy link
Copy Markdown

Summary

Adds Perplexity as the default web search backend in Hermes Agent, and registers a Perplexity LLM provider plugin so users can route chat completion through the Perplexity Agent API (OpenAI-compatible).

Two integration paths, both opt-in via a single API key:

Surface What it does Source of truth
Web search backend tools/web_providers/perplexity.py calls POST https://api.perplexity.ai/search and returns normalized {title, url, snippet} results. Wired into tools/web_tools.py as the new default in _get_backend(). Search API docs
LLM provider plugin plugins/model-providers/perplexity/ registers a ProviderProfile with base_url=https://api.perplexity.ai (OpenAI-compatible) so any Hermes flow that takes a provider can route through Perplexity. Agent API docs

Why default to Perplexity for web search?

  • Drop-in normalized results — same {title, url, snippet} shape
  • Single key, single endpointPERPLEXITY_API_KEY (or legacy PPLX_API_KEY) and one POST /search call.
  • Backward compatible — if web.backend is set in ~/.hermes/config.yaml, that wins. If any of the existing keys (FIRECRAWL_*, PARALLEL_*, TAVILY_*, EXA_*, etc.) is set without web.backend, those still resolve to their existing backends. The only behavior change for an existing user is "no key configured" → now defaults to Perplexity instead of Firecrawl (which would also have failed at client init).

Changes

Web search backend

  • New tools/web_providers/perplexity.pyPerplexitySearchProvider exposing .search(query, limit), plus perplexity_search() / _perplexity_search_request() / _normalize_perplexity_results() / _get_perplexity_api_key(). Bearer auth, X-Pplx-Integration: hermes-agent/1.0 attribution header, limit clamped to [1, 20].
  • Modified tools/web_tools.py:
    • _get_backend() validates "perplexity" in the configured set, places perplexity first in the auto-detect candidate list, and returns "perplexity" as the shipped default when no keys are present.
    • _is_backend_available() resolves "perplexity" via PERPLEXITY_API_KEY or PPLX_API_KEY.
    • _web_requires_env() advertises both env vars.
    • web_search_tool dispatches to the perplexity branch first.
    • check_web_api_key() and the __main__ debug block both learn the new backend.
  • Modified hermes_cli/tools_config.py — Perplexity is now the first option under web providers in the hermes tools wizard with the ★ recommended badge. Firecrawl is demoted to paid.

LLM provider plugin

  • New plugins/model-providers/perplexity/__init__.py and plugin.yamlProviderProfile(name="perplexity", aliases=("pplx",), env_vars=("PERPLEXITY_API_KEY","PPLX_API_KEY"), base_url="https://api.perplexity.ai", fallback_models=("openai/gpt-5.4", "anthropic/claude-sonnet-4-6", "google/gemini-3-1-pro")). All fallback models are reachable through the Perplexity Agent API.

Docs

  • website/docs/user-guide/features/web-search.md — Backends table gains a Perplexity (default) row, full section, updated config snippet, and refreshed auto-detect priority table.
  • website/docs/integrations/providers.md — Inference Providers table gains a Perplexity row (Agent API, OpenAI-compatible).
  • website/docs/integrations/index.md — default backend updated to Perplexity.
  • website/docs/reference/environment-variables.mdPERPLEXITY_API_KEY and PPLX_API_KEY rows added under Tool APIs.
  • website/docs/user-guide/features/mcp.md — new ### Perplexity MCP server for live web research example using server-perplexity-ask.

Tests

  • New tests/tools/test_web_tools_perplexity.py — 21 tests across 5 classes covering API-key resolution (PERPLEXITY_API_KEY wins over PPLX_API_KEY, whitespace stripping, raises when neither is set), request shape (Bearer auth + X-Pplx-Integration header + payload), result normalization (snippet/content fallback, missing fields), limit clamping (0 → 1, 100 → 20), backend selection (configured / auto-detected / PPLX_API_KEY alias / no-keys default), _is_backend_available(), and end-to-end web_search_tool dispatch through the perplexity branch.
  • Modified tests/tools/test_web_tools_config.py_ENV_KEYS lists include PERPLEXITY_API_KEY / PPLX_API_KEY (so the env-clearing setup works as intended), test_fallback_no_keys_defaults_to_firecrawl becomes test_fallback_no_keys_defaults_to_perplexity, plus two new tests (test_fallback_perplexity_only_key, test_fallback_perplexity_takes_priority_over_firecrawl).
  • Modified tests/providers/test_plugin_discovery.py — bump expected profile count (33 → 34) and spot-check "perplexity".

Test plan

uv run --extra dev pytest \
  tests/tools/test_web_tools_perplexity.py \
  tests/tools/test_web_tools_config.py \
  tests/tools/test_web_tools_tavily.py \
  tests/tools/test_web_providers.py \
  tests/tools/test_web_providers_brave_free.py \
  tests/tools/test_web_providers_ddgs.py \
  tests/tools/test_web_providers_searxng.py \
  tests/tools/test_tool_backend_helpers.py \
  tests/providers/test_plugin_discovery.py -q

215 passed in 6.32s locally.

Manual verification

export PERPLEXITY_API_KEY=pplx-...
python -c "from tools.web_tools import _get_backend, _is_backend_available; \
print(_get_backend(), _is_backend_available('perplexity'))"
# perplexity True

python -c "import asyncio, json; from tools.web_tools import web_search_tool; \
print(json.dumps(asyncio.run(web_search_tool('what happened in AI this week', limit=3)), indent=2)[:600])"

Notes / non-goals

  • No Sonar models referenced. Per Perplexity's current direction, the Perplexity provider profile lists only Agent API–routed models. This PR doesn't touch any other backend's logic.
  • Backward compatible. Existing users with FIRECRAWL_API_KEY / PARALLEL_API_KEY / TAVILY_API_KEY / EXA_API_KEY set will continue to resolve to their existing backend with no config change.
  • Attribution header. Outbound POST /search requests carry X-Pplx-Integration: hermes-agent/1.0 so Perplexity can attribute traffic for monitoring & rate-limit hygiene.

Happy to iterate on any of this — naming, default-backend choice, fallback model list, doc placement, etc.

@alt-glitch alt-glitch added type/feature New feature or request comp/tools Tool registry, model_tools, toolsets comp/plugins Plugin system and bundled plugins tool/web Web search and extraction P3 Low — cosmetic, nice to have labels May 11, 2026
Adds a Perplexity Search API backend (https://api.perplexity.ai/search)
as the new default web search provider, and registers a Perplexity LLM
provider plugin for chat completion via the Perplexity Agent API
(OpenAI-compatible at https://api.perplexity.ai).

Web search backend
- New tools/web_providers/perplexity.py with PerplexitySearchProvider
  exposing .search(query, limit) like the other backends. Posts to
  /search with Bearer auth and an X-Pplx-Integration header. Clamps
  limit to 1..20 and normalizes results to {title, url, snippet}.
- tools/web_tools.py: _get_backend() now checks perplexity first in the
  auto-detect fallback and returns 'perplexity' as the no-keys default.
  _is_backend_available, _web_requires_env, web_search_tool dispatch,
  check_web_api_key and the __main__ debug block all learn about the
  new backend. Reads PERPLEXITY_API_KEY (or legacy PPLX_API_KEY).
- hermes_cli/tools_config.py: Perplexity becomes the first/recommended
  web provider in the 'hermes tools' wizard. Firecrawl is demoted from
  recommended to 'paid'.

LLM provider plugin
- plugins/model-providers/perplexity/{__init__.py, plugin.yaml}:
  ProviderProfile with name='perplexity', aliases=('pplx',),
  env_vars=('PERPLEXITY_API_KEY','PPLX_API_KEY'),
  base_url='https://api.perplexity.ai', and fallback models routed
  through the Agent API (gpt-5.4, claude-sonnet-4-6, gemini-3-1-pro).
- tests/providers/test_plugin_discovery.py: bump expected profile count
  and spot-check 'perplexity'.

Docs
- web-search.md: Perplexity row + section, default switched, auto-detect
  priority table updated.
- providers.md / integrations/index.md / environment-variables.md:
  Perplexity added everywhere relevant.
- mcp.md: example for the Perplexity MCP server (server-perplexity-ask).

Tests
- tests/tools/test_web_tools_perplexity.py: 21 tests across 5 classes
  covering API-key resolution, request shape (Bearer + integration
  header), result normalization, limit clamping, backend selection
  (default/configured/auto-detected/PPLX alias), is_backend_available,
  and web_search_tool dispatch through the perplexity branch.
- tests/tools/test_web_tools_config.py: env-clearing lists include
  PERPLEXITY_API_KEY and PPLX_API_KEY; replaced
  test_fallback_no_keys_defaults_to_firecrawl with the new perplexity
  default plus perplexity-only and priority tests.

Signed-off-by: jliounis <james.liounis@perplexity.ai>
@jliounis jliounis force-pushed the feat/perplexity-default-search branch from 13d9f59 to 2b3d38e Compare May 17, 2026 10:39
@jliounis

jliounis commented May 17, 2026

Copy link
Copy Markdown
Author

Rebased onto main; all 223 web-related tests are green.

rbuchmayer-pplx added a commit to rbuchmayer-pplx/hermes-agent that referenced this pull request May 19, 2026
James's PR NousResearch#23981 registers Perplexity as an LLM provider with
fallback_models in provider/model format (`openai/gpt-5.4`,
`anthropic/claude-sonnet-4-6`, `google/gemini-3-1-pro`) — but the profile
inherits `api_mode="chat_completions"` (the dataclass default in
providers/base.py:44) and sets `base_url="https://api.perplexity.ai"`
(no `/v1`). With those values the OpenAI SDK hits `POST /chat/completions`,
which is Sonar-only and rejects provider/model strings with HTTP 400:

  $ curl -X POST https://api.perplexity.ai/chat/completions \
      -d '{"model":"openai/gpt-5.4","messages":[...]}'
  {"error":{"message":"preset \"openai/gpt-5.4\" not found","code":400}}

The multi-provider routing James's PR description references actually
lives at `POST /v1/responses` — Perplexity's OpenAI-Responses-compatible
alias to the Agent API (same backend as `/v1/agent`). Verified live: all
of `openai/gpt-5.4`, `anthropic/claude-sonnet-4-6`, and `perplexity/sonar`
return HTTP 200 at /v1/responses. (`google/gemini-3-1-pro` is not yet
supported there — model-list curation issue, separate from wiring.)

## Changes

1. plugins/model-providers/perplexity/__init__.py:
   - Add `api_mode="codex_responses"` (Hermes' name for the OpenAI
     Responses API transport)
   - Change `base_url` from `https://api.perplexity.ai` to
     `https://api.perplexity.ai/v1` so the OpenAI SDK's
     `responses.create()` resolves to `POST /v1/responses`
   - Expand docstring with the rationale and a pointer to the
     analogous xai profile (`api.x.ai/v1` + `codex_responses`)

2. hermes_cli/runtime_provider.py — `_detect_api_mode_for_url`:
   - Add `api.perplexity.ai` hostname branch returning `codex_responses`
   - Defense-in-depth: ensures URL-based autodetect picks Responses mode
     even when users set `model.base_url` out-of-band without the
     `api_mode` config key. Matches the existing `api.x.ai` /
     `api.openai.com` pattern.

3. tests/hermes_cli/test_detect_api_mode_for_url.py:
   - Add 3 cases to TestCodexResponsesDetection mirroring the xai/openai
     test pattern:
       - test_perplexity_api_returns_codex_responses
       - test_perplexity_host_suffix_does_not_match (lookalike protection)
       - test_perplexity_path_segment_does_not_match
   - Update module docstring header

## Test plan

`uv run pytest` on the 10 suites from NousResearch#23981's test plan plus the URL
detector tests:

  tests/tools/test_web_tools_perplexity.py
  tests/tools/test_web_tools_config.py
  tests/tools/test_web_tools_tavily.py
  tests/tools/test_web_providers.py
  tests/tools/test_web_providers_brave_free.py
  tests/tools/test_web_providers_ddgs.py
  tests/tools/test_web_providers_searxng.py
  tests/tools/test_tool_backend_helpers.py
  tests/providers/test_plugin_discovery.py
  tests/plugins/web/test_web_search_provider_plugins.py
  tests/hermes_cli/test_detect_api_mode_for_url.py

  -> 292 passed in 5.88s locally.

`ruff check` clean on all changed files.

## Live verification through the OpenAI SDK (the actual Hermes code path)

  from openai import OpenAI
  client = OpenAI(base_url="https://api.perplexity.ai/v1", api_key=...)

  r = client.responses.create(model="openai/gpt-5.4", input="say hi")
  # -> status=completed, model=openai/gpt-5.4, text="Hi, how are you?"

  r = client.responses.create(model="anthropic/claude-sonnet-4-6", input="say hi")
  # -> status=completed, model=anthropic/claude-sonnet-4-6, text="Hi there, friend!"

This is the same dispatch path used by `agent/codex_runtime.py:341`
(`active_client.responses.create(**fallback_kwargs)`) when
`agent.api_mode == "codex_responses"`.

## Not changed

- James's `fallback_models` list — `google/gemini-3-1-pro` doesn't work at
  /v1/responses, but model-list curation is James's call (he knows the
  Agent API roadmap; my list-based verification is a snapshot).
- Search API code paths — already work via /search at the root host;
  unrelated to /v1/responses. (PERPLEXITY_API_URL env override on the
  Search backend remains from the prior PR scope.)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/plugins Plugin system and bundled plugins comp/tools Tool registry, model_tools, toolsets P3 Low — cosmetic, nice to have tool/web Web search and extraction type/feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants