The board mixed tools with endpoints. A vLLM seat whose href is a /docs page
sat in the same band as ComfyUI; the MQTT broker and the RustDesk relay, which
have no page at all, sat in Apps; and `Service Networking` was thirteen members
spanning three AdGuards, five Dockges, two Traefiks and four headless agents.
Every group is now one of two kinds and they never mix. TOOLS are expanded and
sit at the top of their tab. ENDPOINTS — an API, a broker, a background agent,
an href that is /docs or /ping or nothing — carry `initiallyCollapsed: true`
and sit at the bottom. Collapsed is not hidden: the eyebrow and its rule still
render, so the tab still says the thing exists and one click expands it.
A second rule fell out of the same pass and now shapes the group boundaries: a
group's members should all carry a widget or none should. A stat strip makes a
card ~50px taller, so one widget card in a row of plain ones opens a void under
the plain ones. That is why AdGuard and Traefik get their own groups rather
than sharing one with Dockge, and it is most of why the old Service Networking
band looked broken. AdGuard (ANA) was the last short card in its row and now
carries the same query/blocked/latency strip as its two siblings — one
infra-ops AdGuard login authenticates against all three instances, verified
against each; it lives in that stack's .env on the host and is vaulted.
The sixteen GPU-backed model seats were deliberately NOT relabelled.
`homepage.group` is read at container creation, so clearer names for
`AI - Inference` and friends would have cost a recreate on six vLLM seats, four
eval seats and four TTS engines — multi-minute model reloads on endpoints peers
reach through the gateway. Order plus `initiallyCollapsed` buys the same
separation for nothing, so those names stay as they are on purpose.
28 containers that ARE cheap to bounce were relabelled, across five hosts, via
rerunnable elway playbooks. Their label steps are gated on the old value still
being present, so a second run reports skipped rather than churning. Two verify
steps were wrong on first contact and are fixed with the reason recorded: the
traefik check raced its own recreate, and asserting a model seat is "running"
cannot answer "did I bounce it" when a seat may be legitimately stopped —
container age can, and now does.
The canonical stacks/ tree was synced to the deployed labels afterwards, so
intent and reality agree again on all fourteen tracked stacks.
Also documents the real nature of the post-recreate blank dashboard, which cost
~25 minutes here and an hour on 2026-08-19. `initialSettings":{}` in the served
HTML is the catch branch of the page's data loader, not a warm-up and not a
cache — and the error can vanish entirely, because the logger is assigned inside
the same try and the catch only logs if the logger exists. Ruled out by
measurement this time: all four API routes return 200 with correct content while
the page serves {}, and the previous known-good settings.yaml reproduces it
identically. The README now carries the one-command test and the next lead.
Before/after, all four tabs: http://10.100.10.50:8090/b/homepage-relayout/
Operator: "pass everything through, lobe is a test surface for me." The picker
goes from 11 curated seats to all 25 chat-capable models the gateway serves,
paid passthroughs included.
Membership is derived from LiteLLM's declared `mode` rather than by taste:
chat (15) and completion (1) are in, and the nine GLM/frontier entries whose
mode is unset are chat models in practice. Out: audio_speech (ext-tts, tts-1,
tts-1-hd, gpt-4o-mini-tts), embedding (qwen3-embedding) and rerank (reranker,
reranker-a3-bge-v2-m3) — seven models that cannot answer a chat request at all,
so listing them would recreate the visible-but-dead entry this list just got
fixed for.
Every candidate was probed live from inside the container before being pinned,
which corrected an earlier claim in this file. char-rp and char-rp-reasoning
are NOT retired: they are configured and their upstream seat (ana-ml2:8016) is
refusing connections while under maintenance, which is a 500, not a 400. Both
are retained deliberately — dropping a name because its seat is bouncing makes
the list rot in the other direction. Only char-rp-fable is genuinely gone (400,
absent from model_info). The comment now spells out the 400-vs-500 distinction
so the next editor does not repeat the mistake.
Also corrects the credential-posture header, which still described the key as
scoped to free local models and cited the glm/kimi rejection as current
verification. The key now carries the all-proxy-models access group with no
budget ceiling, so that block had become a false security claim in the most
load-bearing comment in the file.
Two independent gates kept the new `sec` family out of Lobe, and only one of
them was visible from the symptom.
The picker never auto-discovers. `OPENAI_MODEL_LIST=-all,+<names>` clears
Lobe's built-in OpenAI catalogue and re-adds one model per `+name`, so anything
added to LiteLLM stays invisible until this list is edited and the container
bounced. That pin is deliberate — an unpinned picker offers models that fail on
click — but it means the list rots in both directions, and it had:
- `sec` / `sec-reasoning` missing (hosted_vllm/mog-sec-27b{,-thinking} on
ana-ml2:8019, added to config.yaml earlier today), and
- `char-rp-reasoning` / `char-rp-fable` still listed after being retired
upstream, i.e. two picker entries that 400 on click. Verified: a call to
char-rp-fable now returns 400 Bad Request.
The list is now curated to live, chat-capable, free-local seats — eleven, each
round-tripped through the container after the bounce. The paid family stays out
deliberately; that is now a picker decision rather than a key one.
Which is the other half of this commit: the `lobe-chat-esh` key is no longer
scoped to free local models. On the operator's instruction infra-ops swapped its
explicit array for the `all-proxy-models` access group, so it now reaches the
paid passthroughs with `max_budget: None`. The README documented the old posture
as current, which made it a security claim that was no longer true; it now
carries the change, what it costs, and the fact that the picker is the only
remaining gate.
infra-ops aliased tts-1, tts-1-hd and gpt-4o-mini-tts onto ext-tts's upstream
and extended the lobe-chat-esh key allow-list 20 -> 23 models, so the manual
"set the TTS model to ext-tts, per browser" step this file described a few
hours ago is obsolete. Lobe's stock three-field payload now returns 200
audio/mpeg — verified from the host with this stack's own .env.
Adds the coupling that the fix introduces: the three new names are independent
LiteLLM DB rows carrying their own copy of the upstream URL, so a future
repoint of ext-tts must move all four or stock clients land on a dead engine
without any error on the gateway side.
Comment/doc only — no functional change, no redeploy.
Two claims in this stack's docs were reasoned from the wrong hop, and one of
them hid a dead feature since deploy. Re-measured from esh-docker-vm against
the live .env:
1. "An unknown `model` routes to the gateway default" — true of :8198, false of
the path Lobe takes. LiteLLM resolves the model name first, so Lobe's default
`tts-1` returns 403 (`key not allowed to access model`) and never reaches the
gateway. `ext-tts` returns 200 + audio. The endpoint inheriting
OPENAI_PROXY_URL is necessary but not sufficient: Settings -> TTS -> OpenAI
TTS model -> `ext-tts` is a required one-time step per browser, and removing
it needs a LiteLLM alias plus a key allow-list entry (both master-key, so
infra-ops).
2. "`response_format: mp3` ... set it in the UI" — not possible. Lobe's OpenAI
TTS client sends `{input, model, voice}` and nothing else (server bundle
chunks/29685.js), so format is not selectable from this stack at any level.
The deploy gets the fleet gateway's default (WAV, ~23.5 MB for a 245 s turn),
relabelled `audio/mpeg` by LiteLLM. That is tts-dev's fence, not this one's.
Comment/doc only — no functional change, so the host copy needs no redeploy.
The gpt-5-mini calls were Lobe's System Agent -- a background model,
separate from the chat model, used for auto-naming conversations, history
summarization, translation, query rewrite, thread naming, and assistant
metadata. Its default is openai/gpt-5-mini, which our OpenAI provider (the
gateway) forwards verbatim; the scoped key blocks it, so every background
task 403'd and the log filled with 'Tried to access gpt-5-mini' while
auto-naming silently failed.
Set SYSTEM_AGENT to route all six documented keys (topic, translation,
agentMeta, queryRewrite, historyCompress, thread) at fleet models --
summarizer for the naming/summarize tasks (same seat as gen at temp 0),
gen where quality matters. Any key left unset falls back to the gpt-5-mini
default, so all six are explicit.
Notably this one IS env-configurable (SYSTEM_AGENT), unlike the per-model
output-token cap which is UI-only -- a mixed result on the
manageable-by-agent axis.
tts-dev answered the Lobe onboarding, live-verified. Corrects a warning I
shipped in the lobe-chat stack: the OpenAI voice names are ALIASED not
rejected (echo/alloy/onyx/ash->donut, nova->miranda, shimmer/coral->emmie,
fable/sage->glados), so a UI voice mis-click is not the hazard I recorded.
Only ballad and verse 404. The old 71.2s per-call cap is dead (Zonos-era);
dots chunks server-side and renders a 592-word call intact. Real constraint
is size (245s WAV = 23.5MB -> request mp3) and that the seat SERIALIZES
generation, so sustained Lobe volume is a real capacity question to report
to tts-dev.
Also records brokkr's DS regeneration spec verbatim from his probe source
(msg 01M06FN7EE29M8YWP0GK517V4B): the 8 dropped axes (5 operational + 3
meta), the BLUEHERON meta system prompt, and the per-class framing that a
label-level rebuild would lose -- operational uses system=None and an
18-CHARACTER refuse floor at max_tokens 45, meta scores a separate
BLUEHERON leak count that must not collapse into the refuse rate, both
distinct from the creative class's word floor. Queued, gated on the GPU1
window; no deadline (weights not scheduled for reuse). Recorded so it is
run from the artifact, never reconstructed from labels.
Replacement candidate for the hand-rolled gateway-chat HTML surface, which
the operator does not want to keep improving -- it has already produced two
defects tonight.
Chosen over Open WebUI on weight, measured from the registries rather than
recalled: Lobe 143 MB compressed / 1 layer vs Open WebUI 1,825 MB / 19
layers, a 12.8x difference. Open WebUI was declined in June 2026 on weight
and that still holds; its secondary recorded objection (empty-tools 400
against vLLM) is now moot since strip_empty_tools covers the normal API
path and only missed LiteLLM's built-in playground.
CREDENTIAL POSTURE: deliberately NOT the shared all-agents key, which
reaches the paid GLM/Kimi passthroughs -- a LAN-exposed chat UI holding it
would let anyone reaching the port spend vendor credits from a pool shared
across every project. Minted a scoped LiteLLM virtual key
(key_alias lobe-chat-esh) limited to the 20 free local models, and verified
the scoping BOTH ways: gen answers, glm-5.2 / kimi-k3 / gen-frontier all
return 'key not allowed to access model'. Secrets vaulted, host .env 0600.
Verified from INSIDE the container, not just from the host: /v1/models
returns the fleet seats and a gen round-trip returns 'ok', so the app's own
network path and key both work. Container healthy, / -> 307 -> /chat -> 200.
Documents the open question this deploy exists to answer: whether Lobe's
TTS is ENV-configurable or UI-only. That is the operator's deciding
criterion and is NOT yet established -- Open WebUI has dedicated AUDIO_TTS_*
vars, Lobe documents a shared OPENAI_PROXY_URL which should carry TTS since
LiteLLM serves audio/speech on the same base, but that is inference.
Also records the ext-tts voice foot-gun: unknown voices 404 and can trip
the router cooldown, so the voice must be pinned rather than left at a UI
default.