`wilwheaton` kept failing in OpenWebUI with the 29-name Kokoro enum even after the dropdown was set to Default. The page's 2026-10-18 conclusion — "no user.settings row carries an audio override, so the choice lives in that browser's own storage" — was a probe-depth bug: the personal audio settings live in table `user`, and the 0.12.x front end nests them under `ui` (`settings.ui.audio.tts.engine`). The row did carry `browser-kokoro`, and the SPA re-applies the server copy on every load, so the UI fix never stuck. Corrected facts on the owner page: - where the override really lives, and the version-dependent nesting; - repair BOTH engine and voice: engine "" still sends the stored voice, so a Kokoro-era `voice: "alloy"` returns as frank-tts's own 400; - the one-line tells: an `/wasm/ort-wasm-simd-threaded.jsep.wasm` fetch means Kokoro.js is live in that browser, and uvicorn logs every request so a missing `/api/v1/audio/speech` line is evidence; - deployed build is 0.12.5 (pin `0b399a8b`, bumped 2026-10-30) — the page still claimed 0.11.4 — and the 401 paragraph's duplicated sentence is gone. Verified 2026-11-04: with the row repaired, `POST /api/v1/audio/speech` as a real user over the vhost returns 200 audio/mpeg and frank-tts logs `speech voice=wilwheaton`.
28 KiB
type, description, tags, timestamp
| type | description | tags | timestamp | ||
|---|---|---|---|---|---|
| reference | OpenWebUI is a self-hosted chat interface for LLMs. |
|
2026-09-21 |
OpenWebUI
OpenWebUI is a self-hosted chat interface for LLMs. It connects to OpenAI-compatible endpoints (vLLM, llama.cpp, Ollama) and adds RAG, plugins, tool calling, and multi-user support.
Host: Dankclaw — Port: 3005 (host) → 8080 (container) — Vhost:
ai.franksdank.cloud routes straight to 192.168.0.143:3005 (unguarded —
OpenWebUI enforces its own auth) — Boot-persistent: Quadlet — Docs:
https://github.com/open-webui/open-webui
Facts
| Item | Value |
|---|---|
| Image | ghcr.io/open-webui/open-webui:main (verified from the live quadlet unit) |
| Deployed build | 0.12.5 — GET http://127.0.0.1:3005/api/version → {"version":"0.12.5"} (re-measured 2026-11-04). The repo pin @sha256:0b399a8b… (dankclaw/open-webui/quadlet/open-webui.container, bumped 2026-10-30) is what makes that build reproducible; :main is a rolling tag, so re-measure /api/version instead of quoting a build from this page. Imagegen endpoint is POST /api/v1/images/generations (the comfyui engine is still upstream). |
| Port | 3005 (host) → 8080 (container). Port 3000 is Gitea's HTTP, so it is not usable. |
| Data volume | /home/dank/Containers/open-webui/data → /app/backend/data |
| Backends | The picker's content is webui.db state, not repo state: as recorded on 2026-10-07 it exposes one model, frank-zealot, with frank-dragoon/frank-archon configured but disabled — unverified 2026-10-18 (the DB is the authority and is not readable from the repo). What the repo does fix is the container's own OpenAI target: OPENAI_API_BASE_URL=http://192.168.0.143:7770 (dankclaw/open-webui/quadlet/open-webui.container:32, same value in the live container env) — see The LLM backends |
| Network | Pinned bridge systemd-open-webui (10.89.3.0/24, gateway 10.89.3.1) — see History |
| Persistence | Quadlet unit open-webui.service; [Install] WantedBy=default.target |
| Deploy | scripts/deploy-open-webui-quadlet.sh dankclaw |
The LLM backends
The picker is built from the model table in webui.db (one row per model)
plus the model_ids of each enabled OpenAI connection, NOT from live
connection discovery — so the picker shows exactly what is seeded there.
Since 2026-10-21 the picker exposes all three fleet models to every
account: frank-dragoon, frank-archon, frank-zealot (per Frank: open
frank-archon to everyone; the other two were already public). Proven with
a role=user token: /api/models lists all three ids and a chat turn to
frank-archon answers end-to-end.
Connection (openai.api_configs idx) |
openai.api_base_urls (live 2026-10-21) |
Enabled | model_ids |
Endpoint page |
|---|---|---|---|---|
| 0 | http://host.containers.internal:7770/v1 |
yes | frank-dragoon |
frank-dragoon |
| 1 | http://192.168.0.235:7770/v1 |
yes | frank-archon |
frank-archon |
| 2 | http://host.containers.internal:7771/v1 |
yes | frank-zealot |
frank-zealot |
The URLs in the first column are the DB's recorded values and predate the 2026-08-22
bridge move; on this network they are wrong — see the reachability gotcha below, which
is the corrected law. The picker row for frank-zealot was re-keyed on 2026-10-18 after
that day's payload swap (see frank-zealot for what was deployed and when).
The model table carries three decoration rows — frank-zealot,
frank-dragoon, frank-archon (200,000 context, added 2026-10-21).
Both the context length and the description live inside JSON columns —
params holds {"context": {"length": …}} and meta holds
{"description": …}; there is no context_length or description column, so
sqlite3 one-liners against those names fail. params.context.length is a copy
of the endpoint's per-slot window, so the authoritative number lives on the
endpoint page (frank-zealot, frank-dragoon) and the row must follow it —
the 2026-10-18 re-key existed because frank-dragoon's row still carried the
window its payload had before that day's swap. These rows are
live DB state, so a payload swap must re-key the database too, not only the
repo. There is no sqlite3 binary on Dankclaw — use python3 -c with the
stdlib sqlite3 module, and take a Connection.backup() snapshot first (the
DB is in WAL and the app is live).
A third gate sits after the table, and it is the one that bites family
accounts (2026-10-07): a model row is only visible to a non-admin user when
its access permission grants them. A seeded row defaults to admin-only,
so the picker renders "No results found" for every regular user even though
the connection is enabled, model_ids is correct, and the endpoint answers
/v1/models — and GET /api/v1/models returns [] for that user while an
admin token returns the model. Fix: set the row's access_control to public
(the access column on the model row) via the admin UI (Admin Panel →
Models → visibility) or the DB. The verification battery below must therefore
run as a non-admin user, not only as an admin.
Two gotchas make the wiring non-obvious:
- llama.cpp
/v1/modelsis Ollama-shaped. llama.cpp returns{"models":[{"name":...}]}; OpenWebUI'sextract_data()only reads the OpenAI{"data":[{"id":...}]}shape, so connection discovery silently misses the llama.cpp endpoints (frank-dragoon, frank-zealot). The fix is to setmodel_idsin each connection'sapi_config— when non-empty, OpenWebUI builds the model list directly and never fetches/v1/models. The oldfrank-medium(vLLM) endpoint — retired, no longer in the fleet — served the OpenAI shape and would have discovered fine, but usedmodel_idstoo. - The host's own services are reached via the host LAN IP, not
host.containers.internal. The container sits on the pinned kernel bridgesystemd-open-webui(10.89.3.0/24, gateway 10.89.3.1, container IP 10.89.3.2 —podman inspect open-webui2026-10-18), andhost.containers.internal(169.254.1.2) is a slirp4netns/pasta-only name that does not resolve on this bridge (dankclaw/open-webui/quadlet/open-webui.network:15-19). The deployed unit therefore points at the host LAN IP:OPENAI_API_BASE_URL=http://192.168.0.143:7770andSEARXNG_QUERY_URL=http://192.168.0.143:8081/search(open-webui.container:32,78, both present in the live container env, 2026-10-18). Those ports are published on all interfaces, so the LAN IP answers:ss -ltnshows*:7770and0.0.0.0:7771, andcurl http://192.168.0.143:7771/health→ 200 (2026-10-18). The Dankserver backend is a different host and is reached through its LAN IP192.168.0.235:7770either way.
The Ollama connection is disabled (ollama.enable=false).
All three endpoints are bare-metal systemd services managed by this repository; their shape, swaps and tuning live on frank-dragoon / frank-zealot / frank-archon. Disabling a connection here only hides it from this picker — the endpoint itself keeps serving every other client (the harness, Hermes, API consumers).
Container introspection (DB and backend python)
-
Working dir for imports:
/app/backend; package nameopen_webui(underscore), NOTopenwebui.os.environ["WEBUI_SECRET_KEY"]MUST be set BEFORE importing anyopen_webuimodule (import-time hard requirement) the secret lives at/app/backend/.webui_secret_key. -
The
sqlite3CLI is NOT in the container — use Python'ssqlite3module (podman exec open-webui python3 -c "import sqlite3; conn = sqlite3.connect('/app/backend/data/webui.db'); …"). For config keys use parameterized LIKE queries (avoids shell quoting). -
Backend python:
podman exec -w /app/backend open-webui python3 -c "…"—Models.get_all_models()is a coroutine (await it); model objects are pydanticModelModelinstances (attribute access, not.get()), with NOstatus/enabledattribute — useis_activefrom the DB. -
API auth for verification:
create_tokenlives inopen_webui.utils.auth(NOTopen_webui.utils.utils)get_verified_userrequires anidclaim (not justsub)SESSION_SECRET = WEBUI_SECRET_KEY. Mint an admin token inside the container:podman exec -i open-webui sh -c 'export WEBUI_SECRET_KEY=$(cat /app/backend/.webui_secret_key); \ python -c "from open_webui.utils.auth import create_token; \ print(create_token({\"id\":\"<UID>\",\"sub\":\"<UID>\",\"email\":\"…\",\"role\":\"admin\"}))"'(admin UID from the
usertable).
Verification battery
GET /api/v1/models/list?limit=100→ returns EXACTLY the intended models. Response key isitems, notdata.POST /api/chat/completionsper model with a trivial prompt ("Reply with exactly one word: pong") → each returns contentpong. Reasoning endpoints need a generous budget: thinking consumesmax_tokens, so a short one returns empty content +finish_reason: "length"(see frank-zealot);frank-zealotanswerspongat 1500.- A 500 "Server Connection Error" →
podman logs open-webuiforClientConnectorError: a network-reachability problem (host LAN IP vshost.containers.internal), not a model problem.
Durability: the model table + connections are DB-only (not git-managed) a fresh data volume loses them — including the public access grant above. For reproducibility, add an idempotent first-start seed script or document the setup in the service doc.
API access (auth-gated)
Every /api/* endpoint requires an authenticated session (SSO login JWT)
or a per-user API key (auth.enable_api_keys=true; keys are minted
account under Profile → API Keys). Anonymous web visitors get the pending
role (ui.default_user_role=pending), which cannot chat. Verified
2026-10-07 from loopback :3005 and over the public vhost:
GET /api/v1/models/list, GET /api/v1/models and
POST /api/chat/completions all return 401 with no key (also with a bogus bearer
token); only /api/config, /api/version and /health answer publicly, and none of
them expose the model list. An external program needs an account + API key — there is
no keyless access. Re-measured 2026-11-04 from loopback: /api/version → 0.12.5,
and /api/config publishes
features.enable_login_form=false, features.enable_signup=false and
oauth.providers.oidc="authentik" — the SSO-only posture, visible from outside.
Web search
Chat web search is served by SearXNG on this host, wired in the quadlet
(ENABLE_WEB_SEARCH, WEB_SEARCH_ENGINE=searxng, SEARXNG_QUERY_URL).
- Access path: the quadlet sets
SEARXNG_QUERY_URL=http://192.168.0.143:8081/search(dankclaw/open-webui/quadlet/open-webui.container:78, live in the container env) — gluetun's published port, NOT thesearch.franksdank.cloudvhost. The JSON format parameter is appended by OpenWebUI itself (unverified 2026-10-18: OpenWebUI-side behaviour; the env value carries noformat=json). The Anubis→Authentik guard chain is a browser mechanism (proof-of-work + session flow); a plain HTTP client cannot solve it, and the internal port bypasses Caddy entirely. - Limiter: SearXNG caps non-HTML (JSON) requests at 4/IP/hour (
API_MAXa hardcoded Python constant — no config knob). Thepass_iplist is checked FIRST and short-circuits the whole botdetection chain, and OpenWebUI's bridge egress (10.89.x) is inpass_ip, so the cap never applies. Verified: an 8-request burst from the container returned 8×200. - Config storage: OpenWebUI keeps web-search settings per-key in its SQLite
config table (
webui.db,web.search.*). The quadlet env vars seed that table on first start (fill-missing only); afterwards the DB is authoritative and Admin-UI edits persist there.
SSO (authentik)
Login is authentik via the OIDC authorization-code flow (wired 2026-09-24).
The vhost stays exempt from the Anubis guard chain: the OAuth dance is
browser-driven and only touches the two exempt vhosts (ai. and auth.).
- authentik objects (created via the admin API, 2026-09-24): application
open-webui(slugopen-webui) with the OAuth2 providerOpen WebUI(confidential client,authorization_code+refresh_token, Strict redirect URIhttps://ai.franksdank.cloud/oauth/oidc/callback, implicit consent flow). The client ID is in the quadlet; the client secret lives in the git-ignored host file/home/dank/Containers/open-webui/env(mode 600, loaded viaEnvironmentFile=). - Roles: the scope mapping
Open WebUI roles(scoperoles) reads the application entitlementsOpen WebUI Users→userandOpen WebUI Admins→admin. The entitlements are bound to the groupsopenwebui_users/openwebui_admins.akadmin(frank8465@protonmail.com) is inopenwebui_admins;qa-e2e-adminis inopenwebui_users. - Account merge:
OAUTH_MERGE_ACCOUNTS_BY_EMAIL=truefolds an SSO login into the existing local account with the same email — the pre-SSO admin (frank8465@protonmail.com) keeps its data.ENABLE_OAUTH_SIGNUP=truelets otheropenwebui_userscreate accounts on first login. - First login mints role
pending.ui.default_user_role=pending(config DB, verified 2026-10-09), so a freshly-provisionedopenwebui_usersaccount completes SSO and then cannot chat (picker empty) until its role is flipped:UPDATE "user" SET role='user' WHERE email='<user>@franksdank.cloud';— run with hostpython3over ssh against/home/dank/Containers/open-webui/data/webui.db(nosqlite3CLI on Dankclaw; table isuser, singular; noactivecolumn). No per-user model grant is needed: the picker modelfrank-zealotcarries the globalaccess_grantrow(model, frank-zealot, user, *, read), so anyrole='user'account sees it. Flip on FIRST-LOGIN SIGHTING — the row does not exist until the account signs in (2026-10-09: five provisioned accounts had no row). - IdP reachability: the container is on its own bridge (10.89.3.0/24) and
cannot reach
authentik-server:9000by name. It reaches the IdP over the public URLhttps://auth.franksdank.cloud— Caddy publishes0.0.0.0:443(netavark DNATs published ports from other containers) and Pi-hole resolves the name to the host LAN IP. The server-side code exchange rides that path. - Config authority: the OAuth env vars in the quadlet seed the per-key config DB on first run (fill-missing only); on the live data dir the Admin Panel is authoritative. To rotate the client secret: create a new token in authentik, update the host env file, and set the new value in the Admin Panel (or the DB), then restart the container.
- SSO-only (2026-09-24): after the OIDC round-trip was verified, the local
username/password path was retired.
ENABLE_LOGIN_FORM=falsehides the form andENABLE_PASSWORD_AUTH=falserefuses password logins, so the only way in is the authentik OIDC flow. Theui.enable_login_formconfig-DB override was flipped tofalsein the same change — the per-key DB wins over env, so both must agree, and the DB value is the one to change on a live data dir. To restore the password form: setui.enable_login_formback totruein the Admin Panel (or DB) and drop the two env vars from the quadlet.
Boot persistence (Quadlet)
The unit lives at dankclaw/open-webui/quadlet/open-webui.container. On the host it installs to ~/.config/containers/systemd/open-webui.container. The generator turns it into open-webui.service. Because the unit sets [Install] WantedBy=default.target and user linger is on, it starts at boot. systemctl --user is-enabled open-webui.service reports generated (the quadlet boot-start state).
Volumes
| Container path | Host path |
|---|---|
/app/backend/data |
/home/dank/Containers/open-webui/data |
The data dir holds the database, RAG indexes, and uploads. The deploy script creates it if missing and preserves it across reruns.
Healthcheck
The quadlet carries a built-in healthcheck (30s interval, 3 retries, 300s start period). It checks two things from inside the container:
- App liveness —
GET http://127.0.0.1:8080/healthreturns 200 ({"status":true}). - Network liveness — a non-loopback interface exists in
/sys/class/net. The dead-tap zombie state (History) leaves onlylobehind, and this is the check that catches it.
podman ps shows the container as unhealthy if either fails. Probe manually at http://dankclaw:3005/health.
Image generation (ComfyUI)
Image generation rides the local ComfyUI integration, not the chat
picker — ComfyUI's models never appear in the chat model picker (that is
chat LLMs only), so ComfyUI needs its own image-generation connection
configured in OpenWebUI. ComfyUI itself runs GPU-enabled on Dankclaw and
serves :8188.
Endpoint: POST /api/v1/images/generations — measured on the running build
(0.11.4): the legacy POST /api/images/generations answers 405 while the /v1
route answers 401 without a token, i.e. only the /v1 path exists (2026-10-18).
The comfyui engine is still upstream. Call it with Authorization: Bearer <token>
and body
{"model":"imagegen","prompt":"…","n":1,"size":"1024x1024"}; the response
carries base64 image data or a URL.
Failure modes: connection refused ⇒ OpenWebUI or ComfyUI is down; timeouts on large sizes ⇒ check ComfyUI resources; 401 ⇒ the token is bad.
Text-to-speech (frank-tts)
TTS is the fleet's one OpenAI-compatible speech endpoint. The config DB
audio.tts.* is set to engine openai, audio.tts.openai.api_base_url
http://192.168.0.143:7772/v1, audio.tts.voice wilwheaton (re-keyed
2026-10-20 from the retired narrator1; 7772 is a published port so
host.containers.internal also reaches it, but the LAN IP is the canonical
on-host form). That
port is the rootless quadlet frank-tts (this repo:
dankclaw/voiceservice/quadlet/frank-tts.container
scripts/frank-tts.env), and its shim resolves voice=<name> against
/home/dank/voices/<name>/reference.wav — a name without a reference is a
400 listing the voices that exist, never a silent fallback.
The 29-voice af_* … bm_fable list is NOT frank-tts. That enum belongs
to Kokoro.js running in the browser: the deployed front end ships an
in-page Kokoro implementation (frozen voice table plus _validate_voice,
model onnx-community/Kokoro-82M-v1.0-ONNX, and a personal Kokoro.js dtype
setting). Its engine select takes "" (Default) or browser-kokoro; with
browser-kokoro the page synthesizes locally and rejects any voice outside
its fixed pack, so wilwheaton fails with Voice: "wilwheaton" not found. Should be one of: af_heart, … without a single request leaving the
browser. Kokoro cannot clone — fixed pack only.
The switch is a DB row, not browser storage. Personal audio settings are
the settings JSON column of table user, and in 0.12.x they nest under
ui: ui.audio.tts.engine, .voice, .defaultVoice. In 0.11.x the same
keys sat at the top level (audio.tts.*), and the 2026-10-18 probe listed
only top-level keys, found no audio key, and concluded the choice lived in
that browser's own storage — a probe-depth bug. The row carried the override
the whole way; the 2026-10-30 pin bump to 0.12.5 moved the keys, not the bug.
The SPA reads that row at load (GET /api/v1/users/user/settings) and posts
the whole blob back whenever any preference changes
(POST …/settings/update, body {...settings}). So the dropdown can fix it —
but a second tab still holding the old copy writes browser-kokoro straight
back, which is the loop that makes the toast return after it was "fixed". The
user row is the authority here; the admin config table was never the problem
(its audio.tts.engine has been openai throughout). Repair
settings.ui.audio.tts with the image's stdlib sqlite3 (mode=ro; quote
identifiers, user and group are reserved words — there is no sqlite3 CLI
in the image or on the host), then reload every tab of that account.
Repair the engine and the voice. With engine "" the front end still
sends its stored voice in the request body, so a Kokoro-era voice/
defaultVoice survives the engine fix as frank-tts's own rejection —
measured 2026-11-04: voice: "alloy" → 400 unknown voice 'alloy'; available voices: dr.kern, garrickhagon, jeffhays, vaati, wilwheaton. Set
ui.audio.tts to {"engine": "", "voice": "wilwheaton", "defaultVoice": "wilwheaton"} and reload. Engine "" means "the admin config decides", and
the handler fills voice from audio.tts.voice only when the body omits it.
Two tells settle the case in one line:
- a session that fetched
/wasm/ort-wasm-simd-threaded.jsep.wasmloaded onnxruntime, so Kokoro.js is running in that browser — the server path never asks for that file; - a server-path failure leaves a uvicorn request line for
/api/v1/audio/speechand a frank-tts log line; abrowser-kokorofailure leaves neither. uvicorn logs every request, so silence is evidence.
Verified 2026-11-04: the handler's own outbound shape
({"model":"frank-tts","input":…,"voice":"wilwheaton"} with
Authorization: Bearer none, the configured audio.tts.openai.api_key) →
200 audio/wav — a body without response_format comes back WAV and
either MIME is browser-playable, so OpenWebUI accepts it. The full route as a
real user over the vhost, POST /api/v1/audio/speech → 200 audio/mpeg,
with frank-tts logging speech voice=wilwheaton.
Retired instances
- The compose
open-webuiservice (port 3000) was never run: 3000 is Gitea's HTTP port, so it could not bind. - The bare-container
open-webuion Dankserver (port 3000) pointed at a dead backend (:9090) and was removed. Its data dir is preserved on Dankserver in case it is needed. - The bare-container
searxngon Dankserver (port 8081) was removed. SearXNG now runs only on Dankclaw behind Caddy.
Gotchas
- The image tracks upstream
main; pin a digest in the quadlet unit if reproducible rollback matters more than tracking. (The old "CPU-only host" framing is stale: this APU runs the gfx1151 ROCm build — see ComfyUI.) - The default admin account is created on first login; secure it immediately.
- If the backend is loading, OpenWebUI shows connection errors until the endpoint is ready; each endpoint's boot budget is on its own page (frank-dragoon, frank-zealot, frank-archon).
- Repeated from The LLM backends: the on-host endpoints are reached via the host LAN IP, never
host.containers.internal.
History
-
2026-08-22 — slirp4netns → pinned bridge. The unit ran on
Network=slirp4netns. The slirp4netns helper process died (no OOM, no kill entry, no crash log found), and the container kept running with onlyloin its netns: published port 3005 accepted TCP and reset every request, so chat served empty responses for ~2 days. Nothing logged an error. Moving to a kernel bridge removes the per-container helper entirely, and the healthcheck's interface check makes the zombie state visible if it ever returns. -
2026-09-21 — model picker rebuilt for the 3-model fleet. The picker carried 39 stale rows (37 dead Ollama models + 2
frank-default-*) and was missingfrank-medium. Rebuilt themodeltable to exactlyfrank-dragoon/frank-medium/frank-zealotand setmodel_idson each connection (llama.cpp's Ollama-shaped/v1/modelsis invisible to OpenWebUI's discovery). Pointed the two on-host endpoints athost.containers.internal— the bridge refuses the host LAN IP for non-published ports. All three verified end-to-end via/openai/chat/completions. -
2026-09-24 — SSO via authentik. OIDC authorization-code flow wired per the SSO (authentik) section: application/provider/entitlements created in authentik, client secret in the host env file, roles from the
rolesscope claim. E2E coverage intests/e2e/specs/18-openwebui-sso.spec.ts. -
2026-09-24 — flipped to SSO-only. The OIDC round-trip verified in a real browser (login view renders no password form, only "Continue with authentik", and the flow lands on a signed-in session), so the local username/password path was retired:
ENABLE_LOGIN_FORM=false+ENABLE_PASSWORD_AUTH=falsein the quadlet andui.enable_login_formset tofalsein the config DB (the DB wins over env, so both were changed together).qa-e2e-adminwas demoted out of theauthentik Adminsgroup (it keepsopenwebui_users/homepage-adminsfor app access). -
2026-10-07 — picker narrowed to
frank-zealotonly. Per Frank: the only model accessible in OpenWebUI isfrank-zealot. Rebuilt themodeltable to a singlefrank-zealotrow (the old rowsfrank-dragoon/frank-archon/frank-smallare deleted), setopenai.api_configsindices 0 (frank-dragoon) and 1 (frank-archon) toenable: false, and retargeted index 2'smodel_idsfrom the stalefrank-smalllabel tofrank-zealot— llama.cpp serves by alias, so the old label addressed a model name the server does not know. Pre-change snapshot:/home/dank/Containers/open-webui/data/webui.db.bak-20261007(+-wal/-shm). Verified:/api/v1/models/listreturns exactly["frank-zealot"], chat completion answerspong(needsmax_tokens≥1500— the Qwen3.5 thinking block consumes a 200-token budget), and anonymous API probes are all 401 (see API access). -
2026-10-09 — re-keyed to the Protoss ids (wire rename). The endpoints began serving
frank-zealot/frank-dragoon/frank-archon, so three id-keyed objects had to move in the same window as the alias flip:model.id+name(frank-zealot→frank-zealot,params{"context":{"length":200704}}and themeta.descriptionkept)access_grant(resource_type='model',resource_id, principaluser/*, permissionread), andopenai.api_configsper-backendmodel_ids(index 2 =:7771is the enabled family backend; indices 0/1 =:7770and Dankserver:7770stayenable: false). Snapshot first:/home/dank/Containers/open-webui/data/webui.db.bak-20261009. Apply from inside the container (podman exec -i open-webui python3 -, sqlite3; the image has no sqlite3 binary) thensystemctl --user restart open-webui.service(it is a rootless quadlet) to drop the cached model list. Verify in a real browser as the family QA account: the welcome screen selectsfrank-zealotand answers. A stale id fails silently — the picker simply lists nothing, because OpenWebUI intersects the backend's/v1/modelswithmodel_ids.Law: the model id is a foreign key in three tables. Any future alias change on an endpoint OpenWebUI fronts must ship with this re-key, in the same deploy.
-
2026-10-09 — composer is
contenteditable, not<textarea>. A Playwright probe that waits fortextareaafter SSO times out on the welcome screen: OpenWebUI 0.6 renders the composer as a contenteditable div. The stable anchor is the model button (getByRole("button", { name: /selected model/i })) and the welcome text "How can I help you today?"; that snapshot is also the cheapest proof the picker is populated. -
2026-10-21 — picker opened to all three models, for everyone. Per Frank:
frank-archonaccessible to everyone. Three-leg repair onwebui.db(snapshotwebui.db.bak-20261021first): connection idx 1enable: true, amodelrowfrank-archon(params{"context":{"length":200000}}, meta description from the endpoint card), and anaccess_grantrow(resource_type='model', resource_id='frank-archon', principal_id='*', 'read')—principal_id='*'is the public grant, not a role row. Verified as a plainrole=usertoken:/api/modelslists all three ids, chat tofrank-archonanswers. Token-mint gotchas: the backend package imports only with cwd/app/backend, andWEBUI_SECRET_KEYmust come from PID 1's environ (podman execsessions do not inherit the app process env; the.webui_secret_keyfile happens to equal it — use it explicitly,podman exec -e).
See Also
- Dankclaw Stack
- Frank TTS — the speech endpoint behind Text-to-speech (frank-tts)
- Hermes — the agent OpenWebUI can talk to
- SearXNG — the search engine on this host
- frank-dragoon / frank-zealot / frank-archon — the endpoints behind the picker