Make OpenWebUI reasoning filter order reproducible
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
Executable
+108
@@ -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"
|
||||
Reference in New Issue
Block a user