Every message posted in a channel waited until the server had re-checked the login of every online member of that channel, one connected session at a time with a few database lookups each, before the post went through. In a large channel that added seconds to every single message. The result only decides who gets a notification, and the message is already delivered to everyone before this step, so the check now runs right after the post returns, as the first step of the work that sends notifications and bot replies. Members are re-checked exactly as before, including disconnecting any session whose login is no longer valid.
Postgres 16, standard channel, median time for a post to return:
| Online members | Before | After |
| --- | --- | --- |
| 100 | 0.37 s | 0.03 s |
| 300 | 1.6 s | 0.05 s |
| 1,000 | 3.4 s | 0.10 s |
Since 0.12.0, every group save and every member add or remove made the server recheck access for every chat that any online user had open in a browser tab, one database lookup at a time, before the save finished. With a few thousand people online, saving a group took several seconds, and every one of those open chats reloaded its contents afterwards. When a group change can affect what someone may see, the server already makes that person's browser reconnect, and an open chat checks its access again on reconnect, so group changes now skip the recheck for everyone else. Folder and chat sharing changes keep it.
Postgres 16, 2,000-member group, one open chat per online user:
| Online users | Saving the group without permission changes, before | after | Adding a member, before | after |
| --- | --- | --- | --- | --- |
| 1,000 | 2.0 s | under 0.05 s | 2.2 s | under 0.05 s |
| 3,000 | 6.7 s | under 0.05 s | 6.6 s | under 0.05 s |
When a group change affects its members (new permissions, parent group or default models, or deleting the group), the server makes every member's browser reconnect so it picks up the new permissions without a page reload. To find each member's connections it read through the whole list of connected users again for every single member, so saving a 2,000-member group with 3,000 people online spent almost 9 seconds on this with Redis as the websocket manager, and about 1 second without. It now finds all members' connections in one pass.
Changing one permission on a 2,000-member group, time the save spends making members reconnect:
| Online users | Redis, before | Redis, after | No Redis, before | No Redis, after |
| --- | --- | --- | --- | --- |
| 1,000 | 2.8 s | 0.15 s | 0.09 s | <0.01 s |
| 3,000 | 8.7 s | 0.28 s | 1.2 s | <0.01 s |
Importing a model JSON on the Workspace Models page as a regular user skipped any model whose ID already belonged to another user's model they cannot edit, or to a model from one of the instance's connections such as OpenAI or Ollama. There was no message, so the model just never showed up. The import now stops at that model with the error "Uh-oh! This model id is already registered. Please choose another model id string."; the models before it in the file are imported and the ones after it are not. Admins, and users who can edit the existing model, are unaffected and still overwrite it.
The Docker image now runs Python 3.12 on Debian 13 (trixie) instead of Python 3.11 on Debian 12, and the pip install docs recommend 3.12. Image size stays about the same: the slim image is 1 MB smaller and the standard image is about 50 MB larger, mostly from the newer ffmpeg and pandoc that Debian 13 ships. Admins should know that custom functions, tools and anything installed with pip inside the container now run on Python 3.12, so a package that only has builds for 3.11 needs a newer version. Installing with pip still works on both Python 3.11 and 3.12.
Picking a model control option, such as a Thinking level, needed both the Allow Chat Controls and Allow Chat Params permissions, so admins had to hand out the Advanced Parameters panel just to let people pick a preset. Model control picks now work for anyone with access to the model and are saved per model to their account. Allow Chat Controls still hides the Chat Controls panel and Allow Chat Params still hides the Advanced Parameters panel, in the chat and in personal Settings. Parameters a person saves to their account are no longer filtered by these two permissions, since any chat message could already set any parameter without them.
Fixes#32626
A self-hosted MinerU 4.x server started with an API key rejected every document with a 401, even with the key filled in under Documents settings, because local mode never sent it. The key is now sent with the document requests to a 4.x server whenever one is set. Servers without a key are unaffected.
Fixes#32836
A model connection with its auth type set to OAuth only forwarded the user's sign-in token for requests coming from the browser. The same user calling the API with their bearer token or an API key sent nothing upstream, so an SSO-protected model server rejected every API call and its models could not be listed or used. These connections now fall back to the user's latest sign-in session the same way tool servers already do.
Fixes#32820
With OAuth 2.1 (Static), when the authorization server did not accept the client secret as a form field, Open WebUI used whichever auth method the server's metadata listed first. X Ads lists "none" (public client, no secret) first, so the token exchange went out without the configured client secret and X rejected it with "Missing valid authorization header". When the server supports HTTP Basic auth, the secret is now sent that way, no matter which method it lists first.
Fixes#32813
Context compaction summarizes older messages and keeps the most recent ones verbatim, splitting the chat at a user message. It skipped the chat whenever the first follow-up message sat too close to the end to keep the share set in the Retained Messages admin setting. So a fresh chat with one question and one reply carrying a lot of tool output never compacted: the follow-up failed with a context length error, and regenerating or editing that follow-up failed the same way. Now compaction splits at the first follow-up message in that case, keeping fewer messages than Retained Messages asks for, so the chat keeps working. Chats where a split already met that share compact as before, and compaction between tool calls is unchanged.
Fixes#32781
When a tool on a server built with the standalone FastMCP package returned a list, the model got the list twice, once as plain text and once as structured data, so every list result cost two to three times the tokens. That package sends the whole list as a single piece of text, a format Open WebUI did not recognise as a duplicate. The list now reaches the model once, and structured data that the text does not already contain is still passed along.
Fixes#32719
When the voice provider ended a realtime call, the overlay sometimes said "Voice connection failed" instead of "Voice provider connection closed". It happened when microphone audio reached the server at the same moment the provider hung up, so forwarding that audio failed and the generic error was shown. Anything that fails to reach the provider because it just closed the connection now reports that the provider closed the call.
Fixes#32729
When a model sent the ask_user questions as anything other than a list, for example a JSON-encoded string, the tool answered "ask_user requires 1-3 questions." The model had sent the right number of questions, so it did not understand the error and often repeated the same call in a loop. It now gets a separate error saying the questions must be an array of question objects, and the count error only appears when the count is actually wrong.
Fixes#32662
Closing a page while the server was still sending it data (a file's processing status, a chat export) could interrupt a database read halfway. On SQLite the connection used for it was then never given back, and a later unrelated request could hang forever when that connection was finally cleaned up; once enough were lost, every request needing the database waited 30 seconds and failed with a 503. The connection is now kept and rolled back instead of being thrown away inside the interrupted request, and the session is closed once instead of twice, as either alone still lost connections. Closing 600 chat exports and 300 file status streams mid-load on SQLite used to lose 60 (the whole pool in that test) and 56 connections, now none; Postgres lost none before or after.
Fixes#32609
With ENABLE_FORWARD_USER_INFO_HEADERS turned on, voice calls in Realtime mode connected to the configured realtime provider without any user info headers, so a gateway in front of the provider could not tell which user was on the call. The call connection now sends the same user headers (or the signed user JWT, when configured) as chat requests and realtime text-to-speech already do.
When the model asked the user a question, or a tool call was approved or rejected, the reply was saved in a way that broke adding anything to it later in the same turn. If the model then generated an image, the image was created but never shown, the tool returned "unsupported operand type(s) for +: 'NoneType' and 'list'" and the model kept retrying, generating more hidden images. Progress status updates for that reply were also silently dropped. Both now work after a question or tool approval.
Fixes#32678
Calling the embeddings API with a model from a connection that has a Prefix ID sent the prefixed model name to the provider, which rejected it as an unknown model. The prefix is now removed before the request goes out, the same way chat requests already do it, so every model on such a connection works through the embeddings API again.
Fixes#32715
With Redis, if every tool server was unreachable when Open WebUI loaded the tool server list, it cached an empty list and served it forever. The tools stayed missing from chats and the tool list after the servers came back, until a restart, a save in the tool server settings or a manual delete of the cached Redis key. An empty cached list is now only trusted while no OpenAPI tool server is enabled, so the next request that loads tools fetches the servers again, the same way terminal servers already behave. While an enabled server stays unreachable, each such request tries it again and can wait for its timeout.
Fixes#29573
Adding a custom parameter named tools to a model, for example to turn on a provider's native web search, made Open WebUI drop all of its own tools for that model: notes, knowledge, tasks and any tools attached to the model stopped working, and attached files lost their context. Those tools now stay, and the model's custom tools are sent alongside them. API calls that pass their own tools, or an empty list to turn tools off, behave as before.
Fixes#32528
Files read from a URL are sent to a document parser based on their file name. An arXiv link like arxiv.org/pdf/1706.03762 has a dot in the paper ID, so the name was read as having the extension ".03762", the PDF went to the plain text reader and the model got raw PDF bytes. When the part after the dot is not a known file type, the extension now comes from the file type the server reports, so these links are read as PDFs. Files the server only labels as generic binary keep their name as before.
Before Open WebUI reads a URL for the model, it checks whether the URL is a web page or a file. That check went out without the user agent, browser headers and SSL verification setting the web loader uses, so sites that block unknown clients (Wikimedia, for example) refused it. The URL was then treated as a web page and the model got the raw PDF bytes instead of the document text. The check now sends the same request as the web loader, so it gets the same answer and files go to document extraction again.
The prompt and tool editors resend every stored grant on Save, and the
update routes ran that whole list through filter_allowed_access_grants.
A collaborator without the public sharing permission therefore had the
owner's Public grant stripped, and the stripped list replaced the stored
one, so one Save made the prompt or tool private for everyone else.
Keep grants that are already stored and filter only the ones the save
adds, as the model update route has done since 754c4b576.
Since cloned and forked chats keep each message's original author, a
copy of someone else's shared chat kept the sharer as the author of
every prompt and reply. Regenerate and Continue in the copy then failed
with "You can only regenerate your own messages.", and edits to those
messages were dropped on save, although the copy belongs to the person
who made it.
When a shared chat is cloned, or forked by someone other than its owner,
give the source owner's messages to the new owner, the same per-message
loop shared_chat_response uses to set authors. Messages written by other
participants of an Allow replies chat keep their authors.
When Open WebUI authenticates to the OAuth provider with client_secret_basic, either because OAUTH_TOKEN_ENDPOINT_AUTH_METHOD is set to it or because an MCP tool server's OAuth provider only offers that method, sign-in sent the client ID and secret in an HTTP Basic header but the token refresh always put them in the request body. Providers that only accept Basic rejected every refresh with invalid_client, so once the access token expired Open WebUI deleted the user's saved OAuth tokens, and model connections and tool servers set to forward the user's OAuth token stopped working until the user signed in again. The refresh now authenticates the same way sign-in does for client_secret_basic, with the credentials only in the Basic header. Setups using client_secret_post, or leaving the setting unset, still send the credentials in the request body exactly as before.
Fixes#32506