Make OpenWebUI reasoning filter order reproducible

This commit is contained in:
Mikei386 committed 2026-08-21 14:56:39 +02:00
1 parent bf90698b99
commit 9a40758c7f
7 files changed
+220

No files matched your search

+1
View File
@@ -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. | | 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. | | 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. | | 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 ## Abnahmezustand am 21. August 2026
+3
View File
@@ -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. unveränderlichen Registry-Digest. Es fällt niemals auf `latest` zurück.
Da lokale Docker-Image-IDs beim Wiederherstellen von einem Registry-Digest Da lokale Docker-Image-IDs beim Wiederherstellen von einem Registry-Digest
abweichen können, verifiziert es zusätzlich die gesicherte OCI-Build-Revision. 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 Der alte Host bleibt bis zum bestandenen Abnahmetest unverändert und dient nur
als Referenz. Es werden keine Caches oder unbekannten Altverzeichnisse kopiert. als Referenz. Es werden keine Caches oder unbekannten Altverzeichnisse kopiert.
+22
View File
@@ -1,5 +1,27 @@
# Betrieb # 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 ## Profile
| Profil | Virtuelles Modell | Kontext | Zweck | | Profil | Virtuelles Modell | Kontext | Zweck |
@@ -138,6 +138,11 @@ log "OpenWebUI-Daten atomar ersetzen"
docker stop mike-ai-open-webui >/dev/null 2>&1 || true docker stop mike-ai-open-webui >/dev/null 2>&1 || true
rsync -a --delete "$ui_source/data/" "$volume_path/" 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" 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" WEBUI_SECRET_KEY "$old_webui_key"
set_env_value "$SECRETS_DIR/stack.env" ROUTER_API_KEY "$old_router_key" set_env_value "$SECRETS_DIR/stack.env" ROUTER_API_KEY "$old_router_key"
@@ -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
+57
View File
@@ -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 = """
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<text x="16" y="23" text-anchor="middle" font-size="24">🧠</text>
</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
+108
View File
@@ -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"