Cloud-to-Local LLM Inference Gateway — Resource-aware queue system that routes cloud LLM requests to your local Mac, processes them through Ollama, and returns results. When your machine is busy, it returns 429 so the cloud app can fallback to cloud models.
Cloud App ──HTTPS──▶ Cloudflare Tunnel ──▶ LocalLMQ (:8000) ──▶ Ollama (:11434)
│
Queue + Resource Monitor
(RAM / Thermal / Queue depth)
Running LLM inference locally on Apple Silicon is free and private. But your Mac has limited resources — if 100 requests hit at once, the system will swap and freeze. LocalLMQ solves this by:
- Queuing requests and processing them one at a time (M1 GPU can't parallelize efficiently)
- Monitoring RAM, CPU, and thermal state in real-time
- Rejecting excess load with
HTTP 429+Retry-Afterheader so the cloud app falls back to cloud models - Exposing an OpenAI-compatible API — your cloud app only changes the base URL
- 🔄 OpenAI-compatible API — drop-in replacement (
/v1/chat/completions) - 📡 SSE Streaming — optional token-by-token streaming (
"stream": true) - 🧠 Resource-aware — monitors RAM, CPU, thermal state via
psutil+pmset - 🚦 Backpressure — configurable queue size, instant 429 on overflow
- 💾 Persistent queue — SQLite-backed job history survives restarts
- 🔒 API key auth — Bearer token authentication
- 📊 Monitoring dashboard — real-time web UI at
http://localhost:8000 - 🔁 Auto-restart — launchd integration for crash recovery
- macOS with Apple Silicon (M1/M2/M3/M4)
- Python 3.9+
- Ollama installed and running
git clone <repo-url> locallmq
cd locallmq
pip3 install -e .cp .env.example .env
# Edit .env — at minimum change the API key:
# LOCALLMQ_API_KEY=your-secure-key-here# Make sure Ollama is running
ollama serve
# Start LocalLMQ
./locallmq.sh startOpen http://localhost:8000 to see the monitoring dashboard.
All settings are configured via environment variables in .env:
| Variable | Default | Description |
|---|---|---|
LOCALLMQ_API_KEY |
change-me-to-a-secure-key |
Bearer token for API authentication |
LOCALLMQ_OLLAMA_URL |
http://localhost:11434 |
Ollama server URL |
LOCALLMQ_DEFAULT_MODEL |
qwen3:8b |
Default model when not specified in request |
LOCALLMQ_MAX_QUEUE_SIZE |
5 |
Max pending jobs. Beyond this → 429 |
LOCALLMQ_JOB_TIMEOUT |
300 |
Per-job timeout in seconds |
LOCALLMQ_MEMORY_PRESSURE_THRESHOLD |
80 |
Memory % above which new jobs are rejected |
LOCALLMQ_HOST |
0.0.0.0 |
Server bind address |
LOCALLMQ_PORT |
8000 |
Server port |
OpenAI-compatible endpoint. Your cloud app just changes the base URL.
curl -X POST http://localhost:8000/v1/chat/completions \
-H "Authorization: Bearer your-api-key" \
-H "Content-Type: application/json" \
-d '{
"model": "gemma4:latest",
"messages": [
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": "Hello!"}
],
"temperature": 0.7,
"max_tokens": 1024
}'Streaming (SSE):
curl -X POST http://localhost:8000/v1/chat/completions \
-H "Authorization: Bearer your-api-key" \
-H "Content-Type: application/json" \
-d '{
"model": "gemma4:latest",
"messages": [{"role": "user", "content": "Hello!"}],
"stream": true
}'No authentication required. Useful for load balancers and Cloudflare Tunnel health checks.
curl http://localhost:8000/v1/health{
"status": "healthy",
"ollama_connected": true,
"memory_percent": 61.7,
"memory_available_gb": 6.1,
"thermal_state": "nominal",
"cpu_percent": 15.2,
"queue_depth": 0,
"can_accept_work": true
}status values: healthy, degraded (high memory or thermal), unhealthy.
curl http://localhost:8000/v1/queue/status \
-H "Authorization: Bearer your-api-key"# Get job status
curl http://localhost:8000/v1/queue/{job_id} \
-H "Authorization: Bearer your-api-key"
# Cancel a pending job
curl -X DELETE http://localhost:8000/v1/queue/{job_id} \
-H "Authorization: Bearer your-api-key"# JSON feed (no auth required)
curl http://localhost:8000/api/dashboardYour cloud application just uses the OpenAI SDK with a custom base URL:
from openai import OpenAI
# Try local first, fallback to cloud
try:
client = OpenAI(
base_url="https://llm.yourdomain.com/v1", # LocalLMQ via tunnel
api_key="your-locallmq-key",
timeout=30,
)
response = client.chat.completions.create(
model="gemma4:latest",
messages=[{"role": "user", "content": "Hello!"}],
)
except Exception:
# Fallback to cloud (OpenAI, Anthropic, etc.)
client = OpenAI(api_key="sk-...")
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": "Hello!"}],
)429 handling (recommended):
import httpx
try:
response = client.chat.completions.create(...)
except openai.RateLimitError:
# LocalLMQ is at capacity — use cloud model
response = cloud_client.chat.completions.create(...)LocalLMQ uses launchd (macOS's native service manager) for process lifecycle:
./locallmq.sh install # Install as launchd service (auto-start on login)
./locallmq.sh start # Start
./locallmq.sh stop # Stop
./locallmq.sh restart # Restart
./locallmq.sh status # Health + process info
./locallmq.sh logs # Show recent logs
./locallmq.sh tail # Follow logs in real-time
./locallmq.sh uninstall # Remove the serviceWhat launchd provides:
- ✅ Auto-start on macOS login
- ✅ Auto-restart on crash (10s throttle)
- ✅ Lower CPU priority than UI apps (
Nice=5) - ✅ Logs at
~/.locallmq/logs/
Manual mode (without installing the service):
./locallmq.sh start # Runs in foreground if not installed
# or directly:
python3 -m uvicorn locallmq.main:app --host 0.0.0.0 --port 8000 --workers 1Cloudflare Tunnel exposes your local Mac to the internet securely — no port forwarding, no static IP needed.
brew install cloudflared
cloudflared tunnel --url http://localhost:8000Gives you a temporary https://xxxxx.trycloudflare.com URL. No signup, no config.
1. Login to Cloudflare (one-time):
cloudflared login
# Opens browser → select your domain → saves ~/.cloudflared/cert.pem2. Create the tunnel:
cloudflared tunnel create locallmq
# Output: Created tunnel locallmq with id xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx3. Configure routing:
Create ~/.cloudflared/config.yml:
tunnel: locallmq
credentials-file: /Users/YOUR_USER/.cloudflared/TUNNEL_ID.json
ingress:
- hostname: llm.yourdomain.com
service: http://localhost:8000
originRequest:
noTLSVerify: true
- service: http_status:4044. Add DNS record:
cloudflared tunnel route dns locallmq llm.yourdomain.com5. Run the tunnel:
# Foreground
cloudflared tunnel run locallmq
# Or install as a macOS service (auto-start)
cloudflared service install6. Test it:
curl https://llm.yourdomain.com/v1/healthNote: Cloudflare Tunnel is free. No API keys needed — it uses OAuth via browser for initial auth. Add your domain to Cloudflare DNS first.
Docker is available for Linux deployment or CI environments. On macOS, native execution is recommended because Docker can't access Apple Silicon GPU.
docker compose up -d
# Ollama runs containerized with GPU passthroughollama serve # Native Ollama with Apple Silicon GPU
docker compose -f docker-compose.yml -f docker-compose.mac.yml up locallmqpip3 install -e ".[dev]"
python3 -m pytest tests/ -v53 passed in 1.07s
All tests use a mocked Ollama backend — no running Ollama server needed.
| Suite | Tests | Coverage |
|---|---|---|
| Auth & Security | 6 | Public/protected endpoints, valid/invalid keys |
| Health & Dashboard | 5 | Schema validation, Ollama status, data structure |
| Chat Completions | 8 | Blocking, streaming, validation, defaults |
| Queue Management | 5 | Status, job lookup, cancel, history |
| Resource Monitor | 16 | Memory/thermal thresholds, lifecycle |
| Job Model & Store | 10 | CRUD, stats, ordering, cleanup |
| Backpressure | 3 | Memory 429, thermal 429, Retry-After |
┌─────────────────────────────────────────────────────┐
│ FastAPI Gateway │
│ │
│ /v1/chat/completions ─┐ │
│ /v1/health │ ┌──────────────────┐ │
│ /v1/queue/* ├───▶│ Queue Manager │ │
│ /api/dashboard │ │ (asyncio.Queue │ │
│ / │ │ + SQLite) │ │
│ │ └───────┬──────────┘ │
│ ┌─────────────┐ │ │ │
│ │ Auth Guard │───────┘ ▼ │
│ │ Bearer Token│ ┌──────────────────┐ │
│ └─────────────┘ │ Inference Worker │ │
│ │ (single, serial) │ │
│ ┌─────────────────┐ └───────┬──────────┘ │
│ │ Resource Monitor │ │ │
│ │ psutil + pmset │────gate────▶│ │
│ │ RAM/CPU/Thermal │ ▼ │
│ └─────────────────┘ ┌──────────────┐ │
│ │ Ollama │ │
│ │ :11434 │ │
│ └──────────────┘ │
└─────────────────────────────────────────────────────┘
Key design decisions:
- Single worker — Apple Silicon UMA can't efficiently parallelize GPU inference
- Queue size 5 — Fast 429 rejection for overflow → cloud fallback
- Memory threshold 80% — macOS idles at ~65–75% on 16GB with Ollama loaded
- asyncio.Queue + SQLite — Zero overhead runtime queue, persistent job history
- OpenAI API compatible — Cloud app only changes base URL to switch providers
MIT