Files
esh-pfi-infrastructure/stacks/lobe-chat/compose.yaml
T
vh e9362de065 feat(lobe-chat): stand up Lobe Chat on esh-docker-vm over the LiteLLM gateway
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.
2026-08-16 16:42:05 -07:00

76 lines
3.6 KiB
YAML

# lobe-chat — evaluation chat frontend over the LiteLLM gateway, on esh-docker-vm.
#
# WHY THIS EXISTS
# `gateway-chat` is a hand-rolled single-file HTML surface the operator does not
# want to keep improving; it has already cost two defects (a 1024 max_tokens
# default that read as model degeneracy, and a NaN->null max_tokens bug).
# Lobe was chosen over Open WebUI on weight, measured from the registries
# rather than from marketing: 143 MB compressed / 1 layer, against Open WebUI's
# 1,825 MB / 19 layers. 12.8x.
#
# OPEN QUESTION THIS DEPLOY EXISTS TO ANSWER
# Whether Lobe's TTS base URL / model / voice are settable by ENV or only via
# the settings UI. Open WebUI has dedicated AUDIO_TTS_* vars; Lobe documents a
# SHARED OPENAI_PROXY_URL, which should work because LiteLLM serves both
# /v1/chat/completions and /v1/audio/speech on one base -- but "should" is not
# "does". If it turns out UI-only it fails the operator's manageable-by-agent
# criterion even while winning on weight.
#
# ⚠️ CREDENTIAL POSTURE — deliberately NOT the shared all-agents key.
# That key reaches the PAID passthroughs (GLM, Kimi) and a LAN-exposed chat UI
# holding it would let anyone who can reach the port spend vendor credits on a
# pool shared across every project. This stack uses a purpose-minted LiteLLM
# virtual key (`key_alias: lobe-chat-esh`) scoped to the 20 free LOCAL models.
# Verified at mint time: `gen` answers; `glm-5.2`, `kimi-k3` and `gen-frontier`
# all return "key not allowed to access model".
# Secrets live in the vault, never in git:
# secret get esh-docker-vm/lobe-chat-litellm-key
# secret get esh-docker-vm/lobe-chat-access-code
# secret get esh-docker-vm/lobe-chat-key-vaults-secret
#
# ⚠️ ext-tts FOOT-GUN: an unknown voice 404s and can trip the LiteLLM router
# cooldown. `ext-tts` accepts donut/emmie/glados/miranda (+ emotion variants)
# and the OpenAI aliases nova/alloy. If Lobe sends any other OpenAI voice name
# (echo, fable, onyx, shimmer) it will 404 -- pin the voice rather than leaving
# it at whatever the UI defaults to.
name: lobe-chat
services:
lobe-chat:
image: lobehub/lobe-chat:${LOBE_TAG:-latest}
container_name: ${LOBE_CONTAINER:-lobe-chat}
restart: unless-stopped
ports:
- "${LOBE_PORT:-3210}:3210"
environment:
# Chat + TTS both resolve through the one LiteLLM base.
- OPENAI_API_KEY=${OPENAI_API_KEY:?set in .env, from the vault}
- OPENAI_PROXY_URL=${OPENAI_PROXY_URL:-http://10.250.50.70:4000/v1}
# Pin the visible model list so the picker shows fleet seats rather than
# Lobe's built-in OpenAI catalogue (which would 404 against the gateway).
- OPENAI_MODEL_LIST=${OPENAI_MODEL_LIST:--all,+gen,+gen-reasoning,+char-rp,+char-rp-reasoning,+char-rp-fable,+summarizer,+summarizer-large,+classifier,+image-judge,+coder-fast,+chat-judge}
# LAN-exposed on a home-lab segment with no auth in front: gate it.
- ACCESS_CODE=${ACCESS_CODE:?set in .env, from the vault}
# Encrypts any provider keys a user stores in the UI.
- KEY_VAULTS_SECRET=${KEY_VAULTS_SECRET:?set in .env, from the vault}
healthcheck:
test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:3210/ >/dev/null 2>&1 || exit 1"]
interval: 30s
timeout: 10s
retries: 3
start_period: 60s
networks:
- tnet
labels:
- homepage.group=Apps
- homepage.name=Lobe Chat
- homepage.icon=mdi-chat-processing
- homepage.description=Chat frontend over the LiteLLM gateway (eval)
- homepage.href=http://10.0.50.45:${LOBE_PORT:-3210}
networks:
tnet:
name: traefik-net
external: true