Files
hermes-skills/open-webui-admin/SKILL.md
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

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
linux
hermes
tags related_skills
open-webui
admin
rest-api
sqlite
automation
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

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 — NOT type
  • principal_id — NOT id or group_id
  • permission — NOT access or read_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):

  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:

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:

  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:

# 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:

# 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:

  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 cascadegroup_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