A column rather than a key in capabilities_json, which is rebuilt wholesale
from the submitted checkboxes on every save and would destroy a number
living in it.
0 means unknown, and unknown has to stay tellable from small: the context
percentage and automatic compaction both refuse to act on a figure nobody
supplied. Filled in from /v1/models where the runner advertises it --
OpenRouter, vLLM and llama.cpp each spell it differently, so context_from()
reads the four spellings actually in use, accepts a quoted number but not
"8192 tokens", and rejects anything outside 256..10,000,000. Applied on
discovery only when nothing is set: a refresh must never undo a correction,
since an administrator sets this precisely because the endpoint was wrong.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The brand accent was rune gold. It is now mallorn green: --gold becomes
--leaf in tokens.css and everywhere it resolved, and the mark's wafer is
the green of the leaves lembas travels wrapped in rather than the biscuit
inside. Yellow is left to mean exactly one thing in the interface -- a
warning -- instead of two.
Two knock-on choices the rename forced:
- Code keywords move from the brand accent to --warning. Strings are
--success, which is green; keywords in leaf green beside them is not
a colour scheme.
- --success itself leans teal now. Two greens a hue apart read as one
colour rendered badly, and an unread dot has to be tellable from a
brand badge at a glance.
The mark is redrawn, not just recoloured. The blade is ovate -- widest a
third up from the base, rounded where the stem meets it, drawn out only
at the tip -- because the old one was pointed at both ends and read as an
eye. It is also much larger relative to the tile: at 16px the silhouette
is all that survives, and a small leaf on a large tile is a green square
with a smudge on it. Veins sweep towards the tip and shorten as the blade
narrows. The score cross is thinner and fainter so it stays texture.
A single diagonal score was tried first and rejected: behind a diagonal
leaf it does not read as scoring, it reads as a line struck through the
mark.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Four places a model can reach for, differing in who writes a record and how it
gets in front of the model.
**Knowledge** is uploaded by a person and searched by the model. It goes through
`services/files.py:prepare` — the same pipeline as a chat attachment — so the
same PDF produces the same text whichever way it arrived, and `Document` carries
the same content columns as `Attachment` for the same reason.
**Notes** are written by the model and edited by you. Too long to inject, so
they are searched.
**Memory** is short facts, and every one of them goes into every request. That
single decision is where the rest of its design comes from: records are capped
short, the block has a budget, there is no search tool because the model is
already looking at them, and they are not shareable — a record about a person is
not content to hand round.
**Skills** are saved procedures. Only the name and description are injected; the
body is fetched when the model decides one applies, which is what makes a
hundred skills affordable. A model may write and revise its own — the safety
story is not a gate but a record: every revision is kept, attributed and
revertible. A model that has just read a hostile page can save a skill that
outlives the conversation, and the honest mitigation is that it is visible and
undoable rather than that it was prevented.
**The harness** is why any of it gets used. A model handed a tools array
ignores it and answers from recall, because nothing in the request suggests
otherwise. `services/harness.py` assembles a preamble from what this chat
actually has: when to reach for each tool, the memories, the skill index.
This is an exception to "system prompts are precedence, not concatenation", and
a deliberate one. That rule governs the three *authored* layers and is
untouched — exactly one still wins. The harness is a different axis: it
describes the machinery rather than the behaviour, nobody authored it, and there
is nothing for it to disagree with. It is prepended to whichever authored prompt
won, in one system message, since several endpoints reject a second.
Supporting changes:
- **Sharing**, in one helper. `visible_to()` is the only definition of who can
see a library item and every listing and tool goes through it. Sharing grants
*reading*; two people editing one note with no history and no merge is worse
than copying it. **Administrators do not bypass this** — they bypass
permissions elsewhere because an admin can grant themselves those anyway, but
reading somebody's private notes is a different act.
- **FTS5**, created by `db/migrations.py:ensure_fts` with the triggers an
external-content index needs. Idempotent, like the column sync beside it.
Terms are ANDed and then ORed: the caller is usually a model writing a whole
question, and requiring every word loses the match on one absent term.
- **The attach button is a menu** — file, image, a web page, or a document from
the library. Attaching a document copies it, because history must not change
when a document is edited later.
- **A URL fetcher with an SSRF guard.** This server can reach the router, the
other services on the box and LLeMbas itself, and the address can come from a
model. Private ranges are refused *after resolution* and redirects are followed
by hand so every hop is checked. An admin can open it deliberately.
- **Model capabilities split** into protocol support and a toggle per built-in
tool. Rows predating the split have no `tool_*` keys, and absent counts as on
when `tools` is on — otherwise an upgrade silently takes web search away from
every model already configured for it.
Also fixes the test fixture, which built the schema with `create_all` and so ran
against a database without the FTS tables production has; it now runs
`sync_schema`, the same path startup takes.
430 tests, ruff clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
/admin/models rendered a full edit form for every model. With eight that
was merely long; with a hundred it was unusable, which is the report.
The list is now compact rows only -- avatar, name, badges, position,
reorder buttons, Edit link -- with search across id and display name,
filter tabs (All / Enabled / Disabled / Pinned / Restricted, each with a
count), a connection filter, and pagination at 40. Filters are links, so
a filtered view is a real URL you can keep. Editing moved to
/admin/models/{id}/edit, one model per page, with Previous/Next links so
a freshly imported connection can be tidied without returning to the
list each time.
Measured with 128 models: the list is 73 KB showing 40 rows over 4
pages, and a detail page is 17 KB. The old page would have rendered all
128 forms into one response.
Reordering needed rethinking at that size too. Up/down is fine for
nudging a model one place but hopeless for moving it sixty, so the
detail page has a position field you type into; the value is clamped and
a non-numeric one is ignored rather than throwing. The move buttons take
a `back` field so they return to whatever filtered, paginated view they
were pressed on instead of dumping you at page 1.
Also adds a select-all checkbox for the bulk bar, scoped to a container
selector rather than the page so a future list can carry more than one.
212 tests, ruff clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>