Asked to add a section or change a few lines, models answer with a whole-note
replace_note_content call and the rest of the note is gone, with nothing to
undo because the editor only records versions on chat inserts. The tool
already supports replace_range operations with an expected guard, but its
description never said when to use them or how the offsets work, so models
defaulted to sending the whole note back.
The docstring, which is the description every model receives for this tool
in note chats and normal chats alike, now states the preference for range
edits and the offset, overlap and expected rules the handler enforces.
Verified the text lands in the generated tool spec unchanged and that range
edits, the expected mismatch rejection and whole-note replace behave as
described against a sqlite data dir.
Fetching a single URL through the Playwright loader (fetch_url tool, a URL attached to a
chat, the process/web endpoint) never returned when the page opened a WebSocket. The worker
thread stayed at 100% CPU for the life of the process, and every further hit cost another
core, so the whole instance got slow. Web search was unaffected, it uses the async loader.
The sync loader's websocket route handler called the synchronous close(). Playwright runs
websocket route handlers directly on its dispatcher fiber, so that call waited on the very
loop it was blocking and busy-spun forever. The handler is now a no-op: a routed socket only
reaches the network when the handler asks for it, so the page still cannot dial out, and
nothing in the handler waits on the dispatcher any more.
Aborting the upgrade request from the HTTP route handler instead does not work, page.route
never sees WebSocket handshakes and the connection goes through.
Fixes#30024
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/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
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_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
When Redis is unreachable at shutdown, the socket cleanup tasks do not stop. release_lock is a bare eval called from the finally of both periodic_session_pool_cleanup and periodic_usage_pool_cleanup, so it raises there and replaces the CancelledError already in flight. The usage task's except Exception then catches the Redis error and carries on reaping after shutdown cancelled it, and the session task ends with a ConnectionError in place of its cancellation.
release_lock now logs the failure and returns. aquire_lock sets the key with ex=self.timeout_secs and renew_lock re-expires it with the same value, so a release that never lands costs at most one lock timeout before another node can take over.
The except names both RedisClusterException and RedisError because the cluster-only types subclass Exception directly, and redis_cluster is a supported configuration, so RedisError alone would miss the outage on a cluster. Genuine bugs still propagate.
Verified against a real Redis: acquire, refusal while held, renew, compare-and-delete release and non-owner release are unchanged, a populated pool reaps identically with identical lock TTL lifecycles, and both tasks now cancel cleanly where before one kept running and the other died with the wrong exception.
An authenticated socket client can grow Open WebUI's memory without bound by sending ydoc updates for an empty document id. create_task files every task under item_tasks[id] whatever the id, while cleanup_task removes it only for a truthy id, so each update leaves a uuid behind for the process lifetime. normalize_document_id passes an empty id through, and such an id also skips the note access check.
create_task now files the task only when an id is provided, which is what its own comment already described and what the rest of the file does: redis_save_task and redis_cleanup_task both guard on a truthy item id, and stop_task normalizes a falsy one away. Nothing is filed, so nothing leaks, and cleanup_task's existing guard correctly no-ops.
This also settles a disagreement between the two backends. For an empty id, list_task_ids_by_item_id, has_active_tasks and stop_item_tasks answered one way with Redis and another without it; they now match. The visible consequence is that an instance without Redis no longer cancels a pending save for an empty document id, which is how Redis instances already behaved.
Measured over 300 calls with an empty id: 300 stale entries before, none after. Behaviour for a normal id is unchanged, including ordering, cancellation and key removal when the last task finishes.
A slow Redis freezes the whole worker during sign-in, not just the user signing in. RateLimiter held a synchronous redis-py client and signin called is_limited inline from a coroutine, so every attempt did blocking round trips on the event-loop thread, with REDIS_SOCKET_TIMEOUT defaulting to None so nothing bounded the wait. Its Redis methods are now async and take the handle as their first argument, and both handlers pass request.app.state.redis, the async client the lifespan already creates. Building one in the limiter instead would pin its pooled connection to the first event loop that used it.
Without Redis, which is the default single-instance setup, the fallback store leaked. It was keyed by the rate-limit key and pruned a key's expired buckets only when that same key was checked again, so a login email never seen again was never reclaimed, and that email comes straight from an unauthenticated request body. It is now keyed by bucket, so one prune drops every key an expired bucket held, and it lives on the instance: pruning uses the per-instance num_buckets, so a shared store would let a limiter with a short window delete buckets a longer-windowed one still needs.
With a Redis costing a second per call, the widest event-loop tick gap drops from 2.010s to 0.010s and a concurrent request is answered at 0.05s instead of 2.05s, at no cost to the caller's own latency. Across 20,000 distinct keys the store goes from 40,000 entries and 6.4 MB, growing linearly, to a flat 1,004 entries and 100 KB. Rate-limiting decisions are unchanged across 700,000 randomised calls over 14 window, bucket and limit combinations, against a real Redis and the in-memory fallback alike, and sign-in still returns its first 429 on attempt 16.
Two behaviour changes worth naming. Pruning is now global rather than per key, so a wall clock that jumps forward past a full window and back forgets a hit it previously kept. The two limiters also stop sharing a store, which previously let a sign-in attempt with an IP-shaped email touch the token-exchange limiter's counters.
A single Redis blip permanently stops orphaned websocket sessions from being reaped. periodic_session_pool_cleanup acquires its lock outside the try, and that try has only a finally, so the first timeout or connection reset ends the coroutine for the life of the process. The session pool then only grows, and the sole trace is one "Task exception was never retrieved" at shutdown.
The loop body gets the same try/except Exception its sibling periodic_usage_pool_cleanup already has, which also brings the lock acquire inside the guarded region. The task now logs, releases the lock and retries after the existing delay, so another node can take the lock over meanwhile.
The diff reads long because the body is re-indented one level; nothing changes beyond indentation and the four added lines. Only Redis deployments are affected, since the lock functions are lambda: True otherwise.
Verified by injecting a ConnectionError at each of the four failure points (acquire, renew, the batch scan, the reaping delete), against a real Redis as well: the task survives all four and keeps retrying, where it previously died on the first. Reaping results, lock acquire and release counts, and cancellation at shutdown are unchanged.
Any signed-in user can grow Open WebUI's memory without bound by posting model entries with invalid profile image URLs. ModelMeta's validator keeps a set of every distinct rejected value so it warns about each one once, storing the full string with no size cap. FastAPI validates the request body before create_new_model reaches its workspace.models permission check, so the value is retained even when the caller is refused with a 401.
The set and the warning it served both go away; the validator clears the value exactly as before. The warning named no model and truncated the value at 80 characters, so it identified nothing. models/users.py swallows the identical ValueError and substitutes a fallback with no logging, so silence matches the neighbouring code.
Measured over the real create route with distinct 4KB invalid values: 8.9 MB retained at 2,000 values and 33.7 MB at 8,000 before, flat at 1.1 MB after.
Deleting a tool or a function leaves its full source text in memory for the life of the process. Each delete handler pops the module cache and leaves the matching content cache untouched, so the code of every plugin ever deleted stays resident.
Both handlers now pop the content cache next to the module cache. Measured over 200 deletes of an 8.8KB plugin: 1.78 MB of source retained per kind before, nothing after.
This is memory only, never a stale module. A cache hit needs the id present in both caches and delete already popped the module cache, so the leftover source could not have produced one.
The two routers are one change because it is the same pair of lines at the same point in sibling handlers, with no per-site reasoning and no reason to revert one without the other.
The embed flag defaults to off and is now forwarded through every markdown render path, including the standalone tool-call branch, colon fences and alerts, so embeds render only where a call site opts in. Assistant responses opt in for the viewer's own chat; channel messages pass it explicitly off.
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) Waiting to run
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) Waiting to run
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) Waiting to run
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) Waiting to run
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) Waiting to run
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) Waiting to run
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) Waiting to run
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) Waiting to run
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) Waiting to run
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) Waiting to run
The model listing routes accept the backend index as a path segment and as a query parameter on the index-less sibling route, so the check now runs in the handler and applies to both forms.
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) Waiting to run
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) Waiting to run
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) Waiting to run
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) Waiting to run
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) Waiting to run
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) Waiting to run
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) Waiting to run
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) Waiting to run
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) Waiting to run
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) Waiting to run
Pressing stop saved the assistant message as finished while its output items were still marked as running, so the affected block kept showing "Thinking...", "Analyzing..." or "Executing...". That state was written to the database, so it came back after every reload. A tool call cancelled mid-execution was affected as well: it is marked completed as soon as its arguments finish streaming, well before the tool returns, and the client reads that as still executing until the result item arrives.
Cancelling now closes those items before the message is saved, marking them incomplete, which is the status the client already treats as finished without flagging an error. Tool calls waiting for approval are left untouched so their approval prompt survives a reload. The corrected output is sent on the chat:tasks:cancel event that was already being emitted, so an open tab settles immediately instead of only after a reload.
Emitting a chat:completion event instead would have fired response auto-copy, text to speech playback and the chat finished listeners on a response the user had just cancelled.
Verified on a running instance against a mock upstream: cancelling mid-response leaves no item marked as running in the saved message, and the identical run on unpatched code leaves one.
Fixes#29281
Syncing a knowledge base from a local directory only ever showed a
generic "Error accessing directory" toast when a per-file read
failed, and production builds strip console.error, so nothing else
recorded the cause either. On Windows this hides a real Chromium bug
where files with very long absolute paths throw a NotFoundError from
the File System Access API, leaving no way to tell which file failed
or why.
Both per-file read paths (the directory picker and drag-and-drop) now
wrap read failures with the file's relative path and the underlying
browser error before they reach the toast, so the message actually
names the file and the reason. Reuses the existing translated toast
text instead of adding a new i18n key, so the fix doesn't orphan
existing translations of that string.
When a tool returns a base64 image, Open WebUI only moves it out of the model's context if the entire result is that image, or if the tool is MCP. An image sitting inside a returned object or a list was serialised into the tool message instead, so a single screenshot could cost hundreds of thousands of tokens, push out the rest of the conversation and leave the model answering from garbage.
Any string in a tool result that is exactly one image data URI is now moved into the result's files wherever it sits in the structure, and replaced with a short marker. The model gets the image as an attachment rather than as text, and it renders for the user instead of being dropped.
Detection is deliberately limited to values that are entirely a data URI. Scanning inside longer strings was tried and abandoned: a payload wrapped over several lines, or one followed by text, gets cut short, and shipping a truncated image is worse than the bloat because the provider rejects the whole request.
This also fixes the OpenAPI branch removing entries from the list it was iterating over, which skipped every second data URI and left it in the model's context.
Fixes#29208
The three community origins were repeated inline in five window message handlers and now come from a single COMMUNITY_ORIGINS constant in constants.ts.
The sync stats modal uses that same list in both directions: it reads messages only from a community origin, replies to the origin the message came from, and names the community origins as the targets of the messages it sends. Its chat id goes into the request as one encoded path segment.
Link URLs rendered from markdown, citations and web search results now pass a scheme check before they reach an anchor. A new safeLinkUrl helper sits beside isValidHttpUrl and keeps http, https, mailto, tel and relative URLs, returning undefined for anything else so the label renders without a link. Markdown links with an unusual scheme (ftp, sms, and application deep links such as obsidian or vscode) render as plain text from now on.
The citation checks move off a substring test for "http" onto isValidHttpUrl, which is what Citations.svelte already uses for the same question. That also drops two long-standing quirks: an uppercase HTTP:// source never rendered as a link, and a filename merely containing "http" rendered as a dead external one.
Setting ENABLE_PROFILE_IMAGE_URL_FORWARDING=false stops the user and model profile image endpoints from redirecting browsers to external avatar URLs, but channel webhook avatars kept redirecting regardless. An operator who turned the setting off precisely to stop clients leaking their IP, User-Agent and Referer to outside origins still leaked all three whenever anyone viewed a channel message posted by a webhook with an external profile image URL.
The webhook profile image endpoint now reads the same setting the user and model endpoints already read, and serves the bundled default image instead of the redirect when forwarding is off. Stored URLs are untouched, so turning the setting back on restores the previous behaviour.
Verified against the real handler with seeded webhook rows: with the setting unset or true the endpoint still returns the 302 with the original Location, with it false it returns the default favicon as image/png with no Location and no header carrying the external host, and the data URI, no image and unknown webhook responses are byte identical in both states.
* fix: drop thinking blocks when converting Anthropic Messages requests to Chat Completions
Claude Code and other Anthropic SDK clients pointed at /api/v1/messages send the assistant's earlier thinking blocks back with every follow-up request. Since 0.11.0 those blocks were copied into the OpenAI assistant message as content parts of type thinking, a part type Chat Completions does not define. Strict OpenAI-compatible servers such as NVIDIA Dynamo reject the whole request with 400 "data did not match any variant of untagged enum ChatCompletionRequestAssistantMessageContent", so a conversation with a reasoning model died on its second turn. The error reports its position at the very end of the body, which made the request look cut off; it was complete.
Thinking and redacted_thinking blocks are now skipped in the conversion, which is what happened before 0.11.0. An assistant turn that held only thinking blocks is kept as an empty assistant message so the turn order survives. Native Anthropic and LiteLLM connections are unaffected because they receive the request untouched, and thinking blocks in responses are still produced.
Fixes#29799
* fix: keep signed thinking blocks when converting Anthropic Messages requests
Dropping every thinking block also removed the signed ones. Those are the blocks a gateway such as LiteLLM forwards to Anthropic, which needs the signed thinking block of the previous assistant turn when a tool-use turn continues with extended thinking. Only the unsigned blocks are the problem: Open WebUI creates them itself from reasoning_content, and strict Chat Completions backends reject them because thinking is not a content part type they know.
Unsigned thinking blocks are now dropped while signed thinking and redacted_thinking blocks are kept, the same rule LiteLLM applies before forwarding to Anthropic and the rule the native chat path already uses for Anthropic reasoning details. Backends that never emit a signature, such as NVIDIA Dynamo and vLLM, keep receiving requests without thinking parts, so the original failure stays fixed.