A production-grade, full-stack LLM observability platform with multi-provider chatbot, real-time inference logging, event-driven ingestion, and monitoring dashboards β all runnable with a single command.
- Multi-turn Chatbot UI β Streaming chat with OpenAI, Anthropic, Gemini, and OpenRouter
- Lightweight SDK β Async
LLMLoggerwrapper capturing latency, TTFB, tokens, and errors - Event-Driven Ingestion β Kafka (Redpanda) pipeline with dead letter queue and retry logic
- PII Redaction β Microsoft Presidio anonymizes sensitive data before storage
- Observability Dashboards β Pre-built Grafana panels for latency, throughput, errors
- Conversation Management β List, cancel, and resume conversations from the UI
- Docker Compose β One command brings up all 8 services
This project is completely containerized. You do not need to install Node or Python on your host machineβonly Docker.
- Docker & Docker Compose v2+ installed on your machine.
- An API key for at least one provider (OpenAI, Anthropic, Google, or OpenRouter).
Clone the repository and set up your environment variables:
git clone https://github.com/your-username/llm-inference-platform
cd llm-inference-platform
cp .env.example .envOpen the .env file in your favorite editor and paste your API keys. It should look like this:
# Provide at least one of these:
OPENAI_API_KEY=sk-proj-...
ANTHROPIC_API_KEY=sk-ant-...
GOOGLE_API_KEY=AIza...
OPENROUTER_API_KEY=sk-or-v1-...
# Optional: Disable PII redaction to speed up the system slightly
PII_REDACTION_ENABLED=trueRun the following command to build and start all 8 distributed services:
docker compose up --build -dNote: The very first build will take about ~3-5 minutes as it downloads the Python images, Next.js dependencies, and the ~500MB SpaCy NLP model used for PII redaction. Subsequent boots will be instant.
Check that all containers are healthy:
docker compose psYou should see frontend, chat-api, ingestion-worker, postgres, redpanda, prometheus, and grafana in the Up or Healthy state.
-
Test the Chatbot (Frontend):
- Open http://localhost:3000 in your browser.
- Select a provider from the top dropdown (e.g., OpenRouter or OpenAI).
- Type a prompt like: "Write a Python script to reverse a string."
- Notice how the response streams in real-time, just like ChatGPT.
- Look at the sidebar: Your chat title dynamically updates to match your prompt!
-
Test Conversation Management:
- Click "New Chat" and start a second conversation.
- Click back to your first conversation in the sidebar. Notice how it instantly resumes with full context fetched from the database.
-
Test the Observability Pipeline (Grafana):
- Open http://localhost:3001 in a new tab.
- Login with
admin/admin. - Open the LLM Platform Main Dashboard.
- You will see the total requests, P95 latency, and token throughput for the messages you just sent! (The backend SDK asynchronously pushed these to Kafka, which were ingested and exported to Prometheus).
-
Test the API directly (Optional):
- View the interactive Swagger docs at http://localhost:8000/docs.
- You can programmatically stream a response via curl:
curl -N -X POST http://localhost:8000/chat/stream \ -H "Content-Type: application/json" \ -d '{"message": "Hello!", "provider": "openai", "model": "gpt-4o"}'
π Note for Reviewers: For a comprehensive deep-dive into the ingestion lifecycle, scaling strategies, bottlenecks, and failure handling assumptions, please read the full ARCHITECTURE.md document.
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Frontend (Next.js 14) β
β Chat UI Β· Conversations Β· Grafana Dashboard β
βββββββββββββββββββββββββ¬ββββββββββββββββββββββββββββββββββββββ
β HTTP / SSE Streaming
βββββββββββββββββββββββββΌββββββββββββββββββββββββββββββββββββββ
β Chat API (FastAPI :8000) β
β LLM Providers Β· LLMLogger SDK Β· PII Redaction Β· Metrics β
ββββββββββββ¬βββββββββββββββββββββββββββββ¬ββββββββββββββββββββββ
β Kafka Produce β Async DB Writes
βΌ βΌ
ββββββββββββββββββββ βββββββββββββββββββββββββββββββββββββββ
β Redpanda/Kafka β β PostgreSQL 16 β
β topic: llm-logs β β sessions Β· messages Β· logs Β· dlq β
ββββββββββββ¬ββββββββ βββββββββββββββββββββββββββββββββββββββ
β Consume β²
ββββββββββββΌβββββββββββββββββββββββββββ΄ββββββββββββββββββββββββ
β Ingestion Worker (Python) β
β Kafka Consumer Β· Pydantic Validation Β· Retry Β· DLQ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Prometheus /metrics
ββββββββββββΌβββββββββββββββββββββββββββββββββββββββββββββββββββ
β Prometheus + Grafana β
β Latency P50/P95/P99 Β· Error Rate Β· Throughput Β· Tokens β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
- User sends a message via the chat UI
- Chat API calls the selected LLM provider (streaming SSE)
LLMLoggerSDK wraps the call, capturing:request_ts,ttfb_ms,total_latency_ms,input_tokens,output_tokens,status- PII Redactor (Presidio) strips emails, phones, SSNs from previews
- SDK fire-and-forgets a Kafka produce to topic
llm-logsβ never blocking the stream - Ingestion Worker consumes the event, validates with Pydantic, upserts into PostgreSQL
- On validation failure β Dead Letter Queue (
dead_letter_logstable) - Prometheus metrics emitted; Grafana panels auto-update
Four tables with intentional tradeoffs:
| Table | Purpose | Key Decision |
|---|---|---|
sessions |
Conversation lifecycle | status field enables soft cancel/resume |
messages |
Full chat history | Dual content/content_redacted fields for PII-safe analytics |
inference_logs |
One row per LLM API call | raw_metadata JSONB for forward-compat with new provider fields |
dead_letter_logs |
Failed ingestion events | Kafka coordinates stored for manual replay |
Why PostgreSQL over ClickHouse?
- Simpler ops for MVP with a single Docker container
JSONBhandles flexible provider metadata without migrations- At scale (>1M logs/day), migrate
inference_logsto ClickHouse for time-series queries
Why content_redacted separate from content?
- Analytics and debugging can use the PII-free version safely
- Full content retained for compliance audits (toggle with
STORE_RAW_CONTENT=false)
-- sessions: one row per conversation
sessions (id UUID PK, status TEXT, provider TEXT, model TEXT, message_count INT, title TEXT, metadata JSONB, created_at, updated_at)
-- messages: ordered chat turns
messages (id UUID PK, session_id FK, role TEXT, content TEXT, content_redacted TEXT, sequence_num INT, token_count INT, created_at)
-- inference_logs: one row per LLM API call
inference_logs (id UUID PK, session_id FK, message_id FK, provider TEXT, model TEXT, request_ts, response_ts, total_latency_ms INT, ttfb_ms INT, input_tokens INT, output_tokens INT, total_tokens INT, status TEXT, error_type TEXT, http_status INT, input_preview TEXT, output_preview TEXT, raw_metadata JSONB, created_at)
-- dead_letter_logs: failed events for replay
dead_letter_logs (id UUID PK, raw_payload JSONB, error_reason TEXT, created_at)All settings via environment variables (see .env.example):
| Variable | Default | Description |
|---|---|---|
OPENAI_API_KEY |
β | OpenAI API key |
ANTHROPIC_API_KEY |
β | Anthropic API key |
GOOGLE_API_KEY |
β | Google Gemini API key |
DEFAULT_PROVIDER |
openai |
Default LLM provider |
DEFAULT_MODEL |
gpt-4.1 |
Default model |
DATABASE_URL |
postgres://... | Async PostgreSQL URL |
KAFKA_BOOTSTRAP_SERVERS |
redpanda:9092 |
Kafka broker |
KAFKA_TOPIC |
llm-logs |
Inference log topic |
PII_REDACTION_ENABLED |
true |
Enable Presidio redaction |
STORE_RAW_CONTENT |
true |
Store unredacted content |
OPENROUTER_API_KEY |
β | OpenRouter API key |
The LLMProvider abstract base class normalizes the interface across providers:
provider = get_provider("openai") # or "anthropic" / "google" / "openrouter"
async for token in provider.stream(messages, model="gpt-4.1"):
yield tokenSupported models:
| Provider | Models |
|---|---|
| OpenAI | gpt-4.1, gpt-4o, gpt-3.5-turbo |
| Anthropic | claude-sonnet-4-5, claude-haiku-3-5 |
| gemini-1.5-pro, gemini-1.5-flash | |
| OpenRouter | llama-3-8b-instruct, gemini-flash-1.5, claude-3-haiku, gpt-4o-mini |
Using Microsoft Presidio:
- Entities detected: EMAIL, PHONE, CREDIT_CARD, SSN, PERSON, IP_ADDRESS, LOCATION, DATE
- Applied to
input_previewandoutput_previewin inference logs (first 200 chars) - Applied to
content_redactedin messages table - Configurable: set
PII_REDACTION_ENABLED=falseto disable
The LLMLogger SDK is designed for zero-overhead observability:
- Async fire-and-forget: Kafka produce never blocks the response path
- Lazy initialization: Kafka producer created once via class-level singleton
- Graceful fallback: If Kafka is unavailable, falls back to stderr logging
- TTFB tracking: First token latency captured separately from total latency
- Usage sentinel: Providers emit a hidden
__usage__:N:Mtoken to pass token counts through the stream without breaking SSE
| Concern | Strategy |
|---|---|
| High message volume | Kafka partitioning by session_id; scale ingestion consumers horizontally (kafka_group_id consumer group) |
| DB write pressure | Batched upserts in ingestion worker; connection pooling (pool_size=10) |
| Large context windows | Chat API trims history to last N messages (configurable) |
| PII redaction overhead | Only applied to 200-char previews, not full content |
| SDK latency impact | <1ms added; async produce never awaited in request path |
| Cold start | Redpanda + Postgres healthchecks ensure services are ready before dependents start |
| Failure | Behavior |
|---|---|
| Kafka unavailable (produce) | SDK catches exception, logs to stderr; chat response unaffected |
| Kafka unavailable (consume) | Worker retries connection with exponential backoff |
| Invalid payload | Sent to dead_letter_logs; offset committed; worker continues |
| DB write fails (transient) | 3 retries with exponential backoff (1s, 2s, 4s) |
| DB write fails (permanent) | Sent to dead letter; offset committed |
| LLM provider error | Error captured in inference_logs.status='error'; SSE sends error event |
| Client disconnect | asyncio.CancelledError caught; partial message saved as cancelled |
| Service | Image | Port | Health Check |
|---|---|---|---|
| frontend | node:20-alpine | 3000 | HTTP /api/health |
| chat-api | python:3.12-slim | 8000 | HTTP /health |
| ingestion-worker | python:3.12-slim | 8001 | HTTP /health |
| postgres | postgres:16-alpine | 5432 | pg_isready |
| redpanda | redpandadata/redpanda | 9092, 9644 | rpk cluster health |
| prometheus | prom/prometheus | 9090 | β |
| grafana | grafana/grafana | 3001 | β |
Kubernetes manifests are in k8s/:
kubectl apply -f k8s/namespace.yaml
kubectl apply -f k8s/See k8s/README.md for minikube setup instructions.
- ClickHouse for inference_logs time-series queries at scale (10x faster aggregations)
- OpenTelemetry for end-to-end distributed traces across all services
- Alembic schema migrations instead of raw
init.sql - Auth (JWT/OAuth) for multi-tenant conversation isolation
- Grafana Alerting β Slack/PagerDuty on error rate spikes or P99 > 10s
- Dead Letter Replay API β HTTP endpoint to re-process failed events from DLQ
- Model cost tracking β Input/output token pricing per provider/model
- Rate limiting β Per session/API key request throttling
- Streaming token buffering β Batch small tokens before SSE flush for smoother UX
- Conversation search β Full-text search over message history
llm-inference-platform/
βββ docker-compose.yml # All 8 services
βββ .env.example # Environment variable template
βββ README.md
β
βββ chat-api/ # FastAPI backend
β βββ main.py # App entry + lifespan
β βββ config.py # Pydantic settings
β βββ metrics.py # Prometheus counters/histograms
β βββ providers/ # LLM provider implementations
β β βββ base.py # Abstract LLMProvider
β β βββ openai_provider.py
β β βββ anthropic_provider.py
β β βββ gemini_provider.py
β β βββ openrouter_provider.py
β βββ sdk/ # LLMLogger SDK
β β βββ logger.py # Core instrumentation
β β βββ pii_redactor.py # Presidio integration
β βββ routers/
β β βββ chat.py # SSE streaming endpoint
β β βββ conversations.py # CRUD + cancel/resume
β β βββ health.py # Health + Prometheus metrics
β βββ db/
β βββ models.py # SQLAlchemy ORM models
β βββ session.py # Async session factory
β
βββ ingestion-worker/ # Kafka consumer
β βββ main.py # Entry point + HTTP server
β βββ consumer.py # Kafka consume loop
β βββ processor.py # DB upserts + metrics
β βββ schemas.py # Pydantic validation
β βββ metrics.py # Ingestion-specific metrics
β βββ db/ # Shared ORM models
β
βββ frontend/ # Next.js 14
β βββ app/
β β βββ page.tsx # Chat UI
β β βββ conversations/ # Conversation list
β β βββ dashboard/ # Grafana embed
β βββ components/
β β βββ MessageBubble.tsx
β β βββ Sidebar.tsx
β β βββ ProviderSelector.tsx
β β βββ TypingIndicator.tsx
β βββ lib/api.ts # API client
β
βββ infra/
β βββ init.sql # PostgreSQL schema
β βββ prometheus.yml # Scrape config
β βββ grafana/ # Pre-built dashboards
β
βββ k8s/ # Kubernetes manifests
βββ namespace.yaml
βββ chat-api-deployment.yaml
βββ ingestion-deployment.yaml
βββ postgres-statefulset.yaml
βββ kafka-statefulset.yaml



