Search API
Search the web through the gateway with /v1/search, in the Perplexity Search API shape, from a SearXNG instance you register as a search tool.
4 min readUpdated for v2.4.0Edit on GitHub
search whose API base is a SearXNG instancePOST /v1/search, or POST /v1/search/{tool}searchA client written for the Perplexity Search API, or for LiteLLM's /v1/search, which takes the same shape, works unchanged. The gateway runs each search through the JSON API of a SearXNG instance you run.
Register a search tool
- 1
Turn on JSON output in SearXNG
SearXNG answers JSON only when
jsonis listed undersearch.formatsin its settings. Without it, every search fails with a403from SearXNG. - 2
Add it as a model
On Models, add a model of type Web search (SearXNG) with the instance's root URL as the API base, not a
/v1URL. Its name is the tool name clients send. On save, the dashboard runs a test search and warns if JSON output is off, and the model turns on once its first health check, also a real search, passes. If the instance needs credentials, set the model's API key (sent as a bearer token) or upstream headers. - 3
Let tenants use it
A tenant with a model allowlist needs the tool's name on it, like any model.
Or through the Management API:
curl -X POST http://localhost:9180/api/v1/models \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"model_name": "web",
"upstream_model": "searxng",
"api_base": "http://searxng.example.internal:8080",
"model_type": "search"
}'
To try a tool from the dashboard, use the Playground's Search mode. To let chat models search, see the Web search boon.
Search
curl -s http://localhost:8088/v1/search \
-H "Authorization: Bearer $OBLETH_KEY" \
-H "Content-Type: application/json" \
-d '{
"search_tool_name": "web",
"query": "postgres logical replication slots",
"max_results": 5,
"search_domain_filter": ["postgresql.org"]
}'
The response:
{
"id": "0b7e8f4a-6c1d-4c55-9a43-2f1d8e0c9b12",
"object": "search",
"results": [
{
"title": "Logical Replication",
"url": "https://www.postgresql.org/docs/current/logical-replication.html",
"snippet": "Logical replication is a method of replicating data objects…",
"date": null,
"last_updated": null
}
]
}
id is the request id, also sent as x-obleth-request-id. snippet is SearXNG's content for the result, and date its publication date when SearXNG has one, else null. last_updated is always null.
Pick the search tool
The tool comes from the path (POST /v1/search/{tool}), else the body's search_tool_name, else its model. With none of these, the caller's only search tool is used; with several, the request is refused and names GET /v1/search/tools, which lists the tools the key may use:
{"object": "list", "data": [{"search_tool_name": "web", "search_provider": "searxng"}]}
The bare /search, /search/{tool} and /search/tools paths work too, as on LiteLLM.
Request fields
| Field | Meaning |
|---|---|
query | Required. A string, or a list of strings searched as one, joined with spaces. At most 2,000 characters |
search_tool_name, model | The search tool, when the path doesn't name it |
max_results | 1 to 20. Results are cut to this many; without it, every result SearXNG returns |
search_domain_filter | Up to 20 domains to keep results to. A domain covers its subdomains; -domain leaves it out instead. A single kept domain also adds site: to the query |
country | A two-letter country code, turned into a SearXNG language for countries with one obvious search language, when language isn't set |
categories, engines, language, pageno, time_range, safesearch | Passed to SearXNG. categories and engines also take a list |
LiteLLM ignores max_results and the domain filter; obleth applies both to the results after SearXNG answers.
What a search goes through
A search takes the same path as a model call: key authentication, the tenant's model allowlist, the tenant's input guardrails (the query is scanned like a prompt, and a redacted query is searched as redacted), and fairshare admission into the tool's own pool with no estimated tokens. It takes a slot, which protects the search upstream, but no share of anyone's tokens. Each search writes one usage row with request_type: "search" and no cost.
The call follows the tool's endpoints, moving on from one that is unreachable or answers 5xx. Its timeout is the model's request timeout, capped at 60 seconds. A tool can be staged, deprecated or retired like a model: a staged tool is not listed or picked by default, and a retired one is refused with 410 or answered by its replacement.
Search tools are not models. They are left out of /v1/models and /model/info, and a chat, embeddings or other request that names one gets 400 '<name>' is a search tool: call POST /v1/search.
Errors
| Status | Message |
|---|---|
400 | request body must be a JSON object, query is required, query must not be empty, query is too long (maximum 2000 characters), max_results must be an integer from 1 to 20, search_domain_filter takes at most 20 domains |
400 | search_tool_name is required: GET /v1/search/tools lists the choices, when the key may use several tools |
400 | '<name>' is registered as '<type>', not as a search tool |
403 | search tool is disabled, model not permitted for tenant |
404 | no search tool is available to this key, search tool '<name>' is not registered |
413 | request body too large: the body is over 64 KiB |
429 | the search upstream is rate limiting; retry shortly |
502 | The upstream failed, was unreachable or didn't answer JSON. A 403 from SearXNG reads the search upstream refused the request (HTTP 403); its JSON output may be disabled |
503 | timed out waiting for search capacity, with Retry-After: 5 |
Credential and tenant errors are the same as on every route; see Data-plane routes.
