Relying on a single search provider for your local LLM is a single point of failure. When that provider goes down or hits rate limits, your agent loses web grounding entirely. This tutorial builds a search fallback stack that tries Scavio first ($0.005/query), falls back to Brave Search ($0.005/query), then Tavily ($0.008/credit) if both are unavailable. The stack normalizes responses into a common format so your LLM sees consistent data regardless of which provider responded.
Prerequisites
- Python 3.9+ installed
- requests library installed
- API keys for Scavio, Brave Search, and Tavily
- A local LLM setup (Ollama, llama.cpp, or vLLM)
Walkthrough
Step 1: Define the provider interface
Create a common interface that each search provider implements. All providers return the same normalized result format.
import os, requests, time
from typing import Optional
SCAVIO_KEY = os.environ.get('SCAVIO_API_KEY', '')
BRAVE_KEY = os.environ.get('BRAVE_API_KEY', '')
TAVILY_KEY = os.environ.get('TAVILY_API_KEY', '')
def normalize_result(title: str, url: str, snippet: str, source: str) -> dict:
return {'title': title, 'url': url, 'snippet': snippet, 'source': source}Step 2: Implement each search provider
Write a search function for each provider that returns normalized results. Each function handles its own errors and returns None on failure.
def search_scavio(query: str, num: int = 5) -> Optional[list]:
try:
resp = requests.post('https://api.scavio.dev/api/v1/search',
headers={'x-api-key': SCAVIO_KEY, 'Content-Type': 'application/json'},
json={'query': query, 'country_code': 'us', 'num_results': num}, timeout=10)
resp.raise_for_status()
return [normalize_result(r['title'], r['link'], r.get('snippet', ''), 'scavio')
for r in resp.json().get('organic_results', [])]
except Exception as e:
print(f'Scavio failed: {e}')
return None
def search_brave(query: str, num: int = 5) -> Optional[list]:
try:
resp = requests.get('https://api.search.brave.com/res/v1/web/search',
headers={'X-Subscription-Token': BRAVE_KEY, 'Accept': 'application/json'},
params={'q': query, 'count': num}, timeout=10)
resp.raise_for_status()
return [normalize_result(r['title'], r['url'], r.get('description', ''), 'brave')
for r in resp.json().get('web', {}).get('results', [])]
except Exception as e:
print(f'Brave failed: {e}')
return None
def search_tavily(query: str, num: int = 5) -> Optional[list]:
try:
resp = requests.post('https://api.tavily.com/search',
json={'api_key': TAVILY_KEY, 'query': query, 'max_results': num}, timeout=10)
resp.raise_for_status()
return [normalize_result(r['title'], r['url'], r.get('content', ''), 'tavily')
for r in resp.json().get('results', [])]
except Exception as e:
print(f'Tavily failed: {e}')
return NoneStep 3: Build the fallback stack
Chain providers in priority order. The first provider to return results wins. Log which provider served the request.
PROVIDERS = [
('scavio', search_scavio, 0.005),
('brave', search_brave, 0.005),
('tavily', search_tavily, 0.008),
]
def search_with_fallback(query: str, num: int = 5) -> dict:
for name, fn, cost in PROVIDERS:
results = fn(query, num)
if results is not None:
return {'results': results, 'provider': name, 'cost': cost, 'query': query}
return {'results': [], 'provider': 'none', 'cost': 0, 'query': query}
# Test the fallback
result = search_with_fallback('best python web frameworks 2026')
print(f'Provider: {result["provider"]} (${result["cost"]}/query)')
for r in result['results'][:3]:
print(f' {r["title"]}')
print(f' {r["url"]}')Step 4: Integrate with your local LLM
Use the fallback search as a tool for your local LLM. The LLM never needs to know which provider responded.
def format_for_llm(search_result: dict) -> str:
lines = [f'Web search results for: {search_result["query"]}\n']
for i, r in enumerate(search_result['results'], 1):
lines.append(f'[{i}] {r["title"]}')
lines.append(f' URL: {r["url"]}')
lines.append(f' {r["snippet"]}')
lines.append('')
return '\n'.join(lines)
result = search_with_fallback('latest AI frameworks 2026')
formatted = format_for_llm(result)
print(formatted)
print(f'\n--- Served by: {result["provider"]} at ${result["cost"]}/query ---')Python Example
import os, requests
from typing import Optional
SCAVIO_KEY = os.environ.get('SCAVIO_API_KEY', '')
BRAVE_KEY = os.environ.get('BRAVE_API_KEY', '')
def search_scavio(query, num=5):
try:
resp = requests.post('https://api.scavio.dev/api/v1/search',
headers={'x-api-key': SCAVIO_KEY, 'Content-Type': 'application/json'},
json={'query': query, 'country_code': 'us', 'num_results': num}, timeout=10)
resp.raise_for_status()
return [{'title': r['title'], 'url': r['link'], 'snippet': r.get('snippet', '')}
for r in resp.json().get('organic_results', [])]
except: return None
def search_brave(query, num=5):
try:
resp = requests.get('https://api.search.brave.com/res/v1/web/search',
headers={'X-Subscription-Token': BRAVE_KEY},
params={'q': query, 'count': num}, timeout=10)
resp.raise_for_status()
return [{'title': r['title'], 'url': r['url'], 'snippet': r.get('description', '')}
for r in resp.json().get('web', {}).get('results', [])]
except: return None
def search(query):
for name, fn in [('scavio', search_scavio), ('brave', search_brave)]:
results = fn(query)
if results:
print(f'Served by {name}')
return results
return []
for r in search('python web frameworks 2026')[:3]:
print(f' {r["title"]}')JavaScript Example
const SCAVIO_KEY = process.env.SCAVIO_API_KEY;
const BRAVE_KEY = process.env.BRAVE_API_KEY;
async function searchScavio(query) {
try {
const resp = await fetch('https://api.scavio.dev/api/v1/search', {
method: 'POST',
headers: { 'x-api-key': SCAVIO_KEY, 'Content-Type': 'application/json' },
body: JSON.stringify({ query, country_code: 'us', num_results: 5 })
});
const data = await resp.json();
return (data.organic_results || []).map(r => ({ title: r.title, url: r.link, snippet: r.snippet || '' }));
} catch { return null; }
}
async function searchBrave(query) {
try {
const resp = await fetch(`https://api.search.brave.com/res/v1/web/search?q=${encodeURIComponent(query)}&count=5`, {
headers: { 'X-Subscription-Token': BRAVE_KEY, Accept: 'application/json' }
});
const data = await resp.json();
return (data.web?.results || []).map(r => ({ title: r.title, url: r.url, snippet: r.description || '' }));
} catch { return null; }
}
async function search(query) {
for (const [name, fn] of [['scavio', searchScavio], ['brave', searchBrave]]) {
const results = await fn(query);
if (results) { console.log(`Served by ${name}`); return results; }
}
return [];
}
search('AI frameworks 2026').then(r => r.slice(0, 3).forEach(x => console.log(x.title)));Expected Output
Provider: scavio ($0.005/query)
FastAPI 1.0: The Modern Python Web Framework
https://fastapi.tiangolo.com/release-notes/
Django 6.0 Features and Migration Guide
https://docs.djangoproject.com/en/6.0/
--- Served by: scavio at $0.005/query ---