Import the existing local project snapshot. This commit organizes existing files; it does not represent newly developed functionality.
Axon Search API
Axon is a private, self-hosted AI search API for Linux servers. It aggregates search results from Brave Search, DuckDuckGo, and Serper.dev, optionally scrapes full page content, reranks results with a local BGE cross-encoder, and can synthesize answers or research reports through any OpenAI-compatible LLM endpoint.
Features
POST /searchfor multi-source search with optional scraping, reranking, and answer synthesisPOST /extractfor clean content extraction from one or more URLsPOST /researchfor multi-query research reportsPOST /crawlfor same-site traversal with extractionPOST /mapfor same-site URL discoveryGET /healthfor cache and reranker statusGET /metricsfor lightweight request counters and latencyPOST /ingestandPOST /index/searchfor private local document retrieval/research/jobs,/crawl/jobs,/map/jobs, andGET /jobs/{id}for async long-running work/tavily/*compatibility routes for easier migrationPOST /validate/citationsfor basic citation coverage checks- API key auth via
X-API-Key - Per-key/IP rate limiting
- Redis cache with in-memory fallback
- CORS enabled for all origins
- SSRF protection for URL scraping and extraction
Setup
Axon requires Python 3.11 or newer.
python -m venv venv
source venv/bin/activate
pip install -r requirements.txt
cp .env.example .env
Edit .env with any API keys you want to enable. At least DuckDuckGo works without a key, but Brave and Serper improve coverage and reliability.
Start the server:
uvicorn axon.main:app --host 0.0.0.0 --port 8000
The reranker model is loaded at startup. The first run may download the configured HuggingFace model and can take longer than later starts.
Configuration
| Variable | Required | Default | Purpose |
|---|---|---|---|
BRAVE_API_KEY |
No | empty | Enables Brave Search results |
SERPER_API_KEY |
No | empty | Enables Serper.dev fallback results |
REDIS_URL |
No | redis://localhost:6379 |
Redis cache URL; falls back to memory if unavailable |
API_KEYS |
No | empty | Comma-separated accepted API keys; empty disables auth |
LITELLM_BASE_URL |
No | http://localhost:4000 |
OpenAI-compatible chat completions base URL |
LITELLM_API_KEY |
No | empty | Bearer token for synthesis requests |
SYNTHESIS_MODEL |
No | claude-sonnet-4-6 |
Model sent to the LLM endpoint |
RERANKER_MODEL |
No | BAAI/bge-reranker-v2-m3 |
Sentence Transformers cross-encoder model |
PORT |
No | 8000 |
Preferred server port |
MAX_CONCURRENT_SCRAPES |
No | 10 |
Concurrent scrape limit for advanced search |
SCRAPE_TIMEOUT |
No | 5 |
Per-URL scrape timeout in seconds |
EXTRACT_TIMEOUT |
No | 10 |
Per-URL extraction timeout in seconds |
RATE_LIMIT_PER_MINUTE |
No | 60 |
Per API key or client IP limit; 0 disables |
CRAWL_MAX_PAGES |
No | 50 |
Server-side cap for crawl/map pages |
CRAWL_MAX_DEPTH |
No | 3 |
Server-side cap for crawl/map depth |
INDEX_DB_PATH |
No | axon_index.sqlite3 |
SQLite FTS database path for private indexed docs |
ENABLE_BROWSER_FALLBACK |
No | false |
Enables optional Playwright rendering fallback when installed |
Authentication
If API_KEYS is unset or empty, Axon runs in development mode and does not require authentication.
If API_KEYS is set, send one configured key with each protected request:
curl -H "X-API-Key: your-secret-key" http://localhost:8000/health
/health, /docs, /openapi.json, and /redoc are exempt.
Endpoints
Health
curl http://localhost:8000/health
Example response:
{
"status": "ok",
"redis": "in-memory fallback",
"reranker": "loaded"
}
Metrics
curl http://localhost:8000/metrics
Example response:
{
"requests_total": 42,
"requests_by_path": {
"/search": 10,
"/extract": 3
},
"errors_total": 0,
"average_latency_ms": 1234.5
}
Search
Basic search uses search API snippets and reranks them:
curl -X POST http://localhost:8000/search \
-H "Content-Type: application/json" \
-H "X-API-Key: your-secret-key" \
-d '{
"query": "latest PostgreSQL indexing improvements",
"max_results": 10,
"search_depth": "basic",
"include_answer": false
}'
Advanced search scrapes result pages before reranking:
curl -X POST http://localhost:8000/search \
-H "Content-Type: application/json" \
-H "X-API-Key: your-secret-key" \
-d '{
"query": "private AI search APIs",
"max_results": 10,
"search_depth": "advanced",
"include_answer": true,
"include_raw_content": false,
"include_private": true,
"domains_blacklist": ["reddit.com"],
"topic": "general",
"time_range": "month"
}'
Extract
curl -X POST http://localhost:8000/extract \
-H "Content-Type: application/json" \
-H "X-API-Key: your-secret-key" \
-d '{
"urls": [
"https://example.com/article"
],
"include_images": false
}'
Each URL is processed independently. Failed URLs return success: false with an error while the rest of the request continues.
Research
curl -X POST http://localhost:8000/research \
-H "Content-Type: application/json" \
-H "X-API-Key: your-secret-key" \
-d '{
"query": "state of open source vector databases",
"depth": "deep",
"search_depth": "basic",
"max_results_per_query": 5,
"output_format": "markdown"
}'
depth: "basic" generates 3 sub-queries. depth: "deep" generates 5 sub-queries. Sources are deduplicated by URL, and the final report is synthesized from the combined results. Use search_depth: "basic" for faster research, or advanced when scraped page content is worth the extra latency.
For long-running work, create a background job:
curl -X POST http://localhost:8000/research/jobs \
-H "Content-Type: application/json" \
-H "X-API-Key: your-secret-key" \
-d '{"query": "OpenAI", "search_depth": "basic"}'
curl http://localhost:8000/jobs/<job_id> \
-H "X-API-Key: your-secret-key"
Crawl
curl -X POST http://localhost:8000/crawl \
-H "Content-Type: application/json" \
-H "X-API-Key: your-secret-key" \
-d '{
"url": "https://docs.example.com",
"max_depth": 1,
"max_pages": 20,
"include_raw_content": false,
"instructions": "python sdk"
}'
/crawl stays on the same hostname as the start URL, extracts each matching page, and returns discovered links. It is intentionally bounded by CRAWL_MAX_PAGES and CRAWL_MAX_DEPTH.
Map
curl -X POST http://localhost:8000/map \
-H "Content-Type: application/json" \
-H "X-API-Key: your-secret-key" \
-d '{
"url": "https://docs.example.com",
"max_depth": 2,
"max_pages": 50,
"instructions": "api reference"
}'
/map returns same-site URLs without extracting full content.
Private Index
Ingest private documents:
curl -X POST http://localhost:8000/ingest \
-H "Content-Type: application/json" \
-H "X-API-Key: your-secret-key" \
-d '{
"documents": [
{
"id": "runbook-1",
"title": "Production Runbook",
"content": "Internal deployment and rollback notes.",
"url": "private://runbook-1"
}
]
}'
Search only the private index:
curl -X POST http://localhost:8000/index/search \
-H "Content-Type: application/json" \
-H "X-API-Key: your-secret-key" \
-d '{"query": "rollback notes"}'
Normal /search includes private index hits by default. Set "include_private": false to use only web sources.
Tavily-Compatible Routes
Axon exposes compatibility routes under /tavily:
curl -X POST http://localhost:8000/tavily/search \
-H "Content-Type: application/json" \
-H "X-API-Key: your-secret-key" \
-d '{"query": "OpenAI", "max_results": 5}'
Available compatibility routes:
/tavily/search/tavily/extract/tavily/crawl/tavily/map
Citation Validation
curl -X POST http://localhost:8000/validate/citations \
-H "Content-Type: application/json" \
-H "X-API-Key: your-secret-key" \
-d '{
"report": "OpenAI was founded in 2015 [1].",
"sources": [
{"title": "Source", "url": "https://example.com", "content": "OpenAI was founded in 2015."}
]
}'
Search Source Logic
Axon fetches Brave and DuckDuckGo concurrently. Serper.dev is called only when Brave returns fewer than 5 results. All results are merged and deduplicated by URL before filtering, scraping, reranking, and response truncation.
If all enabled sources return no results, /search returns HTTP 503. Individual source failures are logged and treated as empty result sets.
DuckDuckGo uses the current ddgs package when installed and falls back to duckduckgo-search for compatibility.
Caching
- Search responses are cached for 10 minutes.
- Successful extracted URL content is cached for 1 hour.
- Redis is used when
REDIS_URLis configured and reachable. - If Redis is unavailable, Axon logs a warning and uses in-memory cache for the current process.
Performance Notes
Advanced search is slower than basic search because it fetches and extracts each result page. Tune MAX_CONCURRENT_SCRAPES and SCRAPE_TIMEOUT for your server and network limits.
The reranker model is loaded once during FastAPI startup and reused for requests. Use a smaller cross-encoder model if startup time, memory use, or CPU latency is too high for your deployment.
Reranking is serialized through a process-local lock and capped to a small number of chunks per document. This keeps the self-hosted service stable on modest servers, at the cost of some recall on very long pages.
Optional browser rendering requires installing Playwright and browser assets separately, then setting ENABLE_BROWSER_FALLBACK=true.
Development
Run tests:
source venv/bin/activate
python -m pytest tests/ -v
Run a focused test file:
python -m pytest tests/test_search_router.py -v