LLeMbas now runs end to end. Register, add an OpenAI-compatible connection, and hold a real streaming conversation organised into folders. Verified against the local llama-swap instance. Streaming is the one genuinely tricky part. Sending a message returns two HTML fragments -- the user bubble and an empty assistant bubble carrying an sse-connect -- and that attribute is the ONLY thing that starts a generation. Rendering an incomplete assistant message as a streaming shell falls out of the same template, which means loading a page whose last reply never finished simply picks it up again. Details worth knowing about, each commented where it matters: - SSE payloads are split across several data: lines. A raw newline in one data: line truncates the event, which shows up the first time a model emits a code block. - Markdown is rendered server-side by the same helper for both the page and the final streamed frame, so the two cannot disagree. The fence renderer is replaced outright rather than using markdown-it's highlight option, which re-wraps output in a second <pre>. - escape_text is html.escape, not nh3.clean_text: it escapes character by character, so escaping stream chunks separately equals escaping the whole string. - The stream opens its own session via session_scope(); it outlives the request handler and the dependency-scoped session may be closed. - Deleting a folder keeps the chats inside it (FK is SET NULL). Losing a conversation to a mis-clicked folder delete is unforgivable. - Login failures use one message for "no such account" and "wrong password" so the form cannot enumerate registered addresses. Also adds deploy/ for the gamebox install at https://chat.lan: system unit, nginx vhost with buffering off (buffering on turns streaming into one lump at the end), and install/update scripts following the same service-user and /srv bind-mount conventions as llama-swap and comfyui. 70 tests, ruff clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A self-hosted web UI for your language models, written in Python.
Talks to anything that speaks the OpenAI API. Themed after Middle-earth.
Lembas is the Elvish waybread — one bite sustains a traveller for a day's march. The capitals hide what it runs on: LLeMbas.
Why this exists
Most self-hosted LLM front-ends are large JavaScript applications with a Python
API bolted underneath. LLeMbas is the other way round: server-rendered
Python, with htmx and a little Alpine for interactivity. There is no
package.json, no bundler, no build step, and nothing is fetched from a CDN at
runtime. Clone it, pip install -e ., run it.
Features
Working now
- Chats — streaming replies, Markdown with server-side syntax highlighting, copy and regenerate, automatic chat titles
- Folders — arbitrarily nested, delete a folder without losing the chats inside it
- OpenAI connections — point at OpenAI, LM Studio, vLLM, llama.cpp, llama-swap, Ollama or OpenRouter; models are discovered and cached
- Accounts — first account becomes the administrator, argon2 password hashing, revocable server-side sessions
- Two themes — Moria (dark) and Shire (light), switchable per user
Planned
Users & groups with permissions · file upload, vision and PDFs · built-in tools with admin settings · custom tools and MCP servers · agentic execution (local and over SSH) · image generation.
Quick start
git clone https://git.houmeres.sk/Houmeres/LLeMbas.git
cd LLeMbas
python -m venv .venv && . .venv/bin/activate
pip install -e ".[dev]"
cp .env.example .env
lembas secret-key # paste the result into LEMBAS_SECRET_KEY
lembas serve # http://127.0.0.1:8080
Open the address and create the first account — it becomes the administrator.
Then go to Admin → Connections and add an endpoint. For a local runner that
is usually http://localhost:1234/v1 with no API key. Press Test & refresh
and its models appear in the chat model picker.
The vendored browser libraries (htmx, Alpine) are committed, so no network access is needed to run. To re-fetch or bump them:
python scripts/fetch_vendor.py --update.
Configuration
All variables are prefixed LEMBAS_ and can live in .env. See
.env.example for the annotated list.
| Variable | Default | Purpose |
|---|---|---|
LEMBAS_SECRET_KEY |
generated | Signs sessions and encrypts stored API keys. Set this. A generated key changes every restart, signing everyone out and making stored API keys unreadable. |
LEMBAS_DATA_DIR |
./data |
SQLite database and uploads. |
LEMBAS_HOST / LEMBAS_PORT |
127.0.0.1 / 8080 |
Bind address. |
LEMBAS_ALLOW_SIGNUP |
true |
Let new users register themselves. The first account is always an admin regardless. |
LEMBAS_DEFAULT_THEME |
moria |
moria (dark) or shire (light). |
LEMBAS_SESSION_TTL |
2592000 |
Session lifetime in seconds. |
LEMBAS_REQUEST_TIMEOUT |
300 |
Seconds to wait on an upstream model. |
Commands
lembas serve # run the server
lembas info # where data lives, what is configured
lembas secret-key # generate a value for LEMBAS_SECRET_KEY
lembas create-admin # create or promote an administrator
How it fits together
Browser ──form POST──▶ FastAPI ──▶ SQLite
▲ │
│ └──httpx──▶ any OpenAI-compatible endpoint
└──── server-sent events ◀───────────────┘ (streamed reply)
Sending a message stores the turn and returns two HTML fragments: the user's
bubble and an empty assistant bubble carrying an sse-connect. That opens a
server-sent event stream which appends tokens as they arrive, then replaces the
whole bubble with the finished, Markdown-rendered version. Rendering and
highlighting happen in Python, so the streamed and final views cannot disagree.
src/lembas/
api/ routes: auth, chats, folders, admin, pages
db/models/ SQLAlchemy schema
security/ password hashing, sessions
services/ llm client, chat orchestration, markdown, crypto, sse
web/ Jinja templates and static assets
assets/ SVG artwork masters
scripts/ artwork generator, vendored-JS fetcher
deploy/ systemd unit and nginx vhost for a real install
Development
pytest # test suite
ruff check . # lint
python scripts/build_artwork.py # regenerate the SVG artwork
python scripts/fetch_vendor.py # verify vendored JS against the lockfile
There is no migration tool. The schema is SQLite-only and created at startup,
so changing a column on a live database is a manual job — see CLAUDE.md.
Artwork
The logo, favicon and banner are original vector work, generated by
scripts/build_artwork.py so the mallorn leaf stays
identical across every size it appears at. The wordmark is
Source Serif 4 (SIL OFL 1.1)
converted to outlines — a README banner cannot load a webfont, and <text>
would render in whatever serif the reader happens to have.
Licence
A note on the theme
This is an independent hobby project, themed as an affectionate nod to J.R.R. Tolkien's world. It is not affiliated with, endorsed by, or connected to the Tolkien Estate, Middle-earth Enterprises, or any related rights holder. All artwork here is original.