12 unversioned skills now versioned at 1.0.0: agent-communication, ascii-video, external-reasoning-augmentation, jotty-notes-api, minecraft-modpack-server, obsidian, pokemon-player, powerpoint, social-search, songwriting-and-ai-music, workspace-context-organization, youtube-content Total repo: 141 skills across all profile scopes
14 KiB
name, description, version, author, license, platforms, metadata
| name | description | version | author | license | platforms | metadata | ||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| open-webui-admin | Programmatic administration of Open WebUI via REST API and direct SQLite DB access — audio config, LLM connections, groups, users, model access grants, and presets. | 1.0.0 | Hermes Agent | MIT |
|
|
open-webui-admin — Programmatic Open WebUI Administration
Overview
Administer Open WebUI entirely from the terminal via its REST API and direct SQLite DB access. No browser clicks needed. Covers audio config, LLM connections, group/user management, model access grants, and preset creation. Used when setting up a new Open WebUI instance or adding profiles/users/groups to an existing one.
When to Use
- Setting up Open WebUI for the first time (audio, connections, groups, users, presets)
- Adding a new Hermes profile as an LLM connection
- Creating groups and assigning users to them
- Setting model visibility (access grants) for group-based bank isolation
- Debugging why a model isn't visible to a user
- Any Open WebUI admin task the operator wants done without clicking through the UI
Prerequisites
- Open WebUI reachable at its HTTP port (default 8080)
- Admin credentials (email + password)
- SSH access to the Open WebUI host for direct DB fallback (optional but recommended)
curl,python3,jqon the agent host
Core Workflow
1. Get Admin Token
TOKEN=$(curl -s -X POST -H "Content-Type: application/json" \
-d '{"email":"<admin-email>","password":"<admin-password>"}' \
http://<host>:8080/api/v1/auths/signin | python3 -c 'import json,sys; print(json.load(sys.stdin)["token"])')
All subsequent calls use -H "Authorization: Bearer $TOKEN".
2. Audio Configuration
Endpoint: POST /api/v1/audio/config/update
Critical pitfall: The endpoint requires ALL provider fields, even unused ones. Missing fields cause a 422 validation error. Set unused providers to empty strings.
Field names are UPPERCASE (as of v0.10.2). The API rejects lowercase aliases like engine/model/url with 422 "Field required" on every uppercase field. Use the exact field names returned by GET /api/v1/audio/config. Always GET first and mirror that schema.
curl -s -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{
"tts": {
"OPENAI_API_BASE_URL": "http://10.0.0.16:8880/v1",
"OPENAI_API_KEY": "not-needed",
"OPENAI_PARAMS": null,
"API_KEY": "not-needed",
"ENGINE": "openai",
"MODEL": "kokoro",
"VOICE": "af_heart",
"SPLIT_ON": "punctuation",
"AZURE_SPEECH_REGION": "",
"AZURE_SPEECH_BASE_URL": "",
"AZURE_SPEECH_OUTPUT_FORMAT": "audio-24khz-160kbitrate-mono-mp3",
"MISTRAL_API_KEY": "",
"MISTRAL_API_BASE_URL": "https://api.mistral.ai/v1"
},
"stt": {
"OPENAI_API_BASE_URL": "http://10.0.0.42:9000/v1",
"OPENAI_API_KEY": "not-needed",
"OPENAI_API_REQUEST_FORMAT": "multipart",
"ENGINE": "openai",
"MODEL": "base",
"SUPPORTED_CONTENT_TYPES": [],
"ALLOWED_EXTENSIONS": ["mp3","wav","m4a","webm","ogg","flac","mp4","mpga","mpeg"],
"WHISPER_MODEL": "base",
"DEEPGRAM_API_KEY": "",
"AZURE_API_KEY": "",
"AZURE_REGION": "",
"AZURE_LOCALES": "",
"AZURE_BASE_URL": "",
"AZURE_MAX_SPEAKERS": "",
"MISTRAL_API_KEY": "",
"MISTRAL_API_BASE_URL": "https://api.mistral.ai/v1",
"MISTRAL_USE_CHAT_COMPLETIONS": false
}
}' \
http://<host>:8080/api/v1/audio/config/update
STT engine note: Use "openai" (not "web") to route to a local faster-whisper backend. Engine "web" uses the browser Web Speech API with no server-side transcription.
Verify: GET /api/v1/audio/config — check that stt.ENGINE is "openai" and stt.OPENAI_API_BASE_URL points at the local backend. For full E2E: POST /api/v1/audio/transcriptions with an audio file.
3. LLM Connections (OpenAI API)
Endpoint: POST /openai/config/update (NOT /api/v1/configs/connections — that endpoint only has ENABLE_DIRECT_CONNECTIONS and ENABLE_BASE_MODELS_CACHE toggles).
curl -s -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{
"urls": ["http://<hermes-host>:8653/v1"],
"keys": ["<open1-api-key>"],
"prefix_ids": [true]
}' \
http://<host>:8080/openai/config/update
Multiple connections: Add both open1 and openz in one call by extending the arrays:
{
"urls": ["http://<host>:8653/v1", "http://<host>:8654/v1"],
"keys": ["<open1-key>", "<openz-key>"],
"prefix_ids": [true, true]
}
Verify: GET /openai/config — check urls and keys arrays. Then POST /openai/verify to confirm reachability.
4. Sync Models from Connections
After adding LLM connections, sync to auto-create model rows:
curl -s -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{}' http://<host>:8080/api/v1/models/sync
This creates model rows for every model returned by /v1/models on each connection. The model id will match the model id from the gateway (e.g., Hermes Agent (open1)).
5. Groups
Create:
curl -s -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"name": "Operators", "description": "Operator group for Rob"}' \
http://<host>:8080/api/v1/groups/create
Returns the group object with id. Capture it.
List: GET /api/v1/groups/
Add user to group:
curl -s -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"group_id": "<group-id>", "user_id": "<user-id>"}' \
http://<host>:8080/api/v1/groups/id/<group-id>/update
Verify: GET /api/v1/groups/id/<group-id>/info — check member_count.
6. Model Access Grants (CRITICAL — read this section twice)
Endpoint: POST /api/v1/models/model/access/update
THE #1 PITFALL: The endpoint silently drops grants with wrong field names. It returns HTTP 200 either way. A "success" response with wrong field names means the grants were discarded — the model is effectively public.
Correct field names (verified by direct DB read + API read-back):
principal_type— NOTtypeprincipal_id— NOTidorgroup_idpermission— NOTaccessorread_write
Correct payload:
{
"id": "<model-id>",
"access_grants": [
{
"principal_type": "group",
"principal_id": "<group-id>",
"permission": "read"
}
]
}
Valid values:
principal_type:"group"or"user"permission:"read"or"write"(there is no"read_write"— add a second grant object for write)
Source: open_webui/models/access_grants.py normalize_access_grants() function — silently discards any grant dict where principal_type is not exactly "user" or "group", or permission is not exactly "read" or "write".
Verification (MANDATORY — do not trust the 200):
- API read-back:
GET /api/v1/models/model?id=<url-encoded-model-id>→access_grantsmust be a non-empty list - Direct DB (if available):
SELECT * FROM access_grant WHERE resource_id = '<model-id>';→ must return rows
7. Presets
Create:
curl -s -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{
"id": "hermes-operator",
"name": "Hermes-Operator",
"base_model_id": "Hermes Agent (open1)",
"meta": {
"profile_image_url": "/user.png",
"description": "Operator persona (Rob) — wraps the open1 Hermes profile.",
"capabilities": {"vision": false, "file_upload": true, "web_search": true, "tts": true, "stt": true},
"system": "",
"tts": {"voice": "af_heart"}
},
"params": {"temperature": 0.7}
}' \
http://<host>:8080/api/v1/models/create
Update (full replace):
# GET the current model, modify fields, POST back
curl -s -H "Authorization: Bearer $TOKEN" "http://<host>:8080/api/v1/models/model?id=<preset-id>" -o /tmp/preset.json
# Edit /tmp/preset.json (e.g., change meta.tts.voice), then:
curl -s -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d @/tmp/preset.json \
"http://<host>:8080/api/v1/models/model/update?id=<preset-id>"
After creating a preset, immediately set its access grants (same endpoint as step 6, with id = preset id).
CRITICAL — TTS backend switch pitfall: When changing the global TTS engine (e.g., Kokoro → Orpheus), the global audio config update does NOT cascade to preset-level meta.tts.voice overrides. Each preset with a meta.tts.voice field must be individually checked and updated to a voice that exists in the new backend. A preset with a stale voice override (e.g., af_heart pointing at Orpheus) will silently fail TTS — the API accepts the config but the voice name doesn't exist in the new engine. After any TTS backend switch, always:
GET /api/v1/models/model?id=<each-preset>and checkmeta.tts.voice- Update any preset voices to valid names for the new backend
- Verify with a read-back after update
8. Direct DB Access (Fallback)
When the REST API silently fails or you need to verify state, access the SQLite DB directly:
# DB location (Open WebUI 0.10.2, systemd root service):
/root/.open-webui/webui.db
Key tables:
user— id, name, email, role, activegroup— id, name, description, user_id (creator)group_member— id, group_id, user_idmodel— id, name, user_id, base_model_id, meta (JSON), params (JSON)access_grant— id, resource_type, resource_id, principal_type, principal_id, permission, created_at
Remote DB access via SSH + sudo:
# Write a one-line askpass script on the remote host:
echo '#!/bin/sh' > /tmp/askpass.sh
echo 'echo "passw0rd"' >> /tmp/askpass.sh
chmod 700 /tmp/askpass.sh
# Then use SUDO_ASKPASS for passwordless sudo in scripts:
SUDO_ASKPASS=/tmp/askpass.sh sudo -A python3 -c '...'
Orphan cleanup after failed runs: Deleting a group does NOT cascade-delete group_member rows. If a script errors and re-runs, check for orphan rows:
SELECT gm.* FROM group_member gm LEFT JOIN "group" g ON gm.group_id = g.id WHERE g.id IS NULL;
Bank Isolation Pattern
When multiple Hermes profiles serve different users through one Open WebUI instance:
- Each profile gets its own LLM connection (different port, different API key)
- Each user group gets its own model access grants — only their profile's base model + preset
- Never cross-grant: Operators group must NOT have access to the Kids' base model, and vice versa
- Verify isolation:
GET /api/v1/models/model?id=<other-groups-model>should return 404 or empty access_grants for the wrong group
Pitfalls
-
Audio config requires ALL fields — missing provider fields → 422. Set unused ones to
"". -
LLM connections use
/openai/config/update— NOT/api/v1/configs/connections. The latter only has feature toggles. -
access_grants silently drop wrong field names — HTTP 200, grants discarded. Always verify via DB or API read-back.
-
Group deletion doesn't cascade —
group_memberrows survive. Clean up manually if scripts re-run. -
Model sync creates rows for ALL connections — including ones you haven't set up yet. That's fine; just don't grant access to them until ready.
-
User creation endpoint is
POST /api/v1/auths/add, NOT/api/v1/users/add. The wrong endpoint returns HTTP 405 in Open WebUI 0.10.2. Verified live 2026-07-01 and peer-validated. Required body fields:name,email,password. Optional:role(default"pending"),profile_image_url(default"/user.png"). The response includes atokenfield — the new user's JWT, useful for immediate verification of their model-list view without a separate signin. -
/api/v1/users/allreturns a wrapped object, not a bare array. The response shape is{"users": [...]}, not[...]. Parse withd.get("users", [])before iterating. -
access_grantsin the create body can be silently dropped — same silent-drop behavior as the access/update endpoint. If you passaccess_grantsin thePOST /api/v1/models/createbody and the field names are wrong, the API returns HTTP 200 but the grants are discarded. Always verify via read-back or DB after creating a model with inline grants. Safer pattern: create without grants, then do a separatePOST /api/v1/models/model/access/updatecall, then verify. -
Never assume url_idx for multi-connection setups. When adding a third (or Nth) LLM connection, do not hardcode
GET /openai/models/2. The URL list order depends on insertion order, which may differ from the expected sequence. Always discover the index first:GET /openai/config→ enumerateOPENAI_API_BASE_URLS→ use the discovered index. Peer-validated 2026-07-01 during openg runbook review. -
TTS backend switch does NOT cascade to preset voice overrides. When changing the global TTS engine, each preset's
meta.tts.voicemust be individually checked and updated to a voice that exists in the new backend. A stale voice override silently breaks TTS for that preset. Seereferences/orpheus-tts-integration.mdfor the Orpheus-specific voice names and verification commands.
Verification Checklist
- Audio config:
GET /api/v1/audio/config→ TTS engine + STT engine match - LLM connections:
GET /openai/config→ urls/keys arrays correct,POST /openai/verify→ reachable - Groups:
GET /api/v1/groups/→ group exists,GET /api/v1/groups/id/<id>/info→ member_count correct - Model access:
GET /api/v1/models/model?id=<id>→access_grantsnon-empty ANDSELECT * FROM access_grant WHERE resource_id = '<id>'returns rows - Preset:
GET /api/v1/models/model?id=<preset-id>→base_model_idcorrect,meta.tts.voicecorrect - E2E: chat completion through the preset returns correct model id and expected behavior