#!/usr/bin/env bash set -Eeuo pipefail umask 077 CONTAINER=${OPENWEBUI_CONTAINER:-mike-ai-open-webui} VOLUME=${OPENWEBUI_VOLUME:-mike-ai_open-webui-data} die() { printf 'FEHLER: %s\n' "$*" >&2; exit 1; } [[ $EUID -eq 0 ]] || die "Bitte als root ausführen." 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-model-install-$stamp cp -a "$db" "$backup" rm -f "$volume_path/webui.db-wal" "$volume_path/webui.db-shm" python3 - "$db" "${OPENWEBUI_MODEL_OWNER_ID:-}" <<'PY' import json import sqlite3 import sys import time db, requested_owner = sys.argv[1:] con = sqlite3.connect(db) columns = {row[1] for row in con.execute("pragma table_info(model)")} required = { "id", "user_id", "base_model_id", "name", "params", "meta", "is_active", "updated_at", "created_at", } if not required <= columns: raise SystemExit("Unbekanntes OpenWebUI-Modellschema; keine Änderung vorgenommen.") owner = requested_owner if not owner: row = con.execute( "select user_id from model where id in " "('mikeai-fast','mikeai-medium','mikeai-large','mikeai-ultra'," "'mikeai---fast-72k') order by updated_at desc limit 1" ).fetchone() if row: owner = row[0] if not owner: admins = con.execute("select id from user where role='admin'").fetchall() if len(admins) != 1: raise SystemExit( "Modelleigentümer ist nicht eindeutig; OPENWEBUI_MODEL_OWNER_ID setzen." ) owner = admins[0][0] now = int(time.time()) filter_ids = [ "reasoning_default_off", "thinking", "stability_guard", "secret_redaction", "local_performance_metrics", ] # Open WebUI addresses a global MCP server as server:mcp:. # Attach only our bounded, read-only web relay to every profile. This is not # the built-in Open WebUI web-search feature and therefore does not create a # second competing search path. default_tool_ids = ["server:mcp:web-local"] def capabilities(vision: bool) -> dict: return { "file_context": True, "vision": vision, "file_upload": True, "web_search": True, "image_generation": False, "code_interpreter": True, "terminal": True, "citations": True, "status_updates": True, "memory": True, "builtin_tools": True, } profiles = [ { "id": "mikeai-fast", "base": "qwen-fast", "name": "MikeAI · Fast · 76K", "description": ( "Schnelles Alltags- und Agentenprofil: Qwen3.8-27B IQ4-MIX, " "76.800 Token, RTX 5080, MTP2 und Bildanalyse." ), "vision": True, "tags": ["MikeAI", "Schnell", "Vision"], }, { "id": "mikeai-medium", "base": "qwen-medium", "name": "MikeAI · Medium · 160K · Standard", "description": ( "Verbindliches Standardprofil: Qwen3.8-27B IQ4_XS Pure, " "160.000 Token, RTX 5080 + RTX 3060 (90:10), MTP3 und Bildanalyse." ), "vision": True, "tags": ["MikeAI", "Standard", "Vision"], }, { "id": "mikeai-large", "base": "qwen-large", "name": "MikeAI · Large · 192K", "description": ( "Großes Agenten- und MCP-Profil: Qwen3.8-27B IQ4_XS Pure, " "192.000 Token, RTX 5080 + RTX 3060 (86:14), MTP3 und Bildanalyse." ), "vision": True, "tags": ["MikeAI", "Großer Kontext", "Vision"], }, { "id": "mikeai-ultra", "base": "qwen-ultra", "name": "MikeAI · Ultra · 256K · Text", "description": ( "Maximaler Textkontext: Qwen3.8-27B IQ4_XS Pure, 262.144 Token, " "RTX 5080 + RTX 3060 (80:20), MTP2; bewusst ohne Bildanalyse." ), "vision": False, "tags": ["MikeAI", "Maximaler Kontext", "Nur Text"], }, ] params = { "temperature": 0.2, "top_p": 0.8, "top_k": 20, "max_tokens": 32768, } def upsert(model_id, base_model_id, name, model_params, meta, active=True): existing = con.execute( "select created_at from model where id=?", (model_id,) ).fetchone() created_at = existing[0] if existing else now con.execute( """ insert into model (id,user_id,base_model_id,name,params,meta,is_active,updated_at,created_at) values (?,?,?,?,?,?,?,?,?) on conflict(id) do update set user_id=excluded.user_id, base_model_id=excluded.base_model_id, name=excluded.name, params=excluded.params, meta=excluded.meta, is_active=excluded.is_active, updated_at=excluded.updated_at """, ( model_id, owner, base_model_id, name, json.dumps(model_params, ensure_ascii=False), json.dumps(meta, ensure_ascii=False), active, now, created_at, ), ) with con: # Hide the raw router aliases while keeping them resolvable as bases for # the user-facing workspace presets. for raw_id in ("qwen-fast", "qwen-medium", "qwen-large", "qwen-ultra"): upsert( raw_id, None, raw_id, {}, { "description": "Interner Router-Alias; bitte das zugehörige MikeAI-Profil verwenden.", "hidden": True, }, ) for profile in profiles: meta = { "profile_image_url": "/static/favicon.png", "description": profile["description"], "capabilities": capabilities(profile["vision"]), "knowledge": None, "suggestion_prompts": None, "tags": [{"name": tag} for tag in profile["tags"]], "toolIds": default_tool_ids, # Built-in features remain available but are not forced on every # request. The small, bounded local web MCP above is the only # web-search path attached by default. "defaultFeatureIds": [], "filterIds": filter_ids, "actionIds": ["quick_actions"], "tts": {"voice": "alloy"}, "hidden": False, } upsert( profile["id"], profile["base"], profile["name"], params, meta ) # Exact legacy entries from the experimental 72K/Long setup. The database # backup above makes the cleanup fully recoverable. con.execute( "delete from model where id in ('mikeai---fast-72k','qwen-long')" ) con.execute( """ insert into config (key,value,updated_at) values (?,?,?) on conflict(key) do update set value=excluded.value, updated_at=excluded.updated_at """, ("ui.default_models", json.dumps("mikeai-medium"), now), ) print("OpenWebUI-Modelle installiert: Fast, Medium (Standard), Large, Ultra") PY printf 'Datenbanksicherung: %s\n' "$backup"