Local AI gateway with full observability
Route all your LLM traffic through a single proxy with automatic tracing,
per-tool attribution, and session evaluation.
Quick Start · Features · Architecture · Providers · Maintenance
git clone https://github.com/aaronmorris-dev/litellm-langfuse-caddy.git
cd litellm-langfuse-caddy
cp .env.example .env
cp litellm/config.example.yaml litellm/config.yaml
# Generate secrets and edit .env:
openssl rand -hex 32 # → SALT, ENCRYPTION_KEY, NEXTAUTH_SECRET
openssl rand -hex 16 # → LITELLM_MASTER_KEY
# Uncomment your providers in litellm/config.yaml, then:
chmod +x scripts/*.sh
./scripts/start.sh| LiteLLM Proxy | localhost:4000 |
| LiteLLM Admin | localhost:4000/ui |
| Langfuse | localhost:5002 |
| MinIO Console | localhost:9091 |
After ./scripts/start.sh completes:
| 1 | Go to localhost:5002 — sign up, create an org and project |
| 2 | Settings → API Keys → Create. Copy pk-lf-... and sk-lf-... into .env |
| 3 | Run docker compose up -d to pick up the new keys |
| 4 | Go to localhost:4000/ui → Virtual Keys → create per-tool keys with descriptive aliases |
flowchart TD
CLI["🖥️ CLI Tools
Claude Code · Gemini CLI · OpenAI SDK"]
CLI -->|":4000 · :5002"| Caddy
subgraph compose[" Docker Compose Stack "]
Caddy["Caddy
Reverse Proxy · SSE"]
Caddy --> LiteLLM["LiteLLM
Unified LLM Proxy"]
Caddy --> Langfuse["Langfuse
Observability"]
LiteLLM -.->|"traces"| Langfuse
Worker["⚙️ Langfuse Worker"]
LiteLLM --> PG & Redis
Langfuse & Worker --> PG & CH & Redis & MinIO
PG[("PostgreSQL")]
CH[("ClickHouse")]
Redis[("Redis")]
MinIO[("MinIO")]
PG ~~~ CH ~~~ Redis ~~~ MinIO
end
LiteLLM --> Providers["Bedrock · Vertex AI · Gemini · OpenAI · Anthropic"]
- Multi-provider routing — Bedrock, Vertex AI, Gemini, OpenAI, and Anthropic behind one endpoint. Switch models without changing your code.
- Per-tool tracking — Give each tool its own virtual key. Every request is automatically tagged to its source in Langfuse.
- Automatic trace enrichment —
langfuse_enrich.pymaps virtual keys to trace names, daily sessions, and tags. No client changes needed. - Session evaluation — An LLM-as-judge script scores sessions on task completion, approach, and communication.
- Zero-buffered streaming — Caddy delivers tokens instantly with 10-minute response timeouts.
- Hardened by default — Memory limits, health checks, log rotation, and privilege lockdown on every service.
| Service | Image | What it does | Port |
|---|---|---|---|
| Caddy | caddy:2-alpine |
Routes traffic to LiteLLM and Langfuse with zero-buffered streaming | :4000 :5002 |
| LiteLLM | ghcr.io/berriai/litellm:main-v1.81.14-stable |
Sends requests to any provider through one API — routing, keys, and caching | — |
| Langfuse | langfuse/langfuse:3 |
Dashboard for traces, sessions, scores, and usage analytics | — |
| Langfuse Worker | langfuse/langfuse-worker:3 |
Processes incoming traces in the background | — |
| PostgreSQL | postgres:17-alpine |
Stores keys, spend logs, projects, and user data | — |
| ClickHouse | clickhouse/clickhouse-server:26.2 |
Fast analytics storage for traces and observations | — |
| Redis | redis:7.4-alpine |
Response cache and background job queue | — |
| MinIO | cgr.dev/chainguard/minio |
Object storage for event logs and media uploads | :9090 :9091 |
AWS Bedrock — Claude via AWS SSO
# 1. Configure SSO profile
aws configure sso --profile your-profile-name
# 2. Login
aws sso login --profile your-profile-nameAdd to .env:
AWS_PROFILE=your-profile-name
Uncomment in docker-compose.yaml under litellm volumes:
- ~/.aws:/root/.aws # writable — SSO needs token cacheUncomment models in litellm/config.yaml:
- model_name: claude-sonnet-4-6
litellm_params:
model: bedrock/us.anthropic.claude-sonnet-4-6Note: The
~/.awsmount must NOT be:ro— AWS SSO writes token cache files during credential refresh.
Google Vertex AI — Gemini via Application Default Credentials
gcloud auth application-default loginAdd to .env:
GOOGLE_APPLICATION_CREDENTIALS=/root/.config/gcloud/application_default_credentials.json
Uncomment in docker-compose.yaml under litellm volumes:
- ~/.config/gcloud:/root/.config/gcloud:roAdd models to litellm/config.yaml:
- model_name: gemini-2.5-flash
litellm_params:
model: vertex_ai/gemini-2.5-flash
vertex_project: your-gcp-project
vertex_location: us-central1Gemini API — Key-based (no GCP project needed)
Get a key from Google AI Studio, then add to .env:
GEMINI_API_KEY=your-key
Add to litellm/config.yaml:
- model_name: gemini-2.5-flash
litellm_params:
model: gemini/gemini-2.5-flashOpenAI / Anthropic — Direct API keys
Add the env var to the litellm service in docker-compose.yaml:
- OPENAI_API_KEY=${OPENAI_API_KEY}
# or
- ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY}Add to litellm/config.yaml:
- model_name: gpt-4o
litellm_params:
model: openai/gpt-4o
- model_name: claude-sonnet-4-6
litellm_params:
model: anthropic/claude-sonnet-4-6This repo includes a Claude Code skill at .claude/commands/llm-ops/gateway/. Open the project in Claude Code and run /llm-ops:gateway to diagnose, install, or configure the stack conversationally. The bundled diagnostic checks Docker, containers, API health, credentials, and environment variables in one pass.
source scripts/gateway-env.sh
# Tools that use the OpenAI or Anthropic SDK now route through LiteLLM:
# ANTHROPIC_BASE_URL=http://localhost:4000
# OPENAI_BASE_URL=http://localhost:4000/v1See examples/transparent-routing.md for permanent shell setup, per-tool virtual keys, and SDK examples. Tool-specific config examples are in examples/.claude/ and examples/.codex/.
flowchart LR
Key["Virtual Key alias · tags · user_id"] --> Enrich["langfuse_enrich.py
Runs on every request"]
Enrich --> Name["Trace Name
claude"]
Enrich --> Session["Daily Session
claude-2026-03-14"]
Enrich --> Tags["Tags
[claude]"]
Create virtual keys in LiteLLM Admin:
| What you set | Example | What shows in Langfuse |
|---|---|---|
| Key Alias | claude |
Trace name: claude |
| User ID | alice |
User: alice |
| Metadata tags | {"tags": ["claude"]} |
Tags: ["claude"] |
The enrichment hook automatically groups traces into daily sessions (e.g., claude-2026-03-14) — one per tool per day.
Prune old data
# Deletes: spend logs (30d), error logs (14d), traces (30d), scores (60d)
./scripts/prune-postgres.shThis cleans both PostgreSQL (LiteLLM spend/error logs) and ClickHouse (Langfuse traces/observations).
Evaluate a session
Uses LLM-as-judge to score sessions on 4 dimensions (0.0–1.0), then posts scores back to Langfuse.
# Requires: uv (https://docs.astral.sh/uv/)
uv run --script scripts/eval-session.py --today claude
uv run --script scripts/eval-session.py claude-2026-03-14
uv run --script scripts/eval-session.py claude-2026-03-14 --dry-run --verbose| Dimension | Weight | What it measures |
|---|---|---|
task_completion |
50% | Did the assistant complete the user's goals? |
approach_quality |
30% | Engineering soundness and efficiency |
communication |
20% | Clarity, concision, appropriateness |
overall |
— | Weighted composite of the above |
Check provider credentials
./scripts/refresh-credentials.shChecks AWS SSO session and GCloud ADC token, refreshing expired credentials interactively.
Stop / restart / logs
./scripts/start.sh # Start all services (with validation)
./scripts/stop.sh # Stop all services
docker compose logs -f # Follow all logs
docker compose ps # Check service statusEvery service is locked down by default:
| What | Where | Why |
|---|---|---|
| Memory limits | All 8 services | Each service capped (128 MB – 1.5 GB) to prevent runaway use |
| Privilege lockdown | All 8 services | Containers cannot escalate permissions |
| Read-only filesystem | Caddy | Root filesystem is immutable; temp storage for runtime data |
| Process management | LiteLLM, Langfuse, Worker, MinIO | Clean signal handling via tini |
| UTC everywhere | PostgreSQL, ClickHouse | All timestamps in UTC — no timezone surprises |
| Log rotation | All 8 services | 5–20 MB per log file, 3–5 files kept |
| Health checks | 6 of 8 services | Automatic probes with retries and startup grace periods |
| Graceful shutdown | LiteLLM (30s), Langfuse (15s), PG (30s), CH (15s) | Services finish in-flight work before stopping |
| Localhost only | Caddy, MinIO | Exposed ports bound to 127.0.0.1 — not reachable externally |
- OrbStack (recommended) or any Docker-compatible runtime — OrbStack uses less memory and starts faster, which helps with an 8-service stack
- ~4 GB RAM available for containers
- Credentials for at least one LLM provider
.
├── .claude/commands/llm-ops/gateway/ # Claude Code skill (diagnose, install, configure)
│ ├── SKILL.md # Skill definition and instructions
│ ├── scripts/diagnose.sh # Automated diagnostic cascade
│ └── references/topology.md # Service topology and known issues
├── docker-compose.yaml # 8-service stack definition
├── Caddyfile # Reverse proxy config (SSE, timeouts, logging)
├── .env.example # All required environment variables
├── litellm/
│ ├── config.example.yaml # LiteLLM model config template
│ └── langfuse_enrich.py # Trace enrichment hook (auto-loaded by LiteLLM)
├── examples/
│ ├── .claude/settings.json # Claude Code proxy config
│ ├── .codex/config.yaml # Codex CLI proxy config
│ └── transparent-routing.md # Guide: route any tool through the gateway
└── scripts/
├── start.sh # Startup with validation
├── stop.sh # Graceful shutdown
├── gateway-env.sh # Source to route tools through the proxy
├── refresh-credentials.sh # Check/refresh provider credentials
├── prune-postgres.sh # Data retention maintenance
├── prune-postgres.sql # PostgreSQL pruning policy
└── eval-session.py # LLM-as-judge session evaluation