A self-hosted inference orchestration layer — a personal mini-OpenRouter that routes chat requests across local and cloud AI providers, with auth, conversation persistence, semantic memory, image generation, and full observability.
Status: All core features built. Backend complete; frontend client in development.
Prereqs: Docker Desktop (Windows/Mac) or Docker Engine (Linux), and git. Optional — for local models — LM Studio (chat / prompt rewriting / embeddings) on port 1234, ComfyUI on 8188, and Ollama on 11434.
-
Clone and run the setup script — it generates
.envfrom.env.example(random secrets, your local model ids), checks Docker, and starts the stack:git clone https://github.com/ishaab/llm-gateway cd llm-gateway .\setup.ps1 # Windows PowerShell # or ./setup.sh # Linux / macOS / WSL
Already have a
.env? The script detects it and leaves it unchanged.The setup scripts accept two flags:
-SkipStart/--skip-startprepares.envwithout starting Docker (.\setup.ps1 -SkipStart,./setup.sh --skip-start), and-NonInteractive/--non-interactiveskips the prompts for CI/automation (.\setup.ps1 -NonInteractive,./setup.sh --non-interactive).Run setup before
docker compose upsomonitoring/metrics_tokenexists — otherwise Docker creates it as a directory and Prometheus can't read the token. -
Open the API docs: http://localhost:2727/docs (Swagger UI).
-
Register an account (
POST /auth/register). -
Add your providers — see Adding providers. A "Local (LM Studio)" provider row is seeded automatically when
LM_URLis set. -
Start chatting.
From a phone over Tailscale, reach the app through Caddy on port 80 (e.g.
http://<tailscale-ip>); keep port 2727 internal.
Registration is open by default (REGISTRATION_ENABLED=true). Once you've created your
account, close the door: set REGISTRATION_ENABLED=false in .env and restart the backend
(docker compose up -d backend). POST /auth/register then returns 403 {"detail": "registration disabled"} for everyone — including attempts to re-register your own email.
- Use the prod Dockerfile target for anything beyond dev — the compose
backendservice buildstarget: devby default (root user +--reload); theprodtarget runs as a non-root user with no reload (docker compose -f docker-compose.yml -f docker-compose.prod.yml upor a similar override). - Set
REGISTRATION_ENABLED=falsefor a personal deployment once your account exists — see Locking down signups. - The operator OpenRouter key is shared into every account by design — the "OpenRouter" provider row is seeded per user from the same key, so only enable registration if you intend a multi-user deployment where every account is trusted with that key.
- Monitoring is loopback-only — Prometheus (
127.0.0.1:9090) and Grafana (127.0.0.1:3000) are bound to the host; Caddy on:80is the only tailnet ingress.
- Provider Routing — Automatically routes requests to the best provider based on privacy needs, task type, model name, and context length. Sensitive data stays local; coding and long tasks go to OpenRouter.
- Semantic Memory (RAG) — Every message is embedded (Ollama
nomic-embed-text, 768-dim) and stored in pgvector. On each turn, the top-3 semantically similar past messages are injected as context. - Parameter Presets — Save and reuse model parameter profiles (temperature, top_k, top_p, min_p, repeat_penalty, etc.). Default preset created on registration.
- SDXL Prompt Templates — Rewrite natural language prompts into structured SDXL tags using a dedicated LLM (Qwen 2.5 on LM Studio). User-definable template structures.
- ComfyUI Image Generation — Generate images via ComfyUI workflows with optional prompt rewriting. Poll job status and retrieve results.
- JWT Authentication — Access token (60 min) + refresh token (7 days, persisted) flow. Password hashing with bcrypt.
- Rate Limiting — Sliding-window rate limit per user via Redis sorted sets (30 req/min default, configurable).
- Observability — Prometheus metrics (request count, latency, tokens/sec by provider/model) + Langfuse Cloud LLM tracing.
- Agent Loop (tool calling) — Agent mode (
/v1/agent/chat) with first-party tools (recall_recent_exchanges,web_search,fetch_page,current_datetime,search_conversations,generate_image,calculate, plus thememory_read/memory_write/memory_str_replace/memory_append/memory_deletememory tools) plus MCP tools, gated by per-user permissions. Code-execution tools (bash,edit_patch,edit_lines,write_file) are additionally gated by theENABLE_CODE_EXECUTIONmaster switch on both the agent and the plain-chat tool paths. When code execution is enabled,bashruns in a sandbox confined per-tenant: each(user_id, agent_id)gets a distinct OS UID and achmod 700workspace, so a model-driven shell can't read or write another user's workspace on the shared volume (seesandbox/uid_alloc.py). - Memory Files (file store) — Per-user, versioned memory files (a Claude-style file store, deliberately not embeddings/RAG) that the agent reads and edits through the
memory_*tools — durable profile, preferences, and notes that persist across conversations. Every chat and agent prompt is prefixed with a Tier-1 index of the user's files (- {path} — {description}) plus full Tier-1.5 files (default/profile.md,/preferences.md) injected as delimited, byte-capped system context (each file wrapped in<memory_file path="...">and capped atMEMORY_TIER1_5_INJECT_CAP), so the agent knows what it can read before it calls a tool. The mutatingmemory_*tools are deny-by-default (first_party=False, likebashand the file tools) — the model can only write memory after the user explicitly grants it viaPUT /v1/agent/tools/{name}/permission, which breaks the prompt-injection chain (a fetched page can't silently plant persistent memory). A background curation pipeline (arq job) runs after every chat/agent turn: it reads the lastMEMORY_CURATION_MAX_MESSAGEStranscript messages plus the current files (with versions), asks the batch model once to propose memory-file operations (create/write/append/str_replace/delete) under a strict rule set (one-month horizon, user-stated facts only, privacy exclusions, one file per subject, consolidation near the cap), and applies them through the same versioned primitives — private chats never feed memory, and paths the agent wrote in-turn are skipped.
| Layer | Technology | Purpose |
|---|---|---|
| Backend | Python 3.11 + FastAPI | Async API gateway |
| Database | PostgreSQL 16 + pgvector | Relational data + vector embeddings |
| Cache | Redis 7 | Rate limiting |
| Auth | JWT (python-jose) + bcrypt (passlib) | Authentication |
| ORM | SQLAlchemy 2.x (async) + Alembic | Data layer + migrations |
| Providers | LM Studio, Ollama, OpenRouter, ComfyUI | Inference backends |
| Image Gen | ComfyUI + Qwen 2.5 (prompt rewriting) | Text-to-image |
| Observability | Prometheus + Grafana + Langfuse | Metrics, dashboards, LLM tracing |
| Reverse Proxy | Caddy 2 | HTTPS, path-based routing |
| Containerization | Docker + Docker Compose | Service orchestration |
| Method | Path | Description | Auth |
|---|---|---|---|
| POST | /auth/register |
Register a new user | No |
| POST | /auth/login |
Login, get access + refresh tokens | No |
| POST | /auth/refresh |
Exchange refresh token for new access token | No |
| POST | /auth/logout |
Invalidate a refresh token | Yes |
| Method | Path | Description | Auth |
|---|---|---|---|
| POST | /v1/chat/completions |
SSE-streamed chat completion with auto-routing | Yes |
| Method | Path | Description | Auth |
|---|---|---|---|
| GET | /v1/models |
List models loaded in LM Studio | No |
| GET | /v1/openrouter/models |
List free OpenRouter models | No |
| Method | Path | Description | Auth |
|---|---|---|---|
| POST | /v1/convo |
Create conversation | Yes |
| GET | /v1/convo |
List user's conversations | Yes |
| GET | /v1/convo/{id} |
Get messages in a conversation | Yes |
| PATCH | /v1/convo/{id} |
Rename conversation | Yes |
| DELETE | /v1/convo/{id} |
Delete conversation | Yes |
| Method | Path | Description | Auth |
|---|---|---|---|
| POST | /v1/presets |
Create a parameter preset | Yes |
| GET | /v1/presets |
List user's presets | Yes |
| GET | /v1/presets/{id} |
Get preset details | Yes |
| PATCH | /v1/presets/{id} |
Update preset | Yes |
| DELETE | /v1/presets/{id} |
Delete preset | Yes |
| Method | Path | Description | Auth |
|---|---|---|---|
| POST | /v1/templates |
Create a prompt template | Yes |
| GET | /v1/templates |
List user's templates | Yes |
| GET | /v1/templates/{id} |
Get template details | Yes |
| PATCH | /v1/templates/{id} |
Update template | Yes |
| DELETE | /v1/templates/{id} |
Delete template | Yes |
| POST | /v1/templates/rewrite |
Rewrite a prompt for SDXL | Yes |
| Method | Path | Description | Auth |
|---|---|---|---|
| POST | /v1/images/generate |
Generate image via ComfyUI | Yes |
| GET | /v1/images/status/{prompt_id} |
Poll generation status | Yes |
| Method | Path | Description | Auth |
|---|---|---|---|
| GET | /v1/hardware |
GPU/VRAM probe (pynvml or nvidia-smi); also returns ram_total_mb (total system RAM) |
Yes |
| GET | /v1/cookbook |
Fit-score the local LM Studio catalog against total VRAM + RAM offload | Yes |
| GET | /v1/hf/models |
Search Hugging Face models (search, limit 1–50) fit-scored against total VRAM + RAM offload |
Yes |
| GET | /v1/hf/models/{repo_id} |
HF model detail + per-quant GGUF-accurate fit (context_tokens 512–262144) |
Yes |
| Method | Path | Description | Auth |
|---|---|---|---|
| GET | /health |
Health check | No |
| GET | /metrics |
Prometheus metrics | Bearer token (METRICS_TOKEN) |
llm-gateway/
.env # Environment variables (gitignored)
requirements.txt # pip shim → backend/requirements.txt (venv installs)
docker-compose.yml # Orchestrates 6 services
arch-dia.png # Architecture diagram
backend/
Dockerfile # Python 3.11-slim, uvicorn --reload
requirements.txt # Python dependencies
alembic.ini # Database migration config
alembic/ # Migration versions (6 migrations)
app/
main.py # FastAPI entry point, middleware, router registration
db.py # Async SQLAlchemy engine + session
core/
config.py # Pydantic settings from .env
metrics.py # Prometheus + Langfuse observability
redis.py # Async Redis connection pool
security.py # JWT creation/verification, password hashing
middleware/
ratelimit.py # Sliding-window rate limiter (Redis)
models/
users.py
conversations.py
messages.py
refresh_tokens.py
memories.py
presets.py
templates.py
routers/
auth.py # Registration, login, token refresh, logout
chat.py # SSE streaming chat completion
convo.py # Conversation CRUD
images.py # ComfyUI image generation
models.py # Model listing (LM Studio + OpenRouter)
presets.py # Preset CRUD
templates.py # Prompt template CRUD + rewrite
services/
router.py # Provider routing engine
convo.py # Conversation management + semantic memory
memory.py # pgvector embeddings + retrieval
template.py # SDXL prompt rewriting
comfy.py # ComfyUI image generation
caddy/
Caddyfile # Reverse proxy config
monitoring/
prometheus.yml # Prometheus scrape config
workflows/
t2i-default.json # Default ComfyUI workflow
Prerequisites:
- Docker Desktop
- LM Studio running on port 1234 with at least one chat model loaded (chat + prompt rewriting), plus an embedding model matching
LM_EMBED_MODEL(defaulttext-embedding-nomic-embed-text-v1.5) - ComfyUI running on port 8188 (optional, for image generation)
- Ollama on port 11434 (optional)
Setup:
-
Clone the repo:
git clone https://github.com/ishaab/llm-gateway cd llm-gateway -
Create a
.envfile (see Environment Variables). -
Start everything:
docker compose up --build
-
Verify:
- Health check:
http://localhost:2727/health - API docs (Swagger UI):
http://localhost:2727/docs - Prometheus:
http://localhost:9090(loopback-only — not reachable from the tailnet) - Grafana:
http://localhost:3000(loopback-only — not reachable from the tailnet; admin useradmin, password fromGRAFANA_ADMIN_PASSWORDin your generated.env— the setup script generates one if you don't set it)
- Health check:
To work on the FastAPI backend directly from a virtualenv (debugging, IDE tooling, no Docker for the app itself):
Prerequisites:
- Python 3.11+ (the lockfile is compiled on 3.11; 3.13 is verified to install too)
- A Postgres 16 server with the
pgvectorextension and a Redis server reachable from the host. The composepostgres/redisservices publish no host ports, so either run your own (system packages / installers) or expose them via adocker-compose.override.yml(gitignored-safe, Docker-only):services: postgres: ports: ["127.0.0.1:5432:5432"] redis: ports: ["127.0.0.1:6379:6379"]
Setup:
-
Create a venv at the repo root and install the pinned dependencies:
python -m venv .venv source .venv/bin/activate # Windows: .venv\Scripts\activate pip install -r requirements.txt # root shim → backend/requirements.txt
uvloop(pulled in byuvicorn[standard]) is skipped automatically on Windows via an environment marker — the app runs uvicorn's standard event loop there. -
Create
backend/.env— settings and alembic both load it from the directory you run from (backend/). Required keys:LM_URL=http://localhost:1234/v1 LM_DEFAULT_MODEL=<LM Studio model id> COMFY_URL=http://localhost:8188 DATABASE_URL=postgresql+asyncpg://<user>:<password>@localhost:5432/<db> REDIS_URL=redis://localhost:6379/0 SECRET_KEY=<python -c "import secrets; print(secrets.token_hex(32))"> ALGORITHM=HS256
Use
localhost, nothost.docker.internal— there's no container network on this path. Provider API keys that Docker mounts as secrets (e.g.OPENROUTER_API_KEY) fall back to plain env vars viaget_secret()— add them to the same.env. The app boots without OpenRouter (it's optional). -
Migrate and run:
cd backend alembic upgrade head uvicorn app.main:app --reload --port 8000
The arq worker (deep research) needs the same env — from backend/ in a second shell:
arq app.worker.WorkerSettings. Backend unit tests run offline the same way:
python -m unittest discover -s tests -p "test_*.py".
Backend dependencies are pinned with pip-tools:
- Edit
backend/requirements.in— the editable source of truth (direct deps only, comments welcome). - Regenerate the lockfile inside the backend container:
docker compose exec -T backend pip-compile --output-file /app/requirements.txt /app/requirements.in - Commit both files.
backend/requirements.txtis generated — never hand-edit it.
CI (.github/workflows/schema-drift.yml) runs on every push and pull request: it installs
the pinned dependencies, migrates a scratch PostgreSQL database to head and fails when the
SQLAlchemy models and Alembic migrations disagree (schema-drift guard), then runs the
backend unit tests.
Backend tests are stdlib unittest (no pytest dependency):
cd backend && python -m unittest discover -s tests -p "test_*.py"Tests that touch Postgres/asyncio/pgvector/redis/arq/prometheus/langfuse run in the Docker
container (those deps). Offline tests — the workspace store and the agent/tools/sandbox
coverage — stub the optional runtime deps only during import via
tests/agent_test_stubs.py::import_with_stubs, so they run on a bare dev host without
polluting sibling test modules; see AGENTS.md → "Test conventions (backend)".
| Service | Image | Port | Purpose |
|---|---|---|---|
postgres |
pgvector/pgvector:pg16 |
internal | Persistent storage + pgvector extension |
redis |
redis:7-alpine |
internal | Rate limiting backend |
backend |
Build from ./backend/Dockerfile |
127.0.0.1:2727:8000 (loopback-only) |
FastAPI application |
worker |
Build from ./backend/Dockerfile |
internal | arq deep-research job worker (same image as backend) |
searxng |
searxng/searxng |
internal | Optional self-hosted search for web_search + research (start with --profile search) |
prometheus |
prom/prometheus |
127.0.0.1:9090:9090 (loopback-only) |
Metrics collection |
grafana |
grafana/grafana |
127.0.0.1:3000:3000 (loopback-only) |
Dashboards (admin user admin; password = GRAFANA_ADMIN_PASSWORD in .env) |
caddy |
caddy:2 |
80:80 |
Reverse proxy (strips /api prefix) |
The Caddyfile routes:
http://<host>/api/*-> strips/api, proxies tobackend:8000http://<host>/(WebSocket) -> proxies tohost.docker.internal:6969http://<host>/(non-WebSocket) -> proxies tohost.docker.internal:6969
Auto HTTPS is disabled. The frontend should call /api/v1/* and Caddy will forward it as /v1/* to the backend.
| Variable | Required | Description |
|---|---|---|
LM_URL |
Yes | LM Studio base URL (chat + prompt rewriting + embeddings) |
LM_DEFAULT_MODEL |
Yes | LM Studio model for SDXL rewriting |
COMFY_URL |
No | ComfyUI base URL for image generation (default: http://host.docker.internal:8188) |
OPENROUTER_API_KEY |
No | OpenRouter API key |
OPENROUTER_DEFAULT_MODEL |
No | Default OpenRouter model |
SANDBOX_SHARED_SECRET |
Yes for code-execution | Shared secret for the sandbox POST /exec endpoint — compose injects it into both backend and sandbox; the sandbox fail-closes (refuses every exec) when unset. Use any long random string. Required whenever ENABLE_CODE_EXECUTION=true (code-execution is also per-tenant confined: each workspace runs as its own OS UID and is chmod 700'd, so a shell can't cross tenants; and network-isolated: the sandbox sits on its own network with the backend as its only peer, with outbound internet but no route to postgres/redis/backend/host) |
SUGGEST_CLOUD_MODEL |
No | Pin a specific cloud model for POST /v1/agents/suggest (empty = derive from the resolved provider model). Try a :free model if your key is free-only |
SUGGEST_CLOUD_FALLBACK_MODELS |
No | Comma-separated free-model candidates Smart Suggest tries after the primary cloud model, before falling back to local (default meta-llama/llama-3.1-8b-instruct:free, google/gemma-2-9b-it:free, qwen/qwen-2-7b-instruct:free) |
POSTGRES_USER |
Yes | PostgreSQL user, required by docker-compose (default ishaab; the setup scripts add it to .env if missing). Must match the user embedded in DATABASE_URL |
POSTGRES_DB |
Yes | PostgreSQL database name, required by docker-compose (default llmgateway; the setup scripts add it to .env if missing). Must match the database embedded in DATABASE_URL |
DATABASE_URL |
Yes | PostgreSQL connection string (user/db must match POSTGRES_USER / POSTGRES_DB) |
POSTGRES_PASSWORD |
Yes | PostgreSQL password, required by docker-compose (and embedded in DATABASE_URL) |
REDIS_URL |
Yes | Redis connection string |
SECRET_KEY |
Yes | JWT signing secret |
ALGORITHM |
Yes | JWT algorithm (HS256) |
ACCESS_TOKEN_EXPIRY_MINUTES |
Yes | Access token TTL (60) |
REFRESH_TOKEN_EXPIRY_DAYS |
Yes | Refresh token TTL (7) |
GRAFANA_ADMIN_PASSWORD |
Yes | Grafana admin login password, required by docker-compose; the setup script generates one if you don't set it |
METRICS_TOKEN |
No | Bearer token for GET /metrics; empty disables the endpoint (fail-closed). The setup scripts generate one and mirror it to monitoring/metrics_token for Prometheus |
TRUSTED_PROXIES |
No | Comma-separated CIDRs of reverse proxies whose X-Forwarded-For header the rate limiter trusts (default 172.16.0.0/12 — the Docker bridge Caddy sits on). Set to empty to ignore X-Forwarded-For entirely |
REGISTRATION_ENABLED |
No | Set false to disable open signups — POST /auth/register returns 403 "registration disabled" (default true) |
ALLOW_PRIVATE_PROVIDER_URLS |
No | Provider base_urls may point at private/loopback hosts (e.g. local LM Studio); set false to enforce public-only URLs — breaks local providers, use only on locked-down deployments (default true) |
MCP_SERVERS |
No | JSON list of MCP servers (stdio/SSE) the agent can call. Operator-configured only — it can spawn arbitrary commands; never let user input reach this setting |
MEMORY_FILE_CAP_BYTES |
No | Per-file cap for the user memory file store; writes at/over the cap are rejected, never truncated (default 32768) |
MEMORY_TIER1_5_PATHS |
No | Comma-separated memory file paths always injected into chat/agent context as delimited system blocks (default /profile.md,/preferences.md) |
MEMORY_TIER1_5_INJECT_CAP |
No | Per-file byte cap on tier-1.5 context injection (defense-in-depth, default 2000) |
MEMORY_CURATION_MAX_MESSAGES |
No | Transcript window (messages) the background curation pass feeds the batch model (default 20) |
MEMORY_CURATION_MODEL_ROLE |
No | Which provider role the curation batch model prefers: auto (cloud when OpenRouter is configured, else local), local, or cloud (default auto) |
LANGFUSE_PUBLIC_KEY |
No | Langfuse Cloud public key |
LANGFUSE_SECRET_KEY |
No | Langfuse Cloud secret key |
LANGFUSE_BASE_URL |
No | Langfuse endpoint |
MCP trust boundary:
MCP_SERVERSis operator configuration — a stdio entry runs whatevercommandit names, so treat the setting like a Dockerfile: trusted config only. Never derive it from, or let it be influenced by, user input (a chat message must never be able to add or alter an MCP server).
The routing engine (services/router.py) decides where to send each request using this priority:
private: truein request -> Always route to local LM Studio (privacy override)provider: "local"in request -> Route to local LM Studio- Model name contains
/(e.g.openrouter/owl-alpha) -> Route to OpenRouter provider: "openrouter"in request -> Route to OpenRouter- Coding keywords in last message (e.g.
script,code,function,debug,python,c++,javascript) -> Route to OpenRouter - More than 80 messages in conversation -> Route to OpenRouter (longer context windows)
- Default -> Route to local LM Studio
The chat endpoint receives provider, model, private fields in the request body to control this behavior.
The gateway is bring-your-own-key: after registering, create provider rows via POST /v1/providers (or the /docs UI). Supported types:
type |
Use for |
|---|---|
openai_compatible |
Any OpenAI-wire endpoint — LM Studio, Ollama, Groq, vLLM, OpenCode Go, ... |
openai |
OpenAI cloud |
anthropic |
Anthropic |
google |
Google Gemini |
openrouter |
OpenRouter |
Keys are stored encrypted (Fernet) and are write-only — responses only ever show a masked suffix (api_key_masked). Each role (local / cloud) can have one default provider; a request can also pin a specific provider by passing its provider_id in the chat/agent body (overrides every routing heuristic).
Example — an OpenAI-compatible endpoint (LM Studio, Ollama, Groq, ...):
{
"name": "LM Studio",
"type": "openai_compatible",
"role": "local",
"base_url": "http://host.docker.internal:1234",
"api_key": "",
"default_model": "qwen2.5-7b-instruct",
"is_default": true
}base_url gets /v1 appended automatically when it's missing (so http://host:1234 and http://host:1234/v1 both work). A "Local (LM Studio)" row is seeded for you when LM_URL is set, and an "OpenRouter" row only when an OpenRouter key is configured. If you never create rows, the gateway falls back to the legacy env-var configuration (LM_URL, LM_CHAT_MODEL, OPENROUTER_API_KEY, ...).
- Register (
POST /auth/register) -> Creates user, default preset, default SDXL template. - Login (
POST /auth/login) -> Returnsaccess_token(60 min) +refresh_token(7 days). - Use API -> Send
Authorization: Bearer <access_token>header on every protected endpoint. - Refresh (
POST /auth/refresh) -> Exchangerefresh_tokenfor a newaccess_token. - Logout (
POST /auth/logout) -> Invalidates the specificrefresh_tokenin the database.
- User sends
POST /v1/images/generatewith a natural language prompt. - If
rewrite: true, the prompt is rewritten using LM Studio Qwen 2.5 into comma-separated SDXL tags via a template structure. - The rewritten prompt is submitted to ComfyUI (
host.docker.internal:8188) as a KSampler workflow. - The endpoint returns a
prompt_idfor polling. - Client polls
GET /v1/images/status/{prompt_id}untilstatus: "complete", then renders image URLs from the response.
- Every user and assistant message is embedded via Ollama
nomic-embed-text:latestinto a 768-dim vector. - Embeddings are stored in the
memoriestable via pgvector. - On each new message in a conversation, the top 3 semantically similar past messages (lowest cosine distance
<=>) are injected as asystemcontext message before the conversation history.
Presets are reusable parameter profiles for LLM generation. Each user has their own presets. The default preset has:
| Field | Default |
|---|---|
temperature |
0.8 |
context_overflow |
truncate_middle |
Additional supported fields: system_prompt, token_limit, stop_strings (array), top_k, top_p, min_p, repeat_penalty.
Local provider parameters (top_k, min_p, repeat_penalty) are sent via extra_body in the OpenAI-compatible API call.
- Prometheus scrapes
backend:8000/metricsevery 15s. - Custom metrics:
chat_requests_total,chat_latency_seconds,tokens_per_second,prompt_tokens_total,active_conversations_total. - Grafana runs on port 3000.
- Langfuse traces every chat generation with input, output, model, provider, latency, and token metadata. Full chat content is sent to Langfuse unless the message is sent with
private: true(those chats record metadata only). If you don't use Langfuse, remove theLANGFUSE_*keys from.env.
This is a personal project. Issues and PRs are welcome.
MIT
