From 1bfa59acbda0b0a60b73c313f64bededa7e85252 Mon Sep 17 00:00:00 2001 From: Classic298 <27028174+Classic298@users.noreply.github.com> Date: Sun, 6 Sep 2026 21:13:45 +0200 Subject: [PATCH 01/12] fix: keep HTML entities in extracted document text (#29736) Uploading a file whose text contains literal HTML entities stored a rewritten copy of it: ` ` became a non-breaking space, `>` became `>`, and ` ` was decoded twice down to a bare non-breaking space. That stored text is what gets indexed and what the model reads, so notes, specs and source files reached the model differing from the file that was uploaded. Every loaded document goes through `ftfy.fix_text`, which is there to repair mojibake left by the encoding-detection fallback. Its default configuration also decodes HTML entities, per line and sticky forward: entities are decoded on every line up to the first line holding a literal `<`, then left alone for the rest of the document. The same escape therefore survives or vanishes depending on where it sits in the file. This disables that one behaviour and leaves every other ftfy repair in place. Text from a third-party extraction engine that returns escaped output now keeps those escapes. Guessing whether an escape is markup or content is the bug being fixed. Fixes #29732 --- backend/open_webui/retrieval/loaders/main.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/backend/open_webui/retrieval/loaders/main.py b/backend/open_webui/retrieval/loaders/main.py index 13570c37f0..bc50269f3d 100644 --- a/backend/open_webui/retrieval/loaders/main.py +++ b/backend/open_webui/retrieval/loaders/main.py @@ -314,7 +314,11 @@ class Loader: def load(self, filename: str, file_content_type: str, file_path: str) -> list[Document]: loader = self._get_loader(filename, file_content_type, file_path) docs = loader.load() - return [Document(page_content=ftfy.fix_text(doc.page_content), metadata=doc.metadata) for doc in docs] + # ftfy's auto mode unescapes entities on every line before the first literal '<', rewriting the document. + return [ + Document(page_content=ftfy.fix_text(doc.page_content, unescape_html=False), metadata=doc.metadata) + for doc in docs + ] async def aload(self, filename: str, file_content_type: str, file_path: str) -> list[Document]: """ From 68a74da70bdda7dd1cda4f4eac05668509155b14 Mon Sep 17 00:00:00 2001 From: Classic298 <27028174+Classic298@users.noreply.github.com> Date: Sun, 6 Sep 2026 21:20:41 +0200 Subject: [PATCH 02/12] Fix: mps inference evaluations (#29735) * fix(retrieval): serialize local embedding and reranking on MPS On Apple Silicon the server process is killed outright (SIGSEGV or SIGTRAP, no traceback) partway through answering any question that retrieves from a knowledge base with hybrid search and a local reranking model. The client sees a dropped connection and the answer is lost. Hybrid search fans its queries out concurrently and every task calls the same shared local model on a worker thread. Torch's Metal shader cache is a process-wide singleton whose lookup tables have no lock, so two of those threads racing inside it corrupt the cache and take the process down with it. Guard the local SentenceTransformer and CrossEncoder calls with a shared lock that is only a real lock when the selected device is MPS. CPU and CUDA installs keep the concurrency they have today, and external reranking endpoints are untouched. Reranking several queries on a Mac now runs one at a time, which is the cost of the process staying alive. Verified by driving the real hybrid-search fan-out with 16 concurrent queries: peak simultaneous entries into the local model drops from 16 to 1 on MPS, stays at 16 on CPU, and the returned documents, scores and ordering are byte-identical in every case. Fixes #29722 * fix(evaluations): serialize the leaderboard embedder against retrieval on MPS The leaderboard's tag-similarity search builds its own SentenceTransformer, and on Apple Silicon sentence-transformers places it on the MPS device. It runs on a worker thread, so an admin running a leaderboard search while anyone queries a knowledge base puts two threads into torch's Metal backend at the same time, which kills the server process outright with no traceback. Move the lock added for the retrieval path into env.py, beside the device selection that decides whether MPS is used at all, and take it around the leaderboard's embedding calls as well. Sharing one lock between the two modules is the whole point, since two separate locks would still let a leaderboard search collide with a retrieval query. Only inference is guarded, matching the retrieval path. Model construction stays as it is here and in the retrieval routers. Verified by driving the leaderboard similarity path and retrieval reranking from six threads against one instrumented model: peak simultaneous entries drops from six to one on MPS, and the similarity scores are unchanged. Related to #29722. --- backend/open_webui/env.py | 5 +++++ backend/open_webui/retrieval/utils.py | 25 ++++++++++++++--------- backend/open_webui/routers/evaluations.py | 6 ++++-- 3 files changed, 24 insertions(+), 12 deletions(-) diff --git a/backend/open_webui/env.py b/backend/open_webui/env.py index b17df7340a..c3a7da82e2 100644 --- a/backend/open_webui/env.py +++ b/backend/open_webui/env.py @@ -7,7 +7,9 @@ import pkgutil import re import shutil import sys +import threading import traceback +from contextlib import nullcontext from pathlib import Path from typing import Any, Optional from uuid import uuid4 @@ -66,6 +68,9 @@ if sys.platform == 'darwin' and DEVICE_TYPE == 'cpu': except Exception: pass +# Torch MPS inference is not thread-safe and a concurrent call kills the whole process. +MPS_INFERENCE_LOCK = threading.Lock() if DEVICE_TYPE == 'mps' else nullcontext() + #################################### # LOGGING #################################### diff --git a/backend/open_webui/retrieval/utils.py b/backend/open_webui/retrieval/utils.py index 294309984e..ec7c73770e 100644 --- a/backend/open_webui/retrieval/utils.py +++ b/backend/open_webui/retrieval/utils.py @@ -32,6 +32,7 @@ from open_webui.env import ( BYPASS_RETRIEVAL_ACCESS_CONTROL, ENABLE_FORWARD_USER_INFO_HEADERS, ENABLE_RETRIEVAL_UNSCOPED_COLLECTIONS, + MPS_INFERENCE_LOCK, OFFLINE_MODE, ) from open_webui.models.access_grants import AccessGrants @@ -1116,17 +1117,16 @@ def get_embedding_function( 'SentenceTransformer model name, or configure an external ' 'RAG_EMBEDDING_ENGINE (ollama, openai, azure_openai).' ) - return await asyncio.to_thread( - ( - lambda query, prefix=None: embedding_function.encode( + + def encode(): + with MPS_INFERENCE_LOCK: + return embedding_function.encode( query, batch_size=int(embedding_batch_size), **({'prompt': prefix} if prefix else {}), ).tolist() - ), - query, - prefix, - ) + + return await asyncio.to_thread(encode) return async_embedding_function elif embedding_engine in ['ollama', 'openai', 'azure_openai']: @@ -1250,9 +1250,14 @@ def get_reranking_function(reranking_engine, reranking_model, reranking_function [(query, doc.page_content) for doc in documents], user=user ) else: - return lambda query, documents, user=None: reranking_function.predict( - [(query, doc.page_content) for doc in documents], batch_size=int(reranking_batch_size) - ) + + def predict(query, documents, user=None): + with MPS_INFERENCE_LOCK: + return reranking_function.predict( + [(query, doc.page_content) for doc in documents], batch_size=int(reranking_batch_size) + ) + + return predict # UUIDs, SHA-256 digests, and prefixed variants thereof all fit [A-Za-z0-9_-]. diff --git a/backend/open_webui/routers/evaluations.py b/backend/open_webui/routers/evaluations.py index 8a6bf71f4a..e1c7543eea 100644 --- a/backend/open_webui/routers/evaluations.py +++ b/backend/open_webui/routers/evaluations.py @@ -4,6 +4,7 @@ from typing import Optional from fastapi import APIRouter, Depends, HTTPException, Request, status from fastapi.concurrency import run_in_threadpool from open_webui.constants import ERROR_MESSAGES +from open_webui.env import MPS_INFERENCE_LOCK from open_webui.events import EVENTS, publish_event from open_webui.internal.db import get_async_session from open_webui.models.config import Config @@ -179,8 +180,9 @@ def _compute_similarities(feedbacks: list[LeaderboardFeedbackData], query: str) return {} try: - tag_embeddings = embedding_model.encode(all_tags) - query_embedding = embedding_model.encode([query])[0] + with MPS_INFERENCE_LOCK: + tag_embeddings = embedding_model.encode(all_tags) + query_embedding = embedding_model.encode([query])[0] except Exception as e: log.error(f'Embedding error: {e}') return {} From a3c90e5aacd60100bf20fe301a938f6d1ca61f4f Mon Sep 17 00:00:00 2001 From: Classic298 <27028174+Classic298@users.noreply.github.com> Date: Sun, 6 Sep 2026 21:21:09 +0200 Subject: [PATCH 03/12] fix: blank chat messages on iOS home screen apps and in-app browsers (#29734) Chat messages are virtualized with content-visibility: auto, which WebKit paints incorrectly and can leave blank. #26805 skipped that on Safari by looking for the Safari token in the user agent, but iOS in-app browsers, home screen apps and iPadOS desktop-class standalone windows send no such token, so those users still get empty assistant responses. Check the navigator vendor string as well, which every WebKit surface reports regardless of user agent. The user agent check stays, because non-Apple WebKit ports can compile a different vendor string while shipping the same paint bug. That earlier fix also withheld the message-listitem class entirely, and the class doubles as the styling hook the sidebar hover preview reaches through, so hover previews have rendered at full chat spacing and width on Safari since v0.11.0. Only the content-visibility rule is gated now, on its own class, and the hook stays on every message. Safari hover previews become compact like every other engine. Verified across fourteen engine cases: virtualization is off on every Apple WebKit surface, unchanged on Chromium, Firefox and Android, the hover preview overrides apply again on Safari, and the screenshot export still captures every message on both. Refs #26712, #29688 --- src/lib/components/chat/Messages/Message.svelte | 12 ++++++++---- src/lib/components/layout/Navbar/Menu.svelte | 2 +- src/lib/components/layout/Sidebar/ChatMenu.svelte | 2 +- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/lib/components/chat/Messages/Message.svelte b/src/lib/components/chat/Messages/Message.svelte index 9d1bb0672c..97c8cd69a4 100644 --- a/src/lib/components/chat/Messages/Message.svelte +++ b/src/lib/components/chat/Messages/Message.svelte @@ -49,16 +49,20 @@ export let topPadding = false; export let onInsertToNote: ((content: string) => void) | null = null; - // Safari's content-visibility implementation has paint bugs that leave + // WebKit's content-visibility implementation has paint bugs that leave // on-screen messages blank (#26712), so skip virtualization there - const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent); + // iOS WebViews and home screen apps carry no Safari token, hence the vendor check + const isWebKit = + navigator.vendor === 'Apple Computer, Inc.' || + /^((?!chrome|android).)*safari/i.test(navigator.userAgent);