OpenAI-compatible router for keyless free LLM providers — automatic failover, API key management, and IP rotation via Cloudflare WARP. No upstream API keys required.
- Overview
- Features
- Providers
- Model Aliases
- Installation
- Quick Start
- API Reference
- Configuration
- providers.json + models.json
- Architecture
- Routing Modes
- Testing
- Deployment
- Contributing
SparroW aggregates multiple free LLM providers behind a single OpenAI-compatible API. Point any OpenAI SDK or client at SparroW and get automatic failover across 7 providers configured in providers.json and models.json — no API keys to the upstream providers required.
Key Philosophy:
- ✅ Zero upstream keys — all providers are free and keyless
- ✅ OpenAI-compatible — drop-in replacement for any OpenAI SDK or client
- ✅ Automatic failover: if one provider fails, the next is tried within the request contract
- ✅ Streaming support: SSE streaming with failover before the first event
- ✅ Docker-ready — app container with an optional external WARP proxy
- ✅ Lightweight — pure Python, no heavy dependencies
- ✅ Chat Completions —
/v1/chat/completionswith streaming and non-streaming - ✅ Embeddings —
/v1/embeddingsendpoint - ✅ Model Listing —
/v1/modelsreturns all available models - ✅ Provider Listing —
/v1/providerswith health status - ✅ Health Check:
/healthzliveness and/readyzlocal process, configuration, route, and WARP readiness
- ✅ Automatic Failover: tries the next eligible route on retryable upstream failures
- ✅ Model Aliases — request
gpt-4o, get routed to the best free equivalent - ✅ Routing Modes —
fair,fast,quality, andmodelselection - ✅ Health Tracking — circuit breaker prevents repeated calls to failing providers
- ✅ Daily Quotas — per-provider daily request limits
- ✅ API Key Auth —
.env-backed static API keys, accepted throughAuthorization: BearerorX-API-Key
- ✅ WARP Proxy — Cloudflare WARP integration for IP rotation
- ✅ Request Statistics — track provider usage, latency, success rates
- ✅ Dashboard — built-in HTML dashboard with live stats
| Provider | Quality Range | Notes |
|---|---|---|
| GPT.chat | 5 | |
| OpenCode Zen | 5 | |
| BlockRun | 5 | |
| Kilo Gateway | 5 | |
| OVH Cloud | 5 | |
| Codex.chat | 5 | |
| LLM7.io | 5 |
Request well-known model names and SparroW routes them to the best free equivalent:
| Alias | Routes to |
|---|---|
gpt-4o |
Kilo / Nemotron 3 Super 120B |
gpt-4o-mini |
Kilo / OpenRouter Free |
claude-3.5-sonnet |
Kilo / Nemotron 3 Ultra 550B |
claude-3-haiku |
OpenCode / MiMo V2.5 |
mistral-small |
OVHcloud / Mistral Small 3.2 |
auto |
Round-robin across all providers |
git clone https://github.com/hallaxius/sparrow.git
cd sparrow
cp .env.example .env
docker compose up -d --buildThe Sparrow Compose file starts only the application. Run the independent WARP service from the sparrow-warp repository when IP rotation is required.
uv sync
cp .env.example .env
# Set SPARROW_API_KEY in .env before starting the server.
uv run python -m sparrowRequirements:
- Python >= 3.12
- uv (package manager)
- Docker + Docker Compose
cp .env.example .env
# Set SPARROW_API_KEY to a long random secret.Sparrow reads the versioned providers.json and models.json catalogs from the repository root. Catalog discovery and reconciliation are always explicit. Use sparrow catalog check to fetch and inspect live model changes without writing files, then use sparrow catalog reconcile to apply them. The server and entrypoint never fetch /models.
uv run python -c "from sparrow.config.loader import load_all_providers; print(len(load_all_providers()['providers']))"The WARP service is maintained in the separate sparrow-warp repository. Start it independently when the router must use WARP:
cd ../sparrow-warp
docker compose up -d --buildThe app-only Compose configuration points to socks5://host.docker.internal:1080 by default. Keep SPARROW_WARP_REQUIRED=false for direct fallback, or set it to true after the WARP service is ready.
For a WARP service reached through a public TCP proxy, use the generated host and port with WireProxy credentials:
SPARROW_WARP_REQUIRED=true
SPARROW_WARP_URL=socks5://USERNAME:PASSWORD@proxy-host:proxy-port
cd ../sparrow
docker compose up -d --buildcurl -X POST http://localhost:8080/v1/chat/completions \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR-KEY" \
-d '{
"model": "auto",
"messages": [{"role": "user", "content": "Hello!"}]
}'curl -i http://localhost:8080/healthz
curl -i http://localhost:8080/readyz/healthz is a public local liveness endpoint. It reports that the process responds, not that any upstream model can serve traffic. /readyz is a public local readiness endpoint for startup, configuration, eligible routes, and required WARP. It returns 503 until those local conditions are ready, and 200 when they are. Neither endpoint validates upstream inference for every model.
All endpoints follow the OpenAI API format.
| Method | Endpoint | Description |
|---|---|---|
| POST | /v1/chat/completions |
Chat completions (streaming + non-streaming) |
Request:
{
"model": "auto",
"messages": [{"role": "user", "content": "Hello!"}],
"stream": false
}Streaming:
curl -X POST http://localhost:8080/v1/chat/completions \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR-KEY" \
-d '{
"model": "gpt-4o",
"messages": [{"role": "user", "content": "Write a haiku"}],
"stream": true
}'| Method | Endpoint | Description |
|---|---|---|
| POST | /v1/embeddings |
Create embeddings |
{
"model": "auto",
"input": "The quick brown fox"
}| Method | Endpoint | Description |
|---|---|---|
| GET | /v1/models |
List all available models |
| Method | Endpoint | Description |
|---|---|---|
| GET | /v1/providers |
List all providers with health status |
API keys are configured via the required SPARROW_API_KEY environment variable (see Configuration). Send the key in the Authorization: Bearer YOUR-KEY header. X-API-Key: YOUR-KEY is supported for compatibility. API keys in JSON request bodies are not accepted.
/, /healthz, and /readyz are public. Chat, embeddings, model/provider inventory, statistics, and metrics endpoints require authentication. The dashboard shell is public, but its data requests send the configured authorization header.
| Method | Endpoint | Description |
|---|---|---|
| GET | /healthz |
Local process liveness check (no auth required) |
| GET | /readyz |
Local startup, configuration, route, and WARP readiness check (no auth required) |
| GET | /stats |
Request statistics (auth required) |
| GET | /metrics |
Prometheus metrics (auth required) |
Every response includes provider metadata:
| Header | Description |
|---|---|
X-Sparrow-Provider |
Provider that served the request |
X-Sparrow-Model |
Model that was used |
| Variable | Default | Description |
|---|---|---|
SPARROW_HOST |
0.0.0.0 |
Bind address |
PORT |
(platform-provided) | Fallback listen port when SPARROW_PORT is not set |
SPARROW_PORT |
8080 |
Listen port; takes precedence over PORT |
SPARROW_ROUTING |
fair |
Routing mode (fair, fast, quality, model) |
SPARROW_API_KEY |
(required) | Single API key accepted through Authorization: Bearer or X-API-Key |
SPARROW_WARP_URL |
socks5://warp:1080 |
WARP SOCKS5 proxy URL; supports socks5://USER:PASSWORD@HOST:PORT |
SPARROW_WARP_REQUIRED |
false |
Require an available WARP proxy before readiness and provider requests |
SPARROW_WARP_HTTP_URL |
(empty) | Optional HTTP proxy URL for WARP health traffic |
SPARROW_WARP_HEALTH_CHECK_URL |
https://cloudflare.com/cdn-cgi/trace |
WARP health endpoint |
WARP_HEALTH_INTERVAL |
60 |
WARP health check interval (seconds) |
WARP_CONNECT_TIMEOUT |
10 |
WARP connection timeout (seconds) |
WARP_READ_TIMEOUT |
120 |
WARP read timeout (seconds) |
WARP_MAX_CONNECTIONS |
100 |
Maximum WARP connections |
WARP_MAX_KEEPALIVE |
20 |
Maximum WARP keepalive connections |
SPARROW_WARP_STARTUP_TIMEOUT |
90 |
Maximum time to wait for required WARP during startup (seconds) |
SPARROW_WARP_STARTUP_RETRY_INTERVAL |
2 |
Retry interval while waiting for required WARP (seconds) |
SPARROW_REQUEST_DEADLINE |
120 |
One absolute deadline for the entire request, including waits, retries, failover, and streaming (seconds) |
SPARROW_MAX_REQUEST_ATTEMPTS |
4 |
Maximum provider attempts per request |
SPARROW_MAX_ROUTE_ATTEMPTS |
2 |
Maximum attempts for one route |
Boolean settings accept true, false, 1, 0, yes, and no. |
Deploy Sparrow and WARP as separate services. When they share a Railway project and environment, use Railway private networking and the WARP private domain, for example warp.railway.internal when the service is actually named warp. When they are in different projects or platforms, expose only the WARP SOCKS5 listener through a Railway TCP Proxy and use its generated public host and port.
The Sparrow service must listen on Railway's PORT value. Leave SPARROW_PORT unset in Railway unless an explicit override is required. Set SPARROW_WARP_REQUIRED=true and set SPARROW_WARP_URL to either socks5://<warp-private-domain>:1080 or socks5://USERNAME:PASSWORD@<tcp-proxy-host>:<tcp-proxy-port>. The WARP service must listen on 0.0.0.0:1080 for SOCKS5 and use a separate PORT for its HTTP health interface.
The Compose depends_on health condition applies only to local Docker Compose. Railway services start independently, so Sparrow uses the bounded WARP startup wait and /readyz remains 503 until required WARP is available. /healthz remains a liveness check and does not prove provider connectivity. Railway TCP Proxy does not authenticate SOCKS5 traffic, so public WARP deployments must enable WireProxy username/password authentication.
Providers and models are configured in two versioned JSON catalogs. providers.json contains provider metadata and aliases, while models.json contains model definitions grouped by provider UUID. Sparrow reads these files as the runtime provider and model source. Catalog reconciliation is explicit and never runs during startup or through the entrypoint. Use sparrow catalog check for a read only comparison with live provider catalogs, and sparrow catalog reconcile to validate and write the updated catalogs.
{
"providers": {
"my-provider-uuid": {
"name": "My Provider",
"base_url": "https://api.example.com/v1",
"adapter": "openai",
"auth": "none"
}
},
"aliases": {
"gpt-4o": "my-provider-uuid/model-id"
}
}{
"my-provider-uuid": [
{
"id": "model-id",
"name": "Model Name",
"context": 128000,
"quality": 5,
"enabled": true
}
]
}| Field | Type | Description |
|---|---|---|
id |
string | Model identifier (used in API requests) |
name |
string | Display name |
context |
int | Context window size (tokens) |
quality |
int | Quality score 1–10 (higher = better) |
enabled |
bool | Whether the model is active |
daily_quota is an optional provider-level daily request limit. It is enforced atomically for every dispatched upstream attempt; None means unlimited.
Aliases are defined in providers.json under the "aliases" key. The format is "alias_name": "provider_uuid/model_id".
sparrow/
├── sparrow/
│ ├── app.py # Starlette application, endpoint handlers
│ ├── client.py # Async HTTP client with WARP support
│ ├── proxy.py # Cloudflare WARP SOCKS5 proxy manager
│ ├── stats.py # Request statistics tracker
│ ├── dashboard.py # HTML dashboard UI
│ ├── errors.py # Exception hierarchy
│ ├── adapters/
│ │ ├── base.py # ProviderAdapter protocol
│ │ ├── openai_compat.py # OpenAI-compatible adapter implementation
│ │ └── registry.py # Adapter registry (provider_id → adapter)
│ ├── config/
│ │ ├── loader.py # JSON config loader
│ │ ├── aliases.py # Model alias resolver
│ │ └── models.py # Pydantic settings model
│ ├── middleware/
│ │ ├── auth.py # API key auth middleware
│ │ ├── logging.py # Request logging
│ │ └── body_limit.py # Body size limiter
│ ├── models/
│ │ ├── chat.py # Chat completion request/response models
│ │ ├── embedding.py # Embedding request/response models
│ │ ├── provider.py # Provider/model info models
│ │ └── config.py # Provider config models
│ └── routing/
│ ├── engine.py # Routing engine (fair/fast/quality/model modes)
│ ├── health.py # Circuit breaker + health tracking
│ ├── modes.py # Routing strategy functions
│ └── quota.py # Daily quota tracker
├── tests/ # Tests (pytest + pytest-asyncio)
├── entrypoint.sh # Explicit JSON/API-key startup checks
├── providers.json # Provider metadata + aliases configuration
├── models.json # Model definitions per provider
├── docker-compose.yml # Docker Compose for the Sparrow app
├── Dockerfile # Python 3.12-slim + uv
├── pyproject.toml # Project metadata + dev tools
└── .gitignore
Client → AuthMiddleware → chat_completions()
→ Validate request and reject malformed input with safe 400
→ AliasResolver.resolve(model)
→ RoutingEngine.ordered_candidates(model)
→ For each candidate attempt within the global deadline:
→ QuotaTracker.try_acquire() and CircuitBreaker
→ AdapterRegistry.get(provider_id)
→ adapter.chat_completion() / chat_completion_stream()
→ On success: return response
→ On retryable failure: bounded retry or next route
→ On non-retryable upstream failure: next route
→ If the absolute deadline expires: return 504
→ If all candidates fail before the deadline: return 503
| Mode | Behavior |
|---|---|
fair |
Round-robin across eligible routes for the requested model |
fast |
Pick the route with lowest average latency |
quality |
Pick the route with highest quality score |
model |
Preserve the configured candidate order for an explicit model |
The request model auto means all eligible models. The request model fair is an ordinary model identifier; it is not an alias for auto.
Each request has one absolute deadline, at most four dispatched attempts, and at most two attempts on one route. HTTP 408, 429, 5xx, transport errors, and timeouts are retryable; Retry-After is honored only within the remaining deadline. Other upstream 4xx responses advance to the next route without repeating the same route. Local validation errors are never retried. Deadline exhaustion returns 504; other provider exhaustion returns 503.
Streaming may retry or fail over only before the first SSE event. After the first event, the active route is retained. An upstream failure emits exactly one upstream_error SSE event, closes the stream, emits no later [DONE], and never switches providers. The HTTP status remains 200 after streaming starts. A client disconnect or cancellation closes the upstream stream without an SSE error or [DONE], and is neutral: it does not record a provider failure or request outcome.
Daily quota acquisition, request statistics, and breaker state are updated for every dispatched attempt. Circuit breakers allow exactly one half-open probe after recovery.
uv run pytest tests/ -vuv run ruff check sparrow/ tests/
uv run ruff format sparrow/ tests/uv run mypy sparrow/uv run ruff check sparrow/ tests/
uv run mypy sparrow/
uv run pytest tests/ -v
uv run python -m compileall -q sparrow testscp .env.example .env
# Set SPARROW_API_KEY before starting Compose.
docker compose config --quiet
docker compose up -d --buildThis starts only SparroW. The WARP proxy is a separate service from the sparrow-warp repository. Start it independently and keep SPARROW_WARP_URL pointed at its SOCKS5 endpoint. The app-only Compose setup uses host.docker.internal:1080; a Railway deployment must use the WARP service private domain instead.
cp .env.example .env
# Set SPARROW_API_KEY before starting.
uv run python -m sparrowLocal mode uses direct HTTP when WARP is unreachable. When a configured SOCKS5/SOCKS5H proxy is available, WARP is used automatically. Set SPARROW_WARP_REQUIRED=true to make readiness and provider requests require the external proxy.
curl http://localhost:8080/healthz
curl -i http://localhost:8080/readyz/healthz is local process liveness and does not require authentication. /readyz is local startup, configuration, route, and required WARP readiness. Neither endpoint proves that every upstream model can serve inference traffic.
See CONTRIBUTING.md for guidelines.
MIT