# 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 — CHANGED 2026-08-21, read this before assuming. # This stack uses a purpose-minted LiteLLM virtual key (`key_alias: # lobe-chat-esh`), originally scoped to the free LOCAL models so a LAN-exposed # chat UI could not spend vendor credits. THAT IS NO LONGER TRUE: on the # operator's instruction the key was moved to the `all-proxy-models` access # group, so it now reaches EVERYTHING behind the gateway, paid passthroughs # (GLM, Kimi, gen-frontier) included, with `max_budget: None`. A paid call # from this UI spends real vendor credits and nothing caps it. The UI is # LAN-exposed behind ACCESS_CODE and nothing else. A ceiling is available from # infra-ops on request; uncapped is the operator's call, not an oversight. # The access group also auto-includes future models and only ever resolves # LIVE ones, so the key no longer needs editing when a seat is added. # 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 # # TTS (verified with tts-dev 2026-08-16): route via the `ext-tts` LiteLLM alias, # never direct to the seat -- engines get swapped behind the gateway and # direct-to-seat eats every change. Auth is the gateway key alone. # CORRECTION to an earlier note in this file: the OpenAI voice names are # ALIASED, not rejected (echo/alloy/onyx/ash->donut, nova->miranda, # shimmer/coral->emmie, fable/sage->glados), so a UI mis-click is NOT the # hazard I first recorded. Only `ballad` and `verse` 404 -- both aliased since. # MODEL NAME -- no longer a manual step (2026-08-17). Lobe defaults to `tts-1`, # and while :8198 ignores `model`, LITELLM IN FRONT OF IT DOES NOT -- it resolves # the name first, so `tts-1` used to 403 (`key not allowed to access model`) and # TTS did nothing from deploy until it was found. infra-ops then aliased `tts-1`, # `tts-1-hd` and `gpt-4o-mini-tts` to ext-tts's upstream AND added them to the # `lobe-chat-esh` key allow-list, so the stock payload works with ZERO settings. # Verified from this host with this .env: {"input":...,"model":"tts-1", # "voice":"alloy"} -> 200 audio/mpeg. Setting the UI field to `ext-tts` also # works and does no harm. # SIZE: Lobe sends only {input, model, voice} -- no `response_format`, no # `speed`, and no UI field for either -- so you get the gateway's default (WAV, # ~23.5 MB for a 245 s turn), relabelled `audio/mpeg` by LiteLLM. Not fixable # from this stack: it is a fleet-gateway default and tts-dev owns it. # The old 71.2s per-call cap is dead (that was Zonos-era); dots chunks # server-side and a 592-word call renders intact. # ⚠️ The seat SERIALIZES generation -- one render at a time, no continuous # batching -- so sustained volume from here is a real capacity question for a # shared GPU. Report ramp to tts-dev on althing. 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). # ⚠️ THIS NEVER AUTO-DISCOVERS. `-all` clears Lobe's catalogue and each # `+name` re-adds ONE model, so a seat added to LiteLLM stays invisible # here until this list is edited and the container bounced. That is the # price of the pin, paid deliberately: an unpinned picker offers Lobe's # built-in OpenAI catalogue, all of which fails against the gateway. # # 2026-08-21 (operator: "pass everything through, lobe is a test surface # for me"): the list is now EVERY chat-capable model the gateway serves, # paid passthroughs included. Membership is decided by LiteLLM's declared # `mode` from /v1/model_info, not by taste — chat + completion are in; # audio_speech (ext-tts, tts-1, tts-1-hd, gpt-4o-mini-tts), embedding # (qwen3-embedding) and rerank (reranker*) are out because they cannot # answer a chat request at all. Re-derive the list from `mode` when # editing rather than appending by hand. # # Distinguish the two failure shapes before removing a name: # 400 = not configured in LiteLLM at all -> genuinely gone, drop it # (char-rp-fable, 2026-08-21) # 500 = configured but its upstream seat is down -> KEEP it, or the list # rots every time a seat bounces (char-rp + char-rp-reasoning were # 500ing on 2026-08-21 with ana-ml2:8016 refusing connections; # seat under maintenance, both deliberately retained) - OPENAI_MODEL_LIST=${OPENAI_MODEL_LIST:--all,+gen,+gen-reasoning,+sec,+sec-reasoning,+char-rp,+char-rp-reasoning,+summarizer,+summarizer-large,+classifier,+chat-judge,+selene-1-mini-8b,+image-judge,+qwen-image-bench,+coder-fast,+gen-frontier,+gen-frontier-reasoning,+glm-5.2,+glm-5.2-reasoning,+glm-5.1,+glm-5.1-reasoning,+glm-5-turbo,+glm-4.7,+glm-4.5-air,+kimi-k3,+kimi-k3-gen-api} # SYSTEM AGENT — the background model Lobe uses for auto-naming # conversations (topic), history summarization, translation, query # rewrite, thread naming, and assistant-meta generation. ITS DEFAULT IS # `openai/gpt-5-mini`, which our OpenAI provider (= the gateway) forwards # verbatim; the scoped key does not grant gpt-5-mini, so every background # task 403s and the log fills with "Tried to access gpt-5-mini". Repoint # ALL keys at fleet models (any key left unset falls back to gpt-5-mini). # summarizer/classifier are the same seat as gen at temp 0 -- the right # fit for naming/summarizing; gen where output quality matters. - SYSTEM_AGENT=${SYSTEM_AGENT:-topic=openai/summarizer,translation=openai/gen,agentMeta=openai/gen,queryRewrite=openai/summarizer,historyCompress=openai/summarizer,thread=openai/summarizer} # 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=AI - Gateways & Chat - 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