Files
Hermes Agent b0d790be34 Add all 104 active skills from all 16 Hermes profiles
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
2026-07-04 11:44:04 -05:00

297 lines
14 KiB
Markdown

---
name: open-webui-admin
description: Programmatic administration of Open WebUI via REST API and direct SQLite DB access — audio config, LLM connections, groups, users, model access grants, and presets.
version: 1.0.0
author: Hermes Agent
license: MIT
platforms: [linux]
metadata:
hermes:
tags: [open-webui, admin, rest-api, sqlite, automation]
related_skills: [hermes-api-server, ask-hermes]
---
# 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`, `jq` on the agent host
## Core Workflow
### 1. Get Admin Token
```bash
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.
```bash
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).
```bash
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:
```json
{
"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:
```bash
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:**
```bash
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:**
```bash
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` — NOT `type`
- `principal_id` — NOT `id` or `group_id`
- `permission` — NOT `access` or `read_write`
**Correct payload:**
```json
{
"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):**
1. API read-back: `GET /api/v1/models/model?id=<url-encoded-model-id>``access_grants` must be a non-empty list
2. Direct DB (if available): `SELECT * FROM access_grant WHERE resource_id = '<model-id>';` → must return rows
### 7. Presets
**Create:**
```bash
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):**
```bash
# 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:
1. `GET /api/v1/models/model?id=<each-preset>` and check `meta.tts.voice`
2. Update any preset voices to valid names for the new backend
3. 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:
```bash
# DB location (Open WebUI 0.10.2, systemd root service):
/root/.open-webui/webui.db
```
**Key tables:**
- `user` — id, name, email, role, active
- `group` — id, name, description, user_id (creator)
- `group_member` — id, group_id, user_id
- `model` — 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:**
```bash
# 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:
```sql
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:
1. Each profile gets its own LLM connection (different port, different API key)
2. Each user group gets its own model access grants — only their profile's base model + preset
3. **Never cross-grant:** Operators group must NOT have access to the Kids' base model, and vice versa
4. Verify isolation: `GET /api/v1/models/model?id=<other-groups-model>` should return 404 or empty access_grants for the wrong group
## Pitfalls
1. **Audio config requires ALL fields** — missing provider fields → 422. Set unused ones to `""`.
2. **LLM connections use `/openai/config/update`** — NOT `/api/v1/configs/connections`. The latter only has feature toggles.
3. **access_grants silently drop wrong field names** — HTTP 200, grants discarded. Always verify via DB or API read-back.
4. **Group deletion doesn't cascade**`group_member` rows survive. Clean up manually if scripts re-run.
5. **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.
6. **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 a `token` field — the new user's JWT, useful for immediate verification of their model-list view without a separate signin.
7. **`/api/v1/users/all` returns a wrapped object, not a bare array.** The response shape is `{"users": [...]}`, not `[...]`. Parse with `d.get("users", [])` before iterating.
8. **`access_grants` in the create body can be silently dropped** — same silent-drop behavior as the access/update endpoint. If you pass `access_grants` in the `POST /api/v1/models/create` body 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 separate `POST /api/v1/models/model/access/update` call, then verify.
9. **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` → enumerate `OPENAI_API_BASE_URLS` → use the discovered index. Peer-validated 2026-07-01 during openg runbook review.
10. **TTS backend switch does NOT cascade to preset voice overrides.** When changing the global TTS engine, each preset's `meta.tts.voice` must be individually checked and updated to a voice that exists in the new backend. A stale voice override silently breaks TTS for that preset. See `references/orpheus-tts-integration.md` for 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_grants` non-empty AND `SELECT * FROM access_grant WHERE resource_id = '<id>'` returns rows
- [ ] Preset: `GET /api/v1/models/model?id=<preset-id>``base_model_id` correct, `meta.tts.voice` correct
- [ ] E2E: chat completion through the preset returns correct model id and expected behavior