LovroG05 58d9c0a9c0 perf: lazy-load log content, populate user_id, keyset-paginate admin logs
Admin logs list no longer joins prompt/completion content for every row —
content is fetched per-row only when a log is expanded in the dashboard,
since a table of chat-length blobs was slow to serialize even at low row
counts.

request_logs.user_id (previously unpopulated despite an index existing on
it) is now set from the owning key on every insert, including synthetic
dashboard keys, and log queries filter on it directly instead of a
virtual_keys subquery. Pagination switches from OFFSET to a (created_at, id)
keyset cursor so paging deep into the log stays cheap. Adds indexes on
provider_name/key_prefix used by the admin filters.
2026-07-01 16:16:49 +02:00
2026-06-27 16:46:12 +02:00
2026-06-30 23:05:21 +02:00
2026-06-30 23:05:21 +02:00
2026-06-28 23:15:33 +02:00

LMR — Local Model Route

An OpenAI-compatible LLM gateway. It puts a single, authenticated, observable endpoint in front of any number of model backends — local servers and commercial APIs alike — and adds virtual-key auth, provider routing, request logging, usage-based billing, and a management UI.

Meant to run locally in a homelab environment as a way to securely-ish expose your local models and back them up with the big boys.

Clients talk to it exactly as they would to OpenAI (POST /v1/chat/completions, SSE streaming included); the gateway handles credentials, routing, and accounting behind the scenes.

Features

  • OpenAI-compatible proxy — drop-in /v1/chat/completions with streaming.
  • Virtual keys — issue, scope, revoke, and expire sk-… keys per user/group, each with its own model allowlist.
  • Multi-provider routing — local (llama.cpp / Ollama) and API providers (OpenAI, Anthropic, GitHub Copilot) behind one interface.
  • Provider abstraction — per-provider quirks handled internally (Anthropic cache_control injection, OpenAI cached_tokens, Copilot token exchange).
  • Usage & billing — per-request cost from a pricing table, written asynchronously so it never blocks the request path.
  • OIDC admin auth — Authentik / any OIDC provider, group-gated admin access.
  • Encrypted secrets — provider credentials stored AES-256-GCM encrypted at rest.
  • Single binary — the React management UI is embedded via go:embed.

Stack

  • Go (chi, pgx/v5, viper, go-oidc, golang-migrate) — see backend/go.mod
  • PostgreSQL 16 — keys, request logs, pricing
  • React + TypeScript (Vite) — management UI, built into the server binary

Repo layout

.
├── backend/                 # Go module: git.govekar.net/LovroG05/localmodelroutex
│   ├── cmd/server/          # entrypoint: config → db → migrate → HTTP server
│   ├── internal/
│   │   ├── api/             # HTTP handlers + middleware (proxy, admin, auth)
│   │   ├── providers/       # modular provider layer (registry + capability ifaces)
│   │   │   ├── openaicompat/ #   shared OpenAI-compatible transport
│   │   │   ├── openai|openrouter|llamacpp|ollama|copilot/  # OpenAI-compatible
│   │   │   ├── anthropic/   #   Messages API + OpenAI<->Anthropic translation
│   │   │   └── all/         #   blank-import aggregator (self-registration)
│   │   ├── catalog/         # runtime model library: cache, sync, any/<slug> routing
│   │   ├── store/           # hand-written pgx data-access layer
│   │   ├── crypto/          # AES-256-GCM seal/open for provider secrets
│   │   ├── config/          # viper config, GATEWAY_ env prefix
│   │   ├── db/              # pgxpool setup + migration runner
│   │   ├── billing/         # cost calculation + async usage logging
│   │   └── web/             # go:embed of the built UI, served as SPA
│   ├── migrations/          # golang-migrate SQL, applied automatically at startup
│   └── dashboard/           # Vite + React TS project (builds into internal/web/dist)
└── README.md

Authentication

Two auth systems that never mix:

Surface Header Verification
Proxy routes (/v1/*) Bearer sk-xxx sha256 of the key → lookup in virtual_keys; check revoked / expired / model allowlist
Admin routes (/api/admin/*) Bearer <OIDC JWT> go-oidc Verify() → groups claim → must contain the admin group

Session keys: POST /auth/exchange trades an Authentik OIDC JWT for an sk-sess-xxx virtual key scoped to that group's model allowlist.

Provider credentials are encrypted with AES-256-GCM using the 32-byte key in GATEWAY_SECURITY_ENCRYPTION_KEY and decrypted only in memory at request time.

Configuration

All settings come from environment variables prefixed GATEWAY_. Dot-notation config keys map to underscores in env (e.g. oidc.client_id → GATEWAY_OIDC_CLIENT_ID). In development, a .env file in the working directory is loaded automatically; in production, set real environment variables.

Variable Default Required Notes
GATEWAY_SERVER_PORT 8080 no HTTP listen port
GATEWAY_SERVER_ENV development no development | production (controls log format/level)
GATEWAY_DB_URL — yes postgres://user:pass@host:5432/db
GATEWAY_DB_MAX_CONNS 20 no pool max connections
GATEWAY_DB_MIN_CONNS 2 no pool min connections
GATEWAY_DB_MAX_CONN_LIFETIME 30 no minutes
GATEWAY_OIDC_ISSUER — yes OIDC issuer URL
GATEWAY_OIDC_CLIENT_ID — yes OIDC client ID
GATEWAY_OIDC_ADMIN_GROUP — yes group required for admin access
GATEWAY_SECURITY_ENCRYPTION_KEY — yes 64 hex chars (32 bytes) for AES-256-GCM

Config is validated on startup; missing required values or a wrong-length encryption key cause the server to exit immediately.

Generate an encryption key:

openssl rand -hex 32

Running locally

Requires Go (see the go directive in backend/go.mod) and a PostgreSQL 16 instance.

# 1. Start Postgres
docker run -d --name lmr-postgres \
  -e POSTGRES_USER=gateway -e POSTGRES_PASSWORD=gateway -e POSTGRES_DB=gateway \
  -p 5432:5432 postgres:16

# 2. Configure
cd backend
cp .env.example .env   # fill in OIDC issuer/client + a 32-byte encryption key

# 3. Run — make loads .env, applies migrations, and starts the server
make run

The server listens on GATEWAY_SERVER_PORT (default :8080).

.env is consumed as environment variables: the Makefile loads it via include .env / export, and viper reads GATEWAY_* from the environment. For Docker/systemd, pass it with env_file / EnvironmentFile instead.

Make targets

Target Action
make run Run the gateway (migrations auto-applied on startup)
make build Build the dashboard, then compile bin/gateway
make ui Build the dashboard into the embedded asset dir
make migrate-up Apply all pending migrations
make migrate-down Roll back the most recent migration
make test / make lint / make tidy Go tests / lint / module tidy

Health checks

Endpoint Meaning
GET /healthz Liveness — process is up
GET /readyz Readiness — database reachable (pings the pool)

Database & migrations

SQL migrations live in backend/migrations/ using golang-migrate's {version}_{name}.{up,down}.sql naming. They are embedded into the binary (go:embed) and applied automatically on startup, so a deployed binary is self-contained — no migration files needed on the host.

To add a migration, create a new numbered pair, e.g.:

migrations/000002_virtual_keys.up.sql
migrations/000002_virtual_keys.down.sql

Providers

Name Kind Notes
llamacpp local OpenAI-compatible; routes through llama-swap
ollama local OpenAI-compatible
openrouter api key OpenAI-compatible; HTTP-Referer/X-Title attribution; reports pricing
openai api key reads cached_tokens from the usage response
anthropic api key Messages API ↔ OpenAI translation; injects cache_control on the system prompt + penultimate message
github-copilot subscription exchanges a GitHub PAT for a ~30-min Copilot token, mutex-guarded refresh

Model addressing

Models are addressed as <provider>/<slug>, where <provider> is the configured provider instance name and <slug> is the upstream model id (which may itself contain slashes, e.g. openrouter/anthropic/claude-3.7-sonnet). The catalog is built at runtime by syncing each provider's model listing on a timer and cached in memory.

  • any/<slug> — a reserved prefix that fans out across every provider serving that model and picks one by the configured routing strategy (round_robin | lowest_cost | priority, in settings).
  • Curated aliases — admins can define a friendly name mapping to one or more catalog entries with priority/weight for local-first/cloud-backup failover.
  • Admin overrides — per-model enabled/blocked, parameter overrides, pricing, and rate limits live alongside the discovered data and survive syncs.

Frontend

The management UI is a Vite + React + TypeScript project in backend/dashboard/. Its production build is written into backend/internal/web/dist/ (set via build.outDir in vite.config.ts) and embedded into the server binary, which serves it as a single-page app with history-API fallback (API routes take precedence).

cd backend/dashboard
npm install
npm run build      # type-checks (tsc), then outputs to ../internal/web/dist

During development, run the Vite dev server alongside the gateway:

npm run dev        # serves the UI on http://localhost:5173 with hot reload

The dev server proxies /v1, /api, and /auth to http://localhost:8080 (configured under server.proxy in vite.config.ts), so the running gateway backs the UI in development.

Project status

Implemented: configuration, the database pool + migration runner, the server entrypoint (logging, health checks, graceful shutdown), the embedded dashboard handler, secret encryption (internal/crypto), the modular provider layer (internal/providers — OpenAI, OpenRouter, llama.cpp, Ollama, Anthropic, GitHub Copilot, with chat streaming + non-streaming and usage accounting), and the runtime model catalog (internal/catalog + internal/store: discovery sync, in-memory cache, <provider>/<slug> + any/<slug> routing) — all verified against Postgres.

Still to build: wiring the catalog syncer into the running server (build live providers from DB rows + decrypt credentials), and the HTTP API (internal/api — virtual-key auth, the /v1 proxy that ties keys → catalog resolution → provider dispatch, admin CRUD for providers/models/overrides, OIDC exchange), billing writes into request_logs, and the dashboard UI itself.

Description
No description provided
Readme AGPL-3.0 2 MiB
Languages
Go 66.4%
TypeScript 27.6%
PLpgSQL 2.5%
CSS 2%
Makefile 0.7%
Other 0.8%