On Windows hosts the built-in code interpreter fails immediately with "Failed to fetch dynamically imported module: .../pyodide/pyodide.asm.mjs", and the browser console shows the server answered with a MIME type of "text/plain". Code execution is unusable for those users.
Python's mimetypes module reads the Windows registry after loading its own table, so a stray registry entry silently replaces the correct type for an extension and Starlette then labels the file with it. Browsers enforce strict MIME checking for module scripts and streaming WASM compilation, so the pyodide loader gets refused. The same workaround already existed for .js; this extends it to the two other extensions pyodide ships, and moves it out of the frontend-build branch so the unconditionally mounted /static assets are covered as well.
Fixes#29133
The Redis-backed model registry skips its write when the content signature matches what is already stored. That skip has never worked across processes. Two of the values it hashes come out of Python sets, and set iteration order varies with each process's hash seed, so every worker computed a different signature for identical content and every worker rewrote the whole registry on every refresh.
Sorting both makes the signature depend on content alone. Measured on a 120 model registry, 522 KiB serialized: a refresh whose content already matches drops from GET, HKEYS, HSET and SET at 5.1 ms to a single GET at 2.2 ms per worker, and the 522 KiB write leaves the wire entirely.
Verified across 12 child processes with 12 distinct hash seeds: 12 different signatures before, 1 after. Filter execution order is unaffected, because the filter pipeline re-sorts by priority and id before running.
On Redis Cluster deployments the stop button never stopped a running response when the request landed on a different instance than the one streaming it. The pub/sub listener that carries the stop signal between instances never managed to subscribe, so the command was published to a channel nobody was listening on.
The listener subscribes through a cluster client that connects lazily, and redis-py resolves the pub/sub node from a slot cache that is still empty at that point, which fails with a bare KeyError. Awaiting initialize() first fills that cache. It is a no-op on standalone and Sentinel clients, so nothing has to branch on the deployment type, and it stays inside the reconnect loop so a failover refreshes the cache instead of resubscribing against a stale one.
Before 0.11.1 the listener died on that first exception and cross-instance stop never worked at all. The reconnect loop added in 0.11.1 turned it into a startup window plus KeyError retry spam in the logs. Reported upstream as redis/redis-py#4296.
Fixes#19840
process_pipeline_inlet_filter() and its outlet counterpart construct and tear
down an aiohttp ClientSession, with its own connector and cookie jar, on
every chat completion and every task generation request just to iterate an
empty filter list. On deployments without pipelines, which is the default,
that is wasted setup on every message.
Both functions now return the payload untouched before the session is
created when there is nothing to call. The per-call saving is small, a few
microseconds of object construction per request on the pinned aiohttp; the
point is that requests stop paying setup for a feature that is not
configured.
* fix: stop streaming responses breaking on a duplicate output key
With reasoning-capable models the chat froze mid-stream: the first chunk of the answer appeared, nothing followed, and the whole message only showed up once generation finished. The browser console showed a Svelte each_key_duplicate error.
When a stream event addresses an output slot past the end of the array, the missing slots were filled with the event's own item, id included, so a gap of two left two entries claiming the same id. The next chunk for that item was matched by id, landed in the first of the two, and the rendered list ended up with two items sharing a key, which Svelte refuses to update.
Only the addressed slot now takes the event's item, and the slots before it are anonymous placeholders. Replayed the reported event sequence against the real code: keys are unique again and the chunks stay in order instead of being split across the copies.
* fix: stream reasoning deltas when the provider also sends reasoning_details
Providers such as OpenRouter emit reasoning_details alongside the reasoning
text on the same delta. Merging those details cleared the pending event
unconditionally, discarding the response.reasoning_text.delta that had just
been built, so the client received no reasoning until the response completed
and the thinking block only appeared after generation finished.
The event is now only dropped when the details were all there was to report.
Details persistence is unchanged.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014uuEg4AXPs9zE3vVUfN1Fj
---------
Co-authored-by: Claude <noreply@anthropic.com>
After a tool call, the model's thinking was streamed into the chat as if it were the main response, and only jumped into the collapsed Thoughts section once the turn finished. Every further tool call repeated it.
Each tool round appended an empty placeholder message item to the output and sent it to the browser, then dropped it again from the copy used to offset the next round's item indices. The browser therefore held one item more than the backend counted, so the first thinking chunk of the next round was written into that leftover message item and rendered as normal text until the finished output replaced it.
The placeholder is removed. It was never needed: a message item is already created when actual content arrives, and dropping it also stops an empty assistant message being sent back to the model on the follow-up request.
Co-authored-by: Claude <noreply@anthropic.com>
Any `<$...>` run in a chat message was treated as an inline skill mention and removed before the request reached the model, so text like `<$(=MonthStart($(vMaxMonthEndINC)))"}, [Registration day] >` silently vanished mid-message and the model only saw the part before it.
The mention regexes accepted any character except `|` and `>` as the skill id, so they matched far more than real mentions. Skill ids are already validated as `[a-z0-9_-]+` when a skill is created, so both regexes now require that charset. Ordinary text passes through untouched while `<$id>`, `<$id|Label>` and `</id|Label>` still resolve and strip as before.
Verified against the reported message (now preserved verbatim) and the three mention forms.
Co-authored-by: Claude <noreply@anthropic.com>
Every chat request hands each builtin tool a fresh copy of its cached spec, because callers mutate what they get. That copy was a full deepcopy of a nested dict, repeated per tool per message.
The builder now caches the spec already serialized, so a request only parses it back. Parsing is what produces the independent tree callers mutate, and the cached value becomes an immutable string, so a request can no longer reach the cached object at all.
Measured on CPython 3.12 with a 1.1 KB spec and 20 builtin tools per request:
| | before | after |
|---|---|---|
| stdlib json, the default | 276.2 us | 66.4 us |
| orjson | 279.5 us | 37.5 us |
Builtin specs are plain JSON by construction: pydantic normalizes every default before it reaches the schema, so a tuple, set, enum or datetime cannot appear in one, and an unserializable default is dropped rather than embedded.
"RAG_METADATA_MAX_VALUE_CHARS" ships unset, and unset means no bound at all, so the limit only protects the deployments that already knew to configure it. A small Office document is a zip archive, and one crafted to expand enormously during extraction can turn a few hundred kilobytes into gigabytes of metadata held in memory; uploading it a handful of times is enough to exhaust a server and take Open WebUI down with it.
When no explicit limit is configured, the bound now follows "RAG_FILE_MAX_SIZE" instead of being absent, on the reasoning that a document cannot legitimately carry more metadata than the file itself is allowed to be. That keeps the number from being an arbitrary guess: it is whatever the administrator already decided an upload may weigh. Setting "RAG_METADATA_MAX_VALUE_CHARS" explicitly still wins, and a deployment that leaves both unset is unchanged, which is the same posture the upload limit itself takes.
"RAG_FILE_MAX_SIZE" is in MB and is treated as unset when it is zero, matching how the document loader already reads it.
Every streamed delta saves a snapshot of the in-progress response so a reconnecting client can resume it, and each save rebuilt the assistant text from scratch. On the Chat Completions path that re-joined every accumulated chunk, including on saves carrying no new text, so a long answer followed by a large tool call re-joined the whole answer once per argument chunk. The Responses API path never collects those chunks and reads the text back out of the output items instead, where the blank check copied it in full every time.
The joined string is now kept and reused until another chunk arrives, since content_parts is only ever appended to; the nonlocal declaration that suggested otherwise was already dead and is dropped, and inlining the single-use helper removes an unreachable branch with it. The blank check in get_output_text now tests the text rather than allocating a stripped copy of it, which is equivalent for all twelve of its callers. Text streaming on the Chat Completions path is unchanged, since a text delta always appends before it saves.
| stream | before | after |
| --- | --- | --- |
| 20k-char answer, 2000 tool-argument chunks | 21.4 ms | 0.06 ms |
| Responses API, 40k deltas, 200k chars | 80.7 ms | 50.5 ms |
Without Redis nothing extra is retained, since the snapshot store already held that string; with Redis one copy of the response text stays alive while the stream runs.
Tag detection for the code interpreter ran regardless of the tool-calling mode, so a model in Native (Agentic) Mode that emitted <code_interpreter> blocks in ordinary reply text had that code sent to the executor. Native mode never teaches the tag format and exposes execute_code as a builtin tool, so the parser had nothing legitimate to pick up there.
Gates detection on the legacy mode, matching the condition that already decides whether the tag prompt is injected at all. The five authorization checks are unchanged, and native mode keeps executing through the tool.
Deployments on native mode whose models emit the tags unprompted will now see them rendered as text.
Some providers send one very large piece of a streamed answer in a single go: a long reasoning trace, a code execution result, a turn with many tool calls, or a response echo carrying a big tool list. Anything past 128 KB in one line killed the chat mid-answer with a misleading `400, message: Got more than 131072 bytes when reading`. Nothing was rejected upstream, that is our own reader giving up on an oversized line.
Open WebUI already had code that assembles lines itself with no such limit, but it only ran when CHAT_STREAM_RESPONSE_CHUNK_MAX_BUFFER_SIZE was set. Unset is the default, and in that case the raw capped reader was used instead, so a default install always broke. That path now always assembles lines, and the setting goes back to being what its name says: an optional cap, off by default. It applies to the Ollama stream as well, since both now share the same reader.
The assembly loop only splits once a line actually completes, because the old one re-concatenated and re-split the whole buffer on every network chunk. Without that, allowing long lines would have traded an error for multi-second event loop stalls.
| | 20 MB in one line | 200k small lines |
| --- | --- | --- |
| before | 4249 ms | 27.3 ms |
| after | 37 ms | 25.2 ms |
* fix: apply the SSRF checks to redirect targets on every web fetch path
Two guards protect server-side fetches: a private-IP check and the operator's `WEB_FETCH_FILTER_LIST`. Neither reached a redirect hop on the aiohttp paths, and the filter list never reached one on the requests paths either.
aiohttp answers IP-literal hosts itself without consulting a resolver, so `_SSRFSafeResolver` was never invoked for a hop such as `http://169.254.169.254/` and the private-IP check simply did not run. With redirect following enabled, a submitted public URL that redirects to an IP literal reached loopback, RFC1918 and cloud-metadata addresses, and the response body was returned to the caller. The filter list was consulted only in `validate_url`, on the originally submitted URL, so a redirect to a filter-listed host was fetched without it ever being applied.
`_SSRFSafeResolver` is replaced by `_SSRFSafeConnector`, which hooks `_resolve_host` so the IP check also covers the IP-literal shortcut and both DNS cache paths. The filter list moves to a per-request hook on each transport, `connect()` for aiohttp and `send()` for the requests adapter, because those see the request destination: at the connection layer a proxied request presents the proxy's host, and a pooled connection skips resolution entirely. This covers every hop, including redirects, on all five aiohttp call sites and both requests sessions. The Playwright loader already validated each hop and is unchanged.
Both gaps required `AIOHTTP_CLIENT_ALLOW_REDIRECTS=true`, which is not the default.
Two behaviour changes for operators. The filter list now applies to redirect targets rather than only to submitted URLs. Under a forward proxy it is evaluated against the request destination instead of the proxy, which also fixes allowlist entries rejecting every fetch in proxied deployments.
* refac: match the web fetch filter list against resolved addresses
The filter list is now evaluated against the hostname together with the addresses it resolves to, at URL validation and on each connection, on both transports. An IPv6 address is also matched by the IPv4 address it carries.
* refac: screen outbound fetch addresses against reserved ranges ipaddress misses
`ipaddress.is_global` was the only test behind the web-fetch address check, and it answers a narrower question than "may we fetch this". Several special-purpose ranges are globally routable by registry while nothing on them is a legitimate destination, so they passed. Classification now screens those ranges on top of `is_global`, and applies the same screen to the IPv4 address embedded in an IPv6 transition encoding rather than only to the literal. All three checkpoints share the predicate, so they all inherit it.
The range list is the exact complement of what CPython's `ipaddress` already models, checked entry by entry against both IANA special-purpose registries. Prefixes IANA marks globally reachable are deliberately left out, so no real destination changes behaviour. Verified against 31 addresses covering every entry, their transition-encoded forms, and public controls in both families: 31/31 expected after, 18/31 before.
* refac: match web fetch filter entries that name an address or a range
A filter entry that parses as an address or a CIDR range is matched by containment rather than by DNS label suffix, so a range covers the addresses inside it and an address matches however it is spelled. A range entry previously matched nothing at all, silently.
The built-in list gains the special-purpose networks that ipaddress.is_global reports as reachable while nothing on them is a legitimate destination, so taking an address out of reach is a WEB_FETCH_FILTER_LIST change rather than a release. Those entries hold whether or not local web fetch is enabled; the private-address rule still follows the toggle.
Deleting an external knowledge base now clears its connection only when an admin removes the last knowledge base referencing it, matching the connection delete route.
The binary branch of the web fetch read the entire response body into memory
before writing it out. It now streams in blocks, applies the configured file
size limit the same way the sibling URL endpoint already does, and removes the
temporary file when a download fails partway instead of leaving it behind.