A lightweight, zero-dependency failover proxy designed for Claude Code. Automatically switches between multiple API endpoints with concurrent streaming, health monitoring, and smart error handling. Also works with any OpenAI-compatible LLM API.
- Concurrent Request Handling: ThreadingHTTPServer processes multiple streaming requests in parallel
- Automatic Failover: Seamlessly switches between multiple API endpoints when one fails
- Smart Error Classification: 5xx/connection errors trip circuit breaker; 401/403/429 retry next; 400/404/422 forward directly
- Health & Status Endpoints:
/_proxy/healthand/_proxy/statusfor monitoring and alerting - Request Tracing: Unique request ID in every log line for end-to-end debugging
- Hot Reload:
SIGHUPto reload config without restarting;SIGTERMfor graceful shutdown - Streaming Support: SSE streaming with chunked transfer encoding for real-time LLM output
- Circuit Breaker: Skips failing endpoints after consecutive failures, with configurable threshold and cooldown
- Model Name Mapping: Transparently maps model names for APIs with different naming conventions
- Multi-Protocol Auth: Supports both Anthropic (
x-api-key) and OpenAI (Authorization: Bearer) styles - Per-Endpoint Timeout: Override global timeout for slow or fast endpoints
- Config Validation:
--validatemode to check config without starting the server - Environment Overrides:
PROXY_*env vars override config file values - Zero Dependencies: Uses only Python standard library
- Request Size Limit: Rejects oversized requests with HTTP 413 (default 50MB)
- Using multiple third-party LLM API providers (e.g., Claude API resellers)
- Need automatic failover when one provider experiences outages
- Running Claude Code on a VPS with concurrent streaming requests
- Different providers use different model naming conventions
- Want operational visibility with health checks and per-endpoint stats
- Python 3.7+
- Linux/macOS (signal handling requires Unix; Windows supported without SIGHUP)
-
Clone the repository
git clone https://github.com/JAMESHPF/claude-code-failover-proxy.git cd claude-code-failover-proxy -
Create configuration file
cp config.example.json ~/.llm-proxy-config.json vim ~/.llm-proxy-config.json
-
Create environment file
cp .env.example ~/.llm-proxy.env vim ~/.llm-proxy.env chmod 600 ~/.llm-proxy.env
-
Validate and start
# Validate config first python3 llm-api-proxy.py --validate -c ~/.llm-proxy-config.json # Start the proxy python3 llm-api-proxy.py
-
Verify it's running
curl http://127.0.0.1:5000/_proxy/health # {"status": "ok", "version": "3.0.0", "uptime_seconds": 5} -
Install as systemd service (optional)
sudo cp llm-api-proxy.service /etc/systemd/system/ sudo vim /etc/systemd/system/llm-api-proxy.service sudo systemctl daemon-reload sudo systemctl enable --now llm-api-proxy
usage: llm-api-proxy.py [-h] [-c CONFIG] [-p PORT] [--host HOST] [-e ENV]
[--log-level {DEBUG,INFO,WARNING,ERROR}]
[--validate] [-v] [--init]
options:
-c, --config CONFIG Path to configuration file
-p, --port PORT Override proxy port
--host HOST Override proxy host
-e, --env ENV Path to .env file
--log-level LEVEL Set log level (DEBUG, INFO, WARNING, ERROR)
--validate Validate config and exit (exit 0 = valid)
-v, --version Show version
--init Create default config in current directory
{
"proxy": {
"host": "127.0.0.1",
"port": 5000,
"timeout": 15,
"circuit_breaker_threshold": 3,
"circuit_breaker_cooldown": 60,
"max_body_size": 52428800
},
"endpoints": [
{
"name": "Primary API",
"base_url": "https://api.example.com",
"api_key_env": "PRIMARY_API_KEY",
"timeout": 10
},
{
"name": "Backup API (slower)",
"base_url": "https://backup-api.example.com",
"api_key_env": "BACKUP_API_KEY",
"timeout": 30,
"model_mapping": {
"claude-opus-4-6": "claude-opus-4-6-thinking"
}
},
{
"name": "OpenAI Compatible",
"base_url": "https://api.openai.com",
"api_key_env": "OPENAI_API_KEY",
"auth_type": "openai"
}
]
}| Field | Default | Description |
|---|---|---|
host |
127.0.0.1 |
Bind address |
port |
5000 |
Bind port |
timeout |
15 |
Global request timeout (seconds) |
circuit_breaker_threshold |
3 |
Failures before skipping endpoint |
circuit_breaker_cooldown |
60 |
Seconds before retrying skipped endpoint |
max_body_size |
52428800 |
Max request body size in bytes (50MB) |
| Field | Required | Description |
|---|---|---|
name |
Yes | Display name for logs |
base_url |
Yes | Upstream API base URL |
api_key_env |
No* | Environment variable name containing API key |
api_key |
No* | Inline API key (not recommended) |
auth_type |
No | "anthropic" (default) or "openai" |
model_mapping |
No | Model name translation map |
timeout |
No | Per-endpoint timeout (overrides global) |
*One of api_key_env or api_key should be provided.
Environment variables override config file values (priority: CLI > env var > config):
| Env Var | Overrides |
|---|---|
PROXY_TIMEOUT |
proxy.timeout |
PROXY_CB_THRESHOLD |
proxy.circuit_breaker_threshold |
PROXY_CB_COOLDOWN |
proxy.circuit_breaker_cooldown |
PROXY_MAX_BODY_SIZE |
proxy.max_body_size |
PROXY_LOG_LEVEL |
Log level (DEBUG/INFO/WARNING/ERROR) |
PRIMARY_API_KEY=sk-your-primary-key
BACKUP_API_KEY=sk-your-backup-key
OPENAI_API_KEY=sk-your-openai-keyQuoted values are supported: KEY="value" and KEY='value' are automatically unquoted.
Returns proxy health status. Use for uptime monitoring and load balancer health checks.
curl http://127.0.0.1:5000/_proxy/health{"status": "ok", "version": "3.0.0", "uptime_seconds": 12345}Returns detailed operational statistics per endpoint.
curl -s http://127.0.0.1:5000/_proxy/status | python3 -m json.tool{
"uptime_seconds": 12345,
"total_requests": 567,
"endpoints": [
{
"name": "Primary API",
"circuit_state": "closed",
"failures": 0,
"stats": {
"success": 500,
"fail_5xx": 2,
"fail_4xx": 0,
"fail_conn": 5
},
"last_success": "2026-03-03T10:00:00",
"last_failure": "2026-03-03T09:55:00"
}
]
}The proxy classifies upstream HTTP errors and handles them differently:
| Status Code | Action | Trips Circuit Breaker |
|---|---|---|
| 5xx | Retry next endpoint | Yes |
| Connection error / timeout | Retry next endpoint | Yes |
| 401, 403, 429 | Retry next endpoint | No (client/auth issue) |
| 400, 404, 422 | Forward to client immediately | No |
| Signal | Action |
|---|---|
SIGHUP |
Reload config from disk (validates before applying) |
SIGTERM |
Graceful shutdown (waits for in-flight requests) |
SIGINT / Ctrl-C |
Immediate shutdown |
# Reload config after editing
kill -HUP $(pgrep -f llm-api-proxy)
# Graceful shutdown
kill $(pgrep -f llm-api-proxy)For systemd services:
sudo systemctl reload llm-api-proxy # SIGHUP
sudo systemctl stop llm-api-proxy # SIGTERMSome API providers use different model naming conventions. The proxy can automatically map model names:
{
"name": "Custom API",
"base_url": "https://custom-api.com",
"api_key_env": "CUSTOM_API_KEY",
"model_mapping": {
"claude-opus-4-6": "claude-opus-4-6-thinking",
"claude-sonnet-4-6": "claude-sonnet-4-6-thinking"
}
}When your client requests claude-opus-4-6, the proxy automatically sends claude-opus-4-6-thinking to this endpoint.
auth_type |
Header Sent | Default |
|---|---|---|
"anthropic" |
x-api-key + anthropic-version |
Yes |
"openai" |
Authorization: Bearer <key> |
No |
Configure Claude Code to use the proxy:
// ~/.claude/settings.json
{
"env": {
"ANTHROPIC_BASE_URL": "http://127.0.0.1:5000",
"ANTHROPIC_AUTH_TOKEN": "PROXY_MANAGED"
}
}# Health check
curl http://127.0.0.1:5000/_proxy/health
# Operational status
curl http://127.0.0.1:5000/_proxy/status
# Test POST request
curl -X POST http://127.0.0.1:5000/v1/messages \
-H 'Content-Type: application/json' \
-H 'anthropic-version: 2023-06-01' \
-d '{"model":"claude-opus-4-6","max_tokens":20,"messages":[{"role":"user","content":"test"}]}'
# Test GET request
curl http://127.0.0.1:5000/v1/modelsEach request gets a unique 8-character ID for tracing:
[a1b2c3d4] POST /v1/messages → Primary API | 200 | 3.21s | streaming | attempts=1
When all endpoints fail, a detailed summary is logged:
[a1b2c3d4] All endpoints exhausted (3 attempted):
1. Primary API: connection timeout
2. Backup API: circuit breaker open
3. OpenAI: HTTP 429
Set log level via CLI (--log-level DEBUG) or env var (PROXY_LOG_LEVEL=DEBUG).
Port already in use
- Change the
portin config or use-pflag
All endpoints fail
- Check
/_proxy/statusfor per-endpoint error counts - Check API keys in
.envfile - Verify endpoint URLs are correct
Service won't start
- Validate config:
python3 llm-api-proxy.py --validate -c config.json - Check logs:
journalctl -u llm-api-proxy -n 50
Config changes not taking effect
- Send SIGHUP:
sudo systemctl reload llm-api-proxy - Check logs for "Config reloaded via SIGHUP" or reload errors
- API keys are stored in environment file with 600 permissions
- Config file is separate from code (easier to version control without secrets)
- Proxy binds to 127.0.0.1 only (not accessible from network)
- Request body size limited to prevent memory exhaustion (default 50MB)
- Never commit
.llm-proxy.envor.llm-proxy-config.jsonto version control
MIT License - see LICENSE file for details
Contributions are welcome! Please feel free to submit a Pull Request.