diff --git a/docs/ATHENA_REBUILD_LOG.md b/docs/ATHENA_REBUILD_LOG.md index 601a419..25bde82 100644 --- a/docs/ATHENA_REBUILD_LOG.md +++ b/docs/ATHENA_REBUILD_LOG.md @@ -33,6 +33,7 @@ Chats oder privaten Nutzdaten. | Image-Backup | Das alte Archiv `openwebui-mcp-images.tar.gz` enthielt trotz seines Namens nicht das inventarisierte OpenWebUI-Image. | Restore nutzt als sicheren Fallback nur den gesicherten unveränderlichen Digest. Künftige Backups benötigen einen isolierten Probeimport. | | Wiederholung | Ein späterer Installerlauf hätte die restaurierte OpenWebUI-Version wieder überschrieben. | Das Restore schreibt den geprüften lokalen Image-Tag auch in die dauerhafte Installationskonfiguration. | | TinySearch-Volume | Das vom Installer vorbereitete Modellvolume wurde von Compose gleichzeitig als Compose-eigen behandelt. | Das Volume besitzt einen festen Namen und ist in Compose ausdrücklich als extern vorbereitet markiert. | +| Reasoning-Filter | Beide globalen Filter besaßen Priorität 0; bei gleicher Priorität entschied die ID-Sortierung statt der gewünschten Logik. | `Reasoning Default Off` läuft mit Priorität 10 sicher vor dem optionalen `Thinking`-Override mit Priorität 20. Filter und sicherer Installer liegen versioniert im Repository. | ## Abnahmezustand am 21. August 2026 diff --git a/docs/MIGRATION.md b/docs/MIGRATION.md index e106664..dc2d5a7 100644 --- a/docs/MIGRATION.md +++ b/docs/MIGRATION.md @@ -72,6 +72,9 @@ enthält, verwendet das Skript ausschließlich den im Docker-Inventar gesicherte unveränderlichen Registry-Digest. Es fällt niemals auf `latest` zurück. Da lokale Docker-Image-IDs beim Wiederherstellen von einem Registry-Digest abweichen können, verifiziert es zusätzlich die gesicherte OCI-Build-Revision. +Nach dem Datenimport wendet es außerdem die im Repository versionierten +OpenWebUI-Filter erneut an. Dadurch bleiben auch deren Prioritäten unabhängig +vom Alter der Datenbanksicherung reproduzierbar. Der alte Host bleibt bis zum bestandenen Abnahmetest unverändert und dient nur als Referenz. Es werden keine Caches oder unbekannten Altverzeichnisse kopiert. diff --git a/docs/OPERATIONS.md b/docs/OPERATIONS.md index 8145dc2..98b43c7 100644 --- a/docs/OPERATIONS.md +++ b/docs/OPERATIONS.md @@ -1,5 +1,27 @@ # Betrieb +## Reasoning-Filter in OpenWebUI + +Die versionierten Filter liegen unter `platform/openwebui/filters/`. Ihre +Reihenfolge ist absichtlich festgelegt: + +1. `Reasoning Default Off`, Priorität 10: setzt jeden Request zunächst auf + `reasoning_effort=none`. +2. `Thinking`, Priorität 20: läuft nur bei aktiviertem Brain-Schalter und + überschreibt den Standard mit Low, Medium oder High. + +OpenWebUI sortiert kleinere Prioritäten zuerst. Nach dem ersten Anlegen eines +Admin-Benutzers oder nach einer Datenwiederherstellung werden beide Filter mit +einer vorherigen Datenbanksicherung installiert beziehungsweise aktualisiert: + +```bash +sudo /opt/mike-ai/stack/platform/openwebui/install-filters.sh +``` + +Bei mehreren Administratoren muss der gewünschte Eigentümer explizit über +`OPENWEBUI_FILTER_OWNER_ID` gesetzt werden. Das Skript liest oder verändert +keine Chats. + ## Profile | Profil | Virtuelles Modell | Kontext | Zweck | diff --git a/platform/migration/restore-reference-backup.sh b/platform/migration/restore-reference-backup.sh index ad1acb3..f9b73a5 100755 --- a/platform/migration/restore-reference-backup.sh +++ b/platform/migration/restore-reference-backup.sh @@ -138,6 +138,11 @@ log "OpenWebUI-Daten atomar ersetzen" docker stop mike-ai-open-webui >/dev/null 2>&1 || true rsync -a --delete "$ui_source/data/" "$volume_path/" +log "Versionierte OpenWebUI-Filter anwenden" +filter_installer=$STACK_DIR/platform/openwebui/install-filters.sh +[[ -x $filter_installer ]] || die "OpenWebUI-Filter-Installer fehlt." +"$filter_installer" + log "Schlüssel und aktuelle Tool-Secrets wiederherstellen" set_env_value "$SECRETS_DIR/stack.env" WEBUI_SECRET_KEY "$old_webui_key" set_env_value "$SECRETS_DIR/stack.env" ROUTER_API_KEY "$old_router_key" diff --git a/platform/openwebui/filters/reasoning_default_off.py b/platform/openwebui/filters/reasoning_default_off.py new file mode 100644 index 0000000..5ff7dda --- /dev/null +++ b/platform/openwebui/filters/reasoning_default_off.py @@ -0,0 +1,24 @@ +""" +title: Reasoning Default Off +author: Mike +version: 1.0.1 +description: Forces llama.cpp reasoning off by default. +""" + +from pydantic import BaseModel + + +class Filter: + class Valves(BaseModel): + # OpenWebUI runs lower priorities first. This default must precede the + # optional Thinking override. + priority: int = 10 + + def __init__(self): + self.valves = self.Valves() + self.toggle = False + + async def inlet(self, body: dict, **kwargs) -> dict: + body["reasoning_effort"] = "none" + body["thinking_budget_tokens"] = 0 + return body diff --git a/platform/openwebui/filters/thinking.py b/platform/openwebui/filters/thinking.py new file mode 100644 index 0000000..e925205 --- /dev/null +++ b/platform/openwebui/filters/thinking.py @@ -0,0 +1,57 @@ +""" +title: Thinking +author: Mike +version: 2.0.1 +description: Optional Qwen3.8 reasoning override - Low / Medium / High. +""" + +from pydantic import BaseModel, Field +from typing import Literal +from urllib.parse import quote + + +class Filter: + class Valves(BaseModel): + # Runs after Reasoning Default Off and may deliberately override it. + priority: int = 20 + + class UserValves(BaseModel): + level: Literal["Low", "Medium", "High"] = Field( + default="Low", + description="Thinking depth", + ) + + def __init__(self): + self.valves = self.Valves() + self.toggle = True + svg = """ + + 🧠 + + """ + self.icon = "data:image/svg+xml," + quote(svg) + + async def inlet( + self, + body: dict, + __user__: dict = None, + **kwargs, + ) -> dict: + level = "Low" + if __user__ and __user__.get("valves") is not None: + valves = __user__["valves"] + if isinstance(valves, dict): + level = valves.get("level", "Low") + else: + level = getattr(valves, "level", "Low") + + if level == "Low": + body["reasoning_effort"] = "low" + body["thinking_budget_tokens"] = 512 + elif level == "Medium": + body["reasoning_effort"] = "medium" + body["thinking_budget_tokens"] = 3072 + elif level == "High": + body["reasoning_effort"] = "xhigh" + body["thinking_budget_tokens"] = 8192 + return body diff --git a/platform/openwebui/install-filters.sh b/platform/openwebui/install-filters.sh new file mode 100755 index 0000000..7339bc6 --- /dev/null +++ b/platform/openwebui/install-filters.sh @@ -0,0 +1,108 @@ +#!/usr/bin/env bash +set -Eeuo pipefail +umask 077 + +CONTAINER=${OPENWEBUI_CONTAINER:-mike-ai-open-webui} +VOLUME=${OPENWEBUI_VOLUME:-mike-ai_open-webui-data} +FILTER_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")/filters" && pwd) + +die() { printf 'FEHLER: %s\n' "$*" >&2; exit 1; } +[[ $EUID -eq 0 ]] || die "Bitte als root ausführen." +for file in reasoning_default_off.py thinking.py; do + [[ -s $FILTER_DIR/$file ]] || die "Filterdatei fehlt: $file" +done + +volume_path=$(docker volume inspect -f '{{.Mountpoint}}' "$VOLUME") +db=$volume_path/webui.db +[[ -s $db ]] || die "OpenWebUI-Datenbank fehlt: $db" + +was_running=false +if [[ $(docker inspect -f '{{.State.Running}}' "$CONTAINER" 2>/dev/null || true) == true ]]; then + was_running=true + docker stop "$CONTAINER" >/dev/null +fi +restart_on_exit() { + if [[ $was_running == true ]]; then + docker start "$CONTAINER" >/dev/null 2>&1 || true + fi +} +trap restart_on_exit EXIT + +stamp=$(date +%Y%m%d-%H%M%S) +backup=$volume_path/webui.db.before-filter-install-$stamp +cp -a "$db" "$backup" +rm -f "$volume_path/webui.db-wal" "$volume_path/webui.db-shm" + +python3 - "$db" "$FILTER_DIR" "${OPENWEBUI_FILTER_OWNER_ID:-}" <<'PY' +import json +import pathlib +import sqlite3 +import sys +import time + +db, filter_dir, requested_owner = sys.argv[1:] +con = sqlite3.connect(db) +columns = {row[1] for row in con.execute("pragma table_info(function)")} +required = { + "id", "user_id", "name", "type", "content", "meta", "valves", + "is_active", "is_global", "updated_at", "created_at", +} +if not required <= columns: + raise SystemExit("Unbekanntes OpenWebUI-Function-Schema; keine Änderung vorgenommen.") + +owner = requested_owner +if not owner: + existing = con.execute( + "select user_id from function where id in (?, ?) order by id limit 1", + ("reasoning_default_off", "thinking"), + ).fetchone() + if existing: + owner = existing[0] +if not owner: + admins = con.execute("select id from user where role='admin'").fetchall() + if len(admins) != 1: + raise SystemExit( + "Filter-Eigentümer ist nicht eindeutig; OPENWEBUI_FILTER_OWNER_ID setzen." + ) + owner = admins[0][0] + +now = int(time.time()) +filters = [ + ("reasoning_default_off", "Reasoning Default Off", 10), + ("thinking", "Thinking", 20), +] +with con: + for function_id, name, priority in filters: + content = pathlib.Path(filter_dir, f"{function_id}.py").read_text() + con.execute( + """ + insert into function + (id,user_id,name,type,content,meta,valves,is_active,is_global,updated_at,created_at) + values (?,?,?,?,?,?,?,?,?,?,?) + on conflict(id) do update set + name=excluded.name, + type=excluded.type, + content=excluded.content, + valves=excluded.valves, + is_active=excluded.is_active, + is_global=excluded.is_global, + updated_at=excluded.updated_at + """, + ( + function_id, owner, name, "filter", content, "{}", + json.dumps({"priority": priority}), True, True, now, now, + ), + ) +print("OpenWebUI-Filter installiert: Default Off=10, Thinking=20") +PY + +if [[ $was_running == true ]]; then + docker start "$CONTAINER" >/dev/null + deadline=$((SECONDS + 180)) + until [[ $(docker inspect -f '{{.State.Health.Status}}' "$CONTAINER" 2>/dev/null || true) == healthy ]]; do + (( SECONDS < deadline )) || die "$CONTAINER wurde nicht rechtzeitig gesund." + sleep 3 + done +fi +trap - EXIT +printf 'Datenbanksicherung: %s\n' "$backup"