[Feat] add serxng search API provider (#16259)

* TestFirecrawlSearch

* add SearchProviders

* add to get_provider_search_config

* add FirecrawlSearchConfig

* add FirecrawlSearchRequest

* add firecrawl API docs

* add pricing firecrawl/search

* add new search APIs

* add SearXNGSearchConfig

* add searxng/search

* add serxng params

* TestSearXNGSearch

* docs serxng

* docs fix

* docs fix

* docs serxng
This commit is contained in:
Ishaan Jaff 2025-11-04 17:56:07 -08:00 committed by GitHub
parent af78a93ecf
commit 60f3a3b0ad
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 705 additions and 2 deletions

View File

@ -2,7 +2,7 @@
| Feature | Supported |
|---------|-----------|
| Supported Providers | `perplexity`, `tavily`, `parallel_ai`, `exa_ai`, `google_pse`, `dataforseo`, `firecrawl` |
| Supported Providers | `perplexity`, `tavily`, `parallel_ai`, `exa_ai`, `google_pse`, `dataforseo`, `firecrawl`, `searxng` |
| Cost Tracking | ✅ |
| Logging | ✅ |
| Load Balancing | ❌ |
@ -205,7 +205,7 @@ See the [official Perplexity Search documentation](https://docs.perplexity.ai/ap
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `query` | string or array | Yes | Search query. Can be a single string or array of strings |
| `search_provider` | string | Yes (SDK) | The search provider to use: `"perplexity"`, `"tavily"`, `"parallel_ai"`, `"exa_ai"`, `"google_pse"`, `"dataforseo"`, or `"firecrawl"` |
| `search_provider` | string | Yes (SDK) | The search provider to use: `"perplexity"`, `"tavily"`, `"parallel_ai"`, `"exa_ai"`, `"google_pse"`, `"dataforseo"`, `"firecrawl"`, or `"searxng"` |
| `search_tool_name` | string | Yes (Proxy) | Name of the search tool configured in `config.yaml` |
| `max_results` | integer | No | Maximum number of results to return (1-20). Default: 10 |
| `search_domain_filter` | array | No | List of domains to filter results (max 20 domains) |
@ -268,6 +268,7 @@ The response follows Perplexity's search format with the following structure:
| Google PSE | `GOOGLE_PSE_API_KEY`, `GOOGLE_PSE_ENGINE_ID` | `google_pse` |
| DataForSEO | `DATAFORSEO_LOGIN`, `DATAFORSEO_PASSWORD` | `dataforseo` |
| Firecrawl | `FIRECRAWL_API_KEY` | `firecrawl` |
| SearXNG | `SEARXNG_API_BASE` (required) | `searxng` |
See the individual provider documentation for detailed setup instructions and provider-specific parameters.

View File

@ -0,0 +1,318 @@
# SearXNG Search
**Open Source:** [https://github.com/searxng/searxng](https://github.com/searxng/searxng)
**Public Instances:** [https://searx.space/](https://searx.space/)
## Overview
SearXNG is a free, open-source metasearch engine that aggregates results from multiple search engines while protecting user privacy. It can be self-hosted or used via public instances.
**Note:** SearXNG returns a fixed number of results per page (~20 by default) and does not support limiting results via the API. The `max_results` parameter is not directly supported by SearXNG.
## LiteLLM Python SDK
```python showLineNumbers title="SearXNG Search"
import os
from litellm import search
# Set your SearXNG instance URL (REQUIRED)
os.environ["SEARXNG_API_BASE"] = "https://serxng-deployment-production.up.railway.app"
response = search(
query="latest AI developments",
search_provider="searxng",
max_results=10
)
```
## LiteLLM AI Gateway
### 1. Setup config.yaml
```yaml showLineNumbers title="config.yaml"
model_list:
- model_name: gpt-4
litellm_params:
model: gpt-4
api_key: os.environ/OPENAI_API_KEY
search_tools:
- search_tool_name: searxng-search
litellm_params:
search_provider: searxng
api_base: https://serxng-deployment-production.up.railway.app
```
### 2. Start the proxy
```bash
litellm --config /path/to/config.yaml
# RUNNING on http://0.0.0.0:4000
```
### 3. Test the search endpoint
```bash showLineNumbers title="Test Request"
curl http://0.0.0.0:4000/v1/search/searxng-search \
-H "Authorization: Bearer sk-1234" \
-H "Content-Type: application/json" \
-d '{
"query": "latest AI developments",
"max_results": 10
}'
```
## Provider-specific Parameters
```python showLineNumbers title="SearXNG Search with Provider-specific Parameters"
import os
from litellm import search
# REQUIRED: Set your SearXNG instance URL
os.environ["SEARXNG_API_BASE"] = "https://serxng-deployment-production.up.railway.app"
response = search(
query="machine learning research",
search_provider="searxng",
max_results=10,
# SearXNG-specific parameters
categories="general,science", # Comma-separated categories
engines="google,duckduckgo,bing", # Comma-separated engines
language="en", # Language code
pageno=1, # Page number
time_range="month" # Time filter: day, month, year
)
```
## Features
SearXNG provides powerful metasearch capabilities:
### Multiple Search Engines
Aggregate results from multiple search engines simultaneously:
- Google, DuckDuckGo, Bing, Brave
- Wikipedia, Startpage
- And many more
### Categories
Search within specific categories:
- `general` - General web search
- `science` - Scientific articles and papers
- `images` - Image search
- `news` - News articles
- `videos` - Video content
- `music` - Music and audio
- `files` - File search
- `it` - IT and technology
- `map` - Maps and location
### Time-Based Filtering
Filter results by time range:
- `day` - Past day
- `month` - Past month
- `year` - Past year
### Privacy-Focused
- No user tracking
- No cookies required
- No profiling
- No ads
### Language Support
Support for 60+ languages with the `language` parameter.
## Self-Hosting
SearXNG can be self-hosted for complete control.
### Quick Deploy
Use our pre-configured deployment repository for easy setup:
**[Fork and Deploy: github.com/BerriAI/serxng-deployment](https://github.com/BerriAI/serxng-deployment)**
This repository includes:
- Docker and Docker Compose setup
- JSON API format pre-configured
- Ready to deploy
### Manual Installation
See the [official SearXNG installation instructions](https://docs.searxng.org/admin/installation.html) for detailed setup.
**Important:** When you install SearXNG, the only active output format by default is the HTML format. You need to activate the JSON format to use the API.
Add the following to your `settings.yml` file:
```yaml
search:
formats:
- html
- json
```
Then restart SearXNG:
```bash
# Using Docker
docker run -d -p 8080:8080 \
-v $(pwd)/settings.yml:/etc/searxng/settings.yml:ro \
-e SEARXNG_BASE_URL=http://localhost:8080 \
searxng/searxng
# Then configure LiteLLM to use your instance
export SEARXNG_API_BASE=http://localhost:8080
```
## Configuration
### Setting API Base URL (Required)
You **must** specify a SearXNG instance URL either via environment variable or in the search call:
```python
# Option 1: Environment variable (Recommended)
import os
os.environ["SEARXNG_API_BASE"] = "https://your-instance.com"
response = search(
query="AI developments",
search_provider="searxng"
)
# Option 2: Pass directly in search call
response = search(
query="AI developments",
search_provider="searxng",
api_base="https://your-instance.com"
)
```
**Note:** There is no default instance URL. You must choose either a [public instance](https://searx.space/) or self-host your own.
### Optional Authentication
Some SearXNG instances may require authentication:
```python
import os
# Set API key if required
os.environ["SEARXNG_API_KEY"] = "your-api-key"
response = search(
query="AI developments",
search_provider="searxng"
)
```
## Cost
SearXNG is completely free:
- **Open source** - No licensing costs
- **Self-hosted** - Only infrastructure costs
- **Public instances** - Usually free, check instance policies
## Advanced Usage
### Custom Engine Selection
```python
response = search(
query="Python tutorials",
search_provider="searxng",
engines="stackoverflow,github,reddit", # Only search these engines
categories="it"
)
```
### Multi-Category Search
```python
response = search(
query="climate change",
search_provider="searxng",
categories="general,science,news", # Search multiple categories
time_range="month"
)
```
### Pagination
```python
# Get page 1
page1 = search(
query="AI research",
search_provider="searxng",
pageno=1
)
# Get page 2
page2 = search(
query="AI research",
search_provider="searxng",
pageno=2
)
```
## Response Format
SearXNG returns results in the standard LiteLLM search format:
```json
{
"object": "search",
"results": [
{
"title": "Example Result",
"url": "https://example.com",
"snippet": "This is the content snippet from the search result...",
"date": "2024-01-15",
"last_updated": null
}
]
}
```
## Troubleshooting
### Test Your Instance First
If LiteLLM with searxng search provider is not working, test your SearXNG instance directly with curl:
```bash
# Test if JSON API is working
curl -s "https://your-searxng-instance.com/search?q=test&format=json" | head -50
# Example with specific instance
curl -s "https://serxng-deployment-production.up.railway.app/search?q=test&format=json" | head -50
```
**Expected response**: JSON with search results
**If you get HTML**: JSON format is not enabled in the instance's `settings.yml`
### No Results
If you get no results:
1. **Try different engines**: Specify `engines` parameter
2. **Broaden categories**: Use multiple categories
3. **Adjust language**: Set appropriate `language` parameter
### JSON Format Not Enabled
If you get HTML instead of JSON:
1. **Test with curl**: Use the curl command above to verify JSON output
2. **Self-host your own instance**: Use [our deployment repo](https://github.com/BerriAI/serxng-deployment) with JSON pre-configured
3. **Check instance configuration**: Not all public instances have JSON enabled
4. **Enable JSON manually**: Add to `settings.yml`:
```yaml
search:
formats:
- html
- json
```

View File

@ -398,6 +398,8 @@ const sidebars = {
"search/parallel_ai",
"search/google_pse",
"search/dataforseo",
"search/firecrawl",
"search/searxng",
]
},
{

View File

@ -0,0 +1,7 @@
"""
SearXNG API integration module.
"""
from litellm.llms.searxng.search.transformation import SearXNGSearchConfig
__all__ = ["SearXNGSearchConfig"]

View File

@ -0,0 +1,7 @@
"""
SearXNG Search API module.
"""
from litellm.llms.searxng.search.transformation import SearXNGSearchConfig
__all__ = ["SearXNGSearchConfig"]

View File

@ -0,0 +1,223 @@
"""
Calls SearXNG's /search endpoint to search the web.
SearXNG API Reference: https://docs.searxng.org/dev/search_api.html
"""
from typing import Dict, List, Optional, TypedDict, Union
import httpx
from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj
from litellm.llms.base_llm.search.transformation import (
BaseSearchConfig,
SearchResponse,
SearchResult,
)
from litellm.secret_managers.main import get_secret_str
class _SearXNGSearchRequestRequired(TypedDict):
"""Required fields for SearXNG Search API request."""
q: str # Required - search query
class SearXNGSearchRequest(_SearXNGSearchRequestRequired, total=False):
"""
SearXNG Search API request format.
Based on: https://docs.searxng.org/dev/search_api.html
"""
categories: str # Optional - comma-separated list of categories
engines: str # Optional - comma-separated list of engines
language: str # Optional - language code
pageno: int # Optional - page number (default 1)
time_range: str # Optional - time range filter (day, month, year)
format: str # Optional - output format (json, csv, rss) - should be 'json'
class SearXNGSearchConfig(BaseSearchConfig):
@staticmethod
def ui_friendly_name() -> str:
return "SearXNG"
def get_http_method(self):
"""
SearXNG supports both GET and POST, but we'll use GET for simplicity.
"""
return "GET"
def validate_environment(
self,
headers: Dict,
api_key: Optional[str] = None,
api_base: Optional[str] = None,
**kwargs,
) -> Dict:
"""
Validate environment and return headers.
SearXNG is open-source and doesn't require an API key by default.
Some instances may require authentication via headers.
"""
# SearXNG typically doesn't require API keys, but support optional auth
api_key = api_key or get_secret_str("SEARXNG_API_KEY")
if api_key:
headers["Authorization"] = f"Bearer {api_key}"
headers["Content-Type"] = "application/json"
return headers
def get_complete_url(
self,
api_base: Optional[str],
optional_params: dict,
data: Optional[Union[Dict, List[Dict]]] = None,
**kwargs,
) -> str:
"""
Get complete URL for Search endpoint with query parameters.
SearXNG uses GET requests, so we build the full URL with query params here.
The transformed request body (data) contains the parameters needed for the URL.
"""
from urllib.parse import urlencode
api_base = api_base or get_secret_str("SEARXNG_API_BASE")
if not api_base:
raise ValueError(
"SEARXNG_API_BASE is not set. Please set the `SEARXNG_API_BASE` environment variable "
"or pass `api_base` parameter. Example: os.environ['SEARXNG_API_BASE'] = 'https://your-searxng-instance.com'"
)
# Append "/search" to the api base if it's not already there
if not api_base.endswith("/search"):
if api_base.endswith("/"):
api_base = f"{api_base}search"
else:
api_base = f"{api_base}/search"
# Build query parameters from the transformed request body
if data and isinstance(data, dict) and "_searxng_params" in data:
params = data["_searxng_params"]
query_string = urlencode(params)
return f"{api_base}?{query_string}"
return api_base
def transform_search_request(
self,
query: Union[str, List[str]],
optional_params: dict,
**kwargs,
) -> Dict:
"""
Transform Search request to SearXNG API format.
Transforms Perplexity unified spec parameters:
- query q
- max_results (handled via pageno, SearXNG returns ~20 results per page)
- search_domain_filter (not directly supported)
- country language (approximate mapping)
- max_tokens_per_page (not applicable, ignored)
All other SearXNG-specific parameters are passed through as-is.
Args:
query: Search query (string or list of strings). SearXNG only supports single string queries.
optional_params: Optional parameters for the request
Returns:
Dict with typed request data following SearXNGSearchRequest spec
"""
if isinstance(query, list):
# SearXNG only supports single string queries, join with spaces
query = " ".join(query)
request_data: SearXNGSearchRequest = {
"q": query,
"format": "json", # Always request JSON format
}
# Transform Perplexity unified spec parameters to SearXNG format
if "country" in optional_params:
# Map country code to language (approximate)
country = optional_params["country"].lower()
if country == "us" or country == "uk":
request_data["language"] = "en"
elif country == "de":
request_data["language"] = "de"
elif country == "fr":
request_data["language"] = "fr"
elif country == "es":
request_data["language"] = "es"
elif country == "jp":
request_data["language"] = "ja"
else:
request_data["language"] = country # Pass through as-is
# Handle max_results via pagination (SearXNG returns ~20 results per page by default)
# For simplicity, we'll just use page 1 and let SearXNG return its default number of results
if "max_results" in optional_params:
# Note: We could calculate pageno based on max_results, but for now we'll ignore this
# and let SearXNG return its default results
pass
# Convert to dict before dynamic key assignments
result_data = dict(request_data)
# Pass through all other SearXNG-specific parameters as-is
for param, value in optional_params.items():
if param not in self.get_supported_perplexity_optional_params() and param not in result_data:
result_data[param] = value
# Store params in special key for GET request URL building
# This will be used by get_complete_url to build the query string
return {"_searxng_params": result_data}
def transform_search_response(
self,
raw_response: httpx.Response,
logging_obj: LiteLLMLoggingObj,
**kwargs,
) -> SearchResponse:
"""
Transform SearXNG API response to LiteLLM unified SearchResponse format.
SearXNG LiteLLM mappings:
- results[].title SearchResult.title
- results[].url SearchResult.url
- results[].content SearchResult.snippet
- results[].publishedDate OR results[].pubdate SearchResult.date
- No last_updated field in SearXNG response (set to None)
Args:
raw_response: Raw httpx response from SearXNG API
logging_obj: Logging object for tracking
Returns:
SearchResponse with standardized format
"""
response_json = raw_response.json()
# Transform results to SearchResult objects
# Note: SearXNG doesn't natively support limiting results via API params
# It returns ~20 results per page by default
results = []
for result in response_json.get("results", []):
# Get date from either publishedDate or pubdate field
date = result.get("publishedDate") or result.get("pubdate")
search_result = SearchResult(
title=result.get("title", ""),
url=result.get("url", ""),
snippet=result.get("content", ""), # SearXNG uses "content" for snippet
date=date,
last_updated=None, # SearXNG doesn't provide last_updated in response
)
results.append(search_result)
return SearchResponse(
results=results,
object="search",
)

View File

@ -7971,6 +7971,14 @@
"litellm_provider": "perplexity",
"mode": "search"
},
"searxng/search": {
"litellm_provider": "searxng",
"mode": "search",
"input_cost_per_query": 0.0,
"metadata": {
"notes": "SearXNG is an open-source metasearch engine. Free to use when self-hosted or using public instances."
}
},
"elevenlabs/scribe_v1": {
"input_cost_per_second": 6.11e-05,
"litellm_provider": "elevenlabs",

View File

@ -2595,6 +2595,7 @@ class SearchProviders(str, Enum):
GOOGLE_PSE = "google_pse"
DATAFORSEO = "dataforseo"
FIRECRAWL = "firecrawl"
SEARXNG = "searxng"
# Create a set of all search provider values for quick lookup

View File

@ -7733,6 +7733,7 @@ class ProviderConfigManager:
ParallelAISearchConfig,
)
from litellm.llms.perplexity.search.transformation import PerplexitySearchConfig
from litellm.llms.searxng.search.transformation import SearXNGSearchConfig
from litellm.llms.tavily.search.transformation import TavilySearchConfig
PROVIDER_TO_CONFIG_MAP = {
@ -7743,6 +7744,7 @@ class ProviderConfigManager:
SearchProviders.GOOGLE_PSE: GooglePSESearchConfig,
SearchProviders.DATAFORSEO: DataForSEOSearchConfig,
SearchProviders.FIRECRAWL: FirecrawlSearchConfig,
SearchProviders.SEARXNG: SearXNGSearchConfig,
}
config_class = PROVIDER_TO_CONFIG_MAP.get(provider, None)
if config_class is None:

View File

@ -7971,6 +7971,14 @@
"litellm_provider": "perplexity",
"mode": "search"
},
"searxng/search": {
"litellm_provider": "searxng",
"mode": "search",
"input_cost_per_query": 0.0,
"metadata": {
"notes": "SearXNG is an open-source metasearch engine. Free to use when self-hosted or using public instances."
}
},
"elevenlabs/scribe_v1": {
"input_cost_per_second": 6.11e-05,
"litellm_provider": "elevenlabs",

View File

@ -1374,6 +1374,23 @@
"rerank": false
}
},
"searxng": {
"display_name": "SearXNG (`searxng`)",
"url": "https://docs.litellm.ai/docs/search/searxng",
"endpoints": {
"chat_completions": false,
"messages": false,
"responses": false,
"embeddings": false,
"image_generations": false,
"audio_transcriptions": false,
"audio_speech": false,
"moderations": false,
"batches": false,
"rerank": false,
"search": true
}
},
"sambanova": {
"display_name": "Sambanova (`sambanova`)",
"url": "https://docs.litellm.ai/docs/providers/sambanova",

View File

@ -0,0 +1,109 @@
import pytest
import litellm
import os
from typing import List, Union
from tests.search_tests.base_search_unit_tests import BaseSearchTest
class TestSearXNGSearch(BaseSearchTest):
"""
Tests for SearXNG Search functionality.
"""
def get_search_provider(self) -> str:
"""
Return search_provider for SearXNG Search.
"""
return "searxng"
@pytest.mark.asyncio
async def test_basic_search(self):
"""
Test basic search functionality with a simple query.
Override to handle free (0.0 cost) provider.
"""
os.environ["LITELLM_LOCAL_MODEL_COST_MAP"] = "True"
litellm.model_cost = litellm.get_model_cost_map(url="")
litellm._turn_on_debug()
search_provider = self.get_search_provider()
print("Search Provider=", search_provider)
try:
response = await litellm.asearch(
query="latest developments in AI",
search_provider=search_provider,
)
print("Search response=", response.model_dump_json(indent=4))
print(f"\n{'='*80}")
print(f"Response type: {type(response)}")
print(f"Response object: {response.object if hasattr(response, 'object') else 'N/A'}")
# Check if response has expected Search format
assert hasattr(response, "results"), "Response should have 'results' attribute"
assert hasattr(response, "object"), "Response should have 'object' attribute"
assert response.object == "search", f"Expected object='search', got '{response.object}'"
# Validate results structure
assert isinstance(response.results, list), "results should be a list"
assert len(response.results) > 0, "Should have at least one result"
# Check first result structure
first_result = response.results[0]
assert hasattr(first_result, "title"), "Result should have 'title' attribute"
assert hasattr(first_result, "url"), "Result should have 'url' attribute"
assert hasattr(first_result, "snippet"), "Result should have 'snippet' attribute"
print(f"Total results: {len(response.results)}")
print(f"First result title: {first_result.title}")
print(f"First result URL: {first_result.url}")
print(f"First result snippet: {first_result.snippet[:100]}...")
print(f"{'='*80}\n")
assert len(first_result.title) > 0, "Title should not be empty"
assert len(first_result.url) > 0, "URL should not be empty"
assert len(first_result.snippet) > 0, "Snippet should not be empty"
# Validate cost tracking in _hidden_params
# For SearXNG (free provider), cost can be None or 0.0
assert hasattr(response, "_hidden_params"), "Response should have '_hidden_params' attribute"
hidden_params = response._hidden_params
assert "response_cost" in hidden_params, "_hidden_params should contain 'response_cost'"
response_cost = hidden_params["response_cost"]
# SearXNG is free, so cost can be None or 0.0
if response_cost is not None:
assert isinstance(response_cost, (int, float)), "response_cost should be a number"
assert response_cost >= 0, "response_cost should be non-negative"
print(f"Cost tracking: ${response_cost:.6f}")
else:
print(f"Cost tracking: Free (None)")
except Exception as e:
pytest.fail(f"Search call failed: {str(e)}")
def test_search_with_optional_params(self):
"""
Test search with optional parameters.
Override for SearXNG since it doesn't natively limit results.
"""
litellm.set_verbose = True
search_provider = self.get_search_provider()
response = litellm.search(
query="machine learning",
search_provider=search_provider,
max_results=5,
)
# Validate response
assert hasattr(response, "results"), "Response should have 'results' attribute"
assert isinstance(response.results, list), "results should be a list"
assert len(response.results) > 0, "Should have at least one result"
# Note: SearXNG doesn't natively limit results, so we don't check <= 5
print(f"\nSearch with optional params validated:")
print(f" - Requested max_results: 5")
print(f" - Received results: {len(response.results)}")