OBLETHdocs

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

You needA search tool: a model of type search whose API base is a SearXNG instance
EndpointPOST /v1/search, or POST /v1/search/{tool}
It costsNothing; logged as request type search

A 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. 1

    Turn on JSON output in SearXNG

    SearXNG answers JSON only when json is listed under search.formats in its settings. Without it, every search fails with a 403 from SearXNG.

  2. 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 /v1 URL. 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. 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.

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

FieldMeaning
queryRequired. A string, or a list of strings searched as one, joined with spaces. At most 2,000 characters
search_tool_name, modelThe search tool, when the path doesn't name it
max_results1 to 20. Results are cut to this many; without it, every result SearXNG returns
search_domain_filterUp 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
countryA 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, safesearchPassed 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
StatusMessage
400request 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
400search_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
403search tool is disabled, model not permitted for tenant
404no search tool is available to this key, search tool '<name>' is not registered
413request body too large: the body is over 64 KiB
429the search upstream is rate limiting; retry shortly
502The 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
503timed out waiting for search capacity, with Retry-After: 5

Credential and tenant errors are the same as on every route; see Data-plane routes.