Create and publish Docker images with specific build args / build (map[arch:linux/amd64 runner:ubuntu-latest], map[build_args: free_disk:false name:main suffix:]) (push) Has been cancelled
Create and publish Docker images with specific build args / build (map[arch:linux/amd64 runner:ubuntu-latest], map[build_args:USE_CUDA=true
USE_CUDA_VER=cu126
free_disk:true name:cuda126 suffix:-cuda126]) (push) Has been cancelled
Create and publish Docker images with specific build args / build (map[arch:linux/amd64 runner:ubuntu-latest], map[build_args:USE_CUDA=true free_disk:true name:cuda suffix:-cuda]) (push) Has been cancelled
Create and publish Docker images with specific build args / build (map[arch:linux/amd64 runner:ubuntu-latest], map[build_args:USE_OLLAMA=true free_disk:false name:ollama suffix:-ollama]) (push) Has been cancelled
Create and publish Docker images with specific build args / build (map[arch:linux/amd64 runner:ubuntu-latest], map[build_args:USE_SLIM=true free_disk:false name:slim suffix:-slim]) (push) Has been cancelled
Create and publish Docker images with specific build args / build (map[arch:linux/arm64 runner:ubuntu-24.04-arm], map[build_args: free_disk:false name:main suffix:]) (push) Has been cancelled
Create and publish Docker images with specific build args / build (map[arch:linux/arm64 runner:ubuntu-24.04-arm], map[build_args:USE_CUDA=true
USE_CUDA_VER=cu126
free_disk:true name:cuda126 suffix:-cuda126]) (push) Has been cancelled
Create and publish Docker images with specific build args / build (map[arch:linux/arm64 runner:ubuntu-24.04-arm], map[build_args:USE_CUDA=true free_disk:true name:cuda suffix:-cuda]) (push) Has been cancelled
Create and publish Docker images with specific build args / build (map[arch:linux/arm64 runner:ubuntu-24.04-arm], map[build_args:USE_OLLAMA=true free_disk:false name:ollama suffix:-ollama]) (push) Has been cancelled
Create and publish Docker images with specific build args / build (map[arch:linux/arm64 runner:ubuntu-24.04-arm], map[build_args:USE_SLIM=true free_disk:false name:slim suffix:-slim]) (push) Has been cancelled
Fetching a page with many media files through the Playwright web loader was extremely slow or timed out, while raw Playwright loaded the same page in a couple of seconds. The loader routes every request the page makes through the backend HTTP client and downloads the full body before the browser sees any of it, and the browser cancelling a media request once it has enough never reaches that download. On a page with a few dozen audio players every file was pulled in full for a text extraction that never reads it.
Image, media and font requests are now aborted in the interceptor before any fetch is made. None of them feed the text extraction. Measured on the page from the report with the default timeout on the same connection:
| | requests fetched | bytes downloaded | elapsed |
|---|---|---|---|
| before | 151 | 55.0 MB | 10.1 s |
| after | 45 | 3.9 MB | 2.6 s |
Fixes#29741
The bulk tool export returned every tool the caller could read, while the per-tool export path returns only what the caller can write. This aligns the two, matching how model export already scopes its query.
Callers still export their own tools and any tool shared with them for writing; admins running with BYPASS_ADMIN_ACCESS_CONTROL are unaffected.
Tool calls to an OpenAPI tool server put every argument the model returned into the JSON request body, including the parameters that were already substituted into the URL. Servers that validate their input strictly (additionalProperties: false) answered 422 "unexpected property", so reads worked and every write through an endpoint with a path or query parameter failed.
The body is now built from the model's arguments minus the operation's declared parameters, keeping any name the requestBody schema declares as a property of its own, so an endpoint that wants the resource id in the body as well as in the path still gets it.
The filter only runs when the resolved body schema lists its properties. A free-form, composed or non-JSON body offers nothing to check a name against, so those requests go out exactly as before.
src/lib/apis/index.ts carries the same request builder for direct tool server connections and had the same bug, so it gets the same fix.
Fixes#29716
Some endpoints stream a tool call whose function name is JSON null instead of a string. Nothing normalized it, so the null stayed on the tool call, was written into the stored message, and was sent back to the endpoint in the assistant message on the next turn, where a null is not a valid function name.
The delta accumulator now replaces a null name with an empty string, at the same point it already normalizes the arguments field. The call still fails as an unknown tool, which is the right outcome for a call that has no name, so the result is one failed tool call instead of a follow-up request the endpoint has to reject.
This is done where the delta enters the accumulator rather than at the consumers, because the name is emitted to the client and persisted while the response is still streaming, before anything downstream could clean it up.
Checked against 1261 streaming delta sequences: behaviour is unchanged except where the delta that creates the tool call carries a null name.
Seen in #29686 with a custom sglang build.
Any authenticated user could fetch a channel webhook's avatar, or be redirected to its external profile image URL, without belonging to the channel or holding any read access to it. This was the only webhook route with neither a channel check nor the channels feature gate.
The route now applies the same read gate every other route in this router uses: active membership for group and direct message channels, admin or a channel read grant otherwise, answering with 403 on denial and 404 when the webhook's channel row no longer exists. It also runs the channels feature and permission gate, so with channels disabled, or the permission withdrawn from regular users, the endpoint now refuses where it previously served the image.
Avatars keep rendering for channel members, and a denied request shows the default logo rather than a broken image, because the avatar component already falls back on an image error.
The Docker image gets about 50 MB smaller on disk (about 20 MB off the pull). uv is only needed to run the requirements install, but it is pip-installed into the image and stays there. It is now bind-mounted from the uv image for that RUN only, pinned to 0.12.10, so the runtime image never contains it. This is the pattern uv's own Docker guide recommends for the case.
Uninstalling uv at the end of the same RUN would also keep it out of the layer; the mount was preferred because it fixes the uv version and the mounted layer is cached by the builder across rebuilds.
pip stays available in the container, so hand-installing optional packages the way requirements.txt describes keeps working; only running uv inside the container goes away. The build now requires BuildKit (the syntax directive alone was a comment to the classic builder, this line makes it mandatory), needs access to ghcr.io next to PyPI, and the uv version is a pin to bump by hand, like the base images.
Part of #29721.
New **pt-BR** translations for items introduced in the latest releases, plus a consistency/quality pass across existing strings (grammar, tone, capitalization, pluralization). Placeholders and hotkeys preserved. No logic changes.
A citation's embed_url now has to be an http or https URL, or protocol-relative, before it is opened or handed to the embed panel. Anything else falls back to the citation modal, which already renders the source.
* fix: skip embedded HTML parts in docx preview
The docx preview no longer renders altChunk parts, so an embedded HTML sub-document is omitted from the rendered output instead of being handed to the renderer.
* fix: restrict docx preview link targets to safe schemes
The docx preview kept whatever link target the document supplied, so a document could point a link at any scheme the browser understands.
After rendering, a link target is now kept only when it resolves to http, https, mailto or tel. Anything else has its target removed and the link renders as plain text. Targets resolve against the page URL, so internal bookmark links and relative targets are unaffected, while an empty target, which the renderer emits for a hyperlink with no external relationship, is dropped instead of reloading the app.
DOMPurify was not used because running it over the rendered document would strip the renderer's own markup and styling, so the check stays limited to link targets. Links using file: or Office application schemes no longer resolve.
The four Noto Sans variable fonts under open_webui/static/fonts have never been loaded. Removing them makes an installed package 40 MB smaller on disk, the wheel about 24 MB, and the Docker image about 80 MB, because the image currently stores the static directory twice.
The PDF generator registers only the static faces through add_font, and the stylesheet that names the variable fonts, pdf-style.css, is read into a variable that nothing ever uses, so no code path can reach them. The frontend never fetches these files either.
Only the four @font-face blocks that pointed at the deleted files are removed; the rest of the stylesheet, font stack included, is left exactly as it is.
The files were reachable under the public /static mount, so anything outside this repository that hotlinked them gets a 404 from now on.
Part of #29721.
The Docker image and a fresh pip install get about 100 MB smaller uncompressed. The removed pins are ones nothing in the backend imports and that no installed package requires, with the one exception of async-timeout, which redis still requires below Python 3.11.3; the resolver installs it there as a transitive dependency, just no longer pinned to 5.0.1 for plain pip installs.
google-api-python-client, google-auth-httplib2 and google-auth-oauthlib were added for Google Drive in 2024, but the picker is frontend-only and loads gapi from apis.google.com; together they are about 95 MB on disk. pymongo, the langchain meta package, pymdown-extensions, pytube, APScheduler and RestrictedPython have no importer; the YouTube loader, the scheduler and the tool sandbox are all hand-written in this repository.
google-genai stays: nothing imports it either, but the tool-call path carries accommodations written for python-genai callers, so Gemini pipes are expected to find it in the shared environment.
uv.lock is regenerated, deletions only.
One user-visible consequence: a tool or function that imported one of the removed packages without declaring it in its frontmatter requirements has worked only because the package was preinstalled. Declaring it fixes that where frontmatter installs are enabled and the instance can reach PyPI; an offline instance needs the package installed into the image instead.
Part of #29721.
The main and CUDA Docker images get about 21 MB smaller (the nltk package, the punkt_tab data and its zip); slim images, which never downloaded the data, about 6 MB.
nltk was in the image for unstructured, which used it to tokenize documents. The Dockerfile download was added for airgapped containers failing on the missing punkt_tab data (#21150; the same request in #16260), the same lookup failed on first use in other setups (#17594, #4642), and the download in start.sh and start_windows.bat came with the Playwright web loader mode and sits in that branch.
unstructured 0.22.31, the pinned version, has no nltk references at all and tokenizes with spaCy, nothing else installed requires nltk outside transformers' testing and dev extras, and nothing in the backend imports it, so the pin and both downloads go together.
One user-visible consequence: a tool or function that imports nltk inside the container stops working unless it declares nltk in its frontmatter requirements. On an offline instance the package, and any nltk data such as punkt_tab, have to be installed into the image instead.
Part of #29721.
Any authenticated user could fetch the profile image of a model they have no access to, and could tell an existing model id from an unknown one by whether the response carried the image or the default logo.
The endpoint now serves an image only to callers who can see the model itself: the owner, an admin under the admin bypass, or the holder of a read grant, with the same rule applied to arena models defined in config. Everyone else gets the default logo, byte for byte the response an unknown id already returned, so ids can no longer be probed. BYPASS_MODEL_ACCESS_CONTROL is honoured here because it is what decides which models reach a user's model list to begin with.
Avatars now fall back to the default logo wherever a viewer meets a model id without holding a grant on it: a model reply in a channel shown to the other members, and the admin analytics and evaluation pages when the admin bypass is switched off.
Two OAuth failure paths interpolated the raw token object into their log message. On the callback path that object is a live credential set, so a provider returning no user data wrote an access token, and usually a refresh token, straight into the application log.
Both messages now log without the payload. The token-exchange failure keeps its error level and its client_id binding and reports the provider's error description instead of the raw response body, which by that branch's own condition never contained an access token anyway. The callback failure keeps its warning level and identifies the provider, matching the other failure logs in that handler.
The apt layer of the Docker image shrinks by about 60 MB. python3-dev installs Debian's own interpreter with its headers, and nothing in the image uses it.
The image's Python is the /usr/local build from the base image, which ships its own headers, and it is also the interpreter that installs tool and function requirements at runtime, so Debian's headers were never on the include path of anything built in the container. The zlib headers python3-dev pulled in stay through libmariadb-dev, which also brings the OpenSSL headers, so the optional mariadb connector still builds; the only other header package that goes with it is libexpat1-dev, which nothing in the pinned tree builds against.
Part of #29721.
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
* 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.
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