Commit graph

7154 commits

Author SHA1 Message Date
Classic298
60ded561a9
fix: load models before resolving automation model defaults (#30379)
Automations lost their model's tool bindings (including MCP servers), default features such as web search, default filters and terminal on the first run after a restart. The model then answered that it had no tools. Later runs and a manual Regenerate worked. Automations on the base models cache were not affected.

The run read the model from app.state.MODELS before anything had loaded it. After a restart or a connection settings save, that cache stays empty until a browser loads the model list or a chat completion runs. The completion runs only after the automation has already built its request.

execute_automation now loads the models when the cache is empty, using the same guard chat_completion uses, before either the chat or the channel target reads it. This also fixes channel automations showing the raw model ID in place of the model name on a cold cache.

Verified end to end on a restarted instance with a mock upstream: before, both chat and channel runs reached the pipeline without tool_ids or features. After, both carry the model's tools and web search, and the upstream receives the tool.

Fixes #27694
2026-09-23 23:51:29 -04:00
Classic298
744ce6cbfe
fix: send the configured USER_AGENT on the Attach Webpage pre-check (#30385)
Attach Webpage fails with 403 Forbidden on sites that reject the bare aiohttp user agent, Wikipedia among them, even when USER_AGENT is set. The web loader sends USER_AGENT, but the request that runs first to decide whether the URL is a page or a file does not, so the attachment fails before the loader is ever reached.

The pre-check now sends USER_AGENT as the request User-Agent when it is set. With it unset the request is unchanged and keeps the aiohttp default.

Verified against the real _fetch_url with https://en.wikipedia.org/wiki/OpenAI: 403 before, page detected after; USER_AGENT unset still returns the same 403 as before, and a direct PDF URL is still detected as a file.

Fixes #29617
2026-09-23 23:50:08 -04:00
Classic298
2867f7225b
refac: sync channel room on member removal (#30446)
Removing members from a group or DM channel now also removes their sessions from the channel room, matching how access grant changes are handled.
2026-09-23 23:43:20 -04:00
Classic298
443736e1fe
refactor: use secrets module for generated secret key (#30441)
Generates the default WEBUI_SECRET_KEY file with secrets.token_bytes, matching how the start scripts read from the OS random source.
2026-09-23 23:36:26 -04:00
Classic298
a973e77cfa
refac: folder file checks (#30442)
Folder file entries are also checked against the user making the change.
2026-09-23 23:36:15 -04:00
Classic298
4caf255389
fix: refresh an expiring OAuth token once across workers and replicas (#30450)
#30426 stopped concurrent requests from refreshing the same OAuth session twice, but its lock only lives inside one process. With several uvicorn workers or replicas, two requests on different workers still send the same refresh token, a rotating provider rejects the second with invalid_grant, and the session gets deleted, so the user's OAuth session is logged out again.

When Redis is configured, which multi-worker and multi-replica deployments require, the refresh now takes a Redis lock per session instead of the in-process one. Single-process deployments without Redis keep the in-process lock. The waiter re-reads the session inside the lock as before and uses the token that was just stored.

It uses redis-py's own async lock because the existing RedisLock is synchronous and never waits. The Sentinel proxy now passes `lock` through unwrapped like `pipeline` and `pubsub`; otherwise it returned a coroutine and every refresh behind Sentinel would fail.

Tested with separate OS processes on one sqlite DB, a real Redis and a rotating mock provider: 2 and 5 processes (and 5 processes x 3 requests) now cause 1 refresh, every caller gets the new token and the session is kept (before: one refresh per process, session deleted every run). Single refresh, failed refresh, valid token and the single-process path without Redis are unchanged.

Follow-up to #30426, refs #30416
2026-09-23 23:33:09 -04:00
G30
0ab3a2b335
fix: restore tag rows after unarchiving all chats and remove unused ones after deleting all chats (#30453) 2026-09-23 23:32:54 -04:00
G30
7e2bea8253
fix: load the leaderboard activity chart for model ids that contain a slash (#30456) 2026-09-23 23:32:42 -04:00
G30
95d6af44d7
fix: mark imported and cloned chats as read (#30754) 2026-09-23 23:32:14 -04:00
Classic298
9e51fe421c
fix: read S3 files with long non-ASCII names (#30418)
With S3 storage, a file whose stored name is close to the 255-byte filename limit and contains non-ASCII characters (for example a Cyrillic name of about 210-218 bytes) uploads fine, but every later read fails with "File name too long". Processing never gets the content, so the file shows as attached while the model receives no text.

The read path used boto3's download_file, which first writes to a temporary name with 9 extra characters. boto3 caps that temporary name by characters, not bytes, so multibyte names end up over the limit even though the final name fits.

The download now streams straight into the local path with download_fileobj, the same way the Azure provider already writes its local copy. That path is the one the upload just wrote successfully, so it always fits. ASCII names, key prefixes and multipart downloads behave as before.

Fixes #30409
2026-09-23 23:30:55 -04:00
Classic298
b3bb82e776
refac: scope ydoc update save scheduling to note documents (#30395)
The ydoc document update handler now schedules the debounced save only for note documents, since notes are the only ydoc documents with a save handler.
2026-09-23 23:30:21 -04:00
Classic298
abd60d33fe
fix: evaluate automation schedules on Windows with PostgreSQL (#30424)
Some checks are pending
Python CI / Ruff Format (3.11) (push) Waiting to run
Python CI / Ruff Format (3.12) (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
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 / merge (map[name:cuda suffix:-cuda]) (push) Blocked by required conditions
Create and publish Docker images with specific build args / merge (map[name:cuda126 suffix:-cuda126]) (push) Blocked by required conditions
Create and publish Docker images with specific build args / merge (map[name:main suffix:]) (push) Blocked by required conditions
Create and publish Docker images with specific build args / merge (map[name:ollama suffix:-ollama]) (push) Blocked by required conditions
Create and publish Docker images with specific build args / merge (map[name:slim suffix:-slim]) (push) Blocked by required conditions
Create and publish Docker images with specific build args / notify-helm-charts (push) Blocked by required conditions
Create and publish Docker images with specific build args / copy-to-dockerhub (, main) (push) Blocked by required conditions
Create and publish Docker images with specific build args / copy-to-dockerhub (-cuda, cuda) (push) Blocked by required conditions
Create and publish Docker images with specific build args / copy-to-dockerhub (-cuda126, cuda126) (push) Blocked by required conditions
Create and publish Docker images with specific build args / copy-to-dockerhub (-slim, slim) (push) Blocked by required conditions
Frontend Build / Unit Tests (push) Waiting to run
Create and publish Docker images with specific build args / copy-to-dockerhub (-ollama, ollama) (push) Blocked by required conditions
Frontend Build / Format & Build (push) Waiting to run
Since 0.11.4, creating or editing an automation on Windows with PostgreSQL fails with a 400, and the scheduler logs NotImplementedError on every tick, so automations do not work at all on that setup.

Schedules are now evaluated in a worker subprocess so a pathological rule can be killed after the 2s budget. On Windows with PostgreSQL, Open WebUI switches to the selector event loop that psycopg needs, and that loop cannot spawn subprocesses.

When spawning fails there, the evaluation now reruns on a Proactor event loop in a worker thread. The subprocess, the 2s budget and the kill on timeout all stay the same, and the global loop policy psycopg depends on is untouched. Falling back to a plain thread was considered and rejected: a thread cannot be stopped, so a costly rule would keep burning CPU after the timeout.

Verified with a loop that refuses subprocesses: base raises NotImplementedError, the fix returns the same results as base, still times out a pathological rule at 2s with the worker killed, and leaves no processes or loops behind under repeated and concurrent calls. Other platforms take the unchanged path.

Fixes #30400
2026-09-23 08:47:04 -05:00
Classic298
9db1a518d3
fix: refresh an expiring OAuth token once when requests race (#30426)
With an OIDC provider that rotates refresh tokens, sending a chat to a system_oauth connection often logged the user's OAuth session out. Two requests reached the refresh at the same time and both sent the same refresh token. The provider rejected the second one with invalid_grant, and Open WebUI deleted the session, so every following request lost its token until the user logged in again.

Refreshes now take a per-session lock. A request that waited for another one re-reads the session and uses the token that was just stored, so the provider sees one refresh per rotation.

Tested with real sqlite sessions and a rotating mock provider: 2 and 5 concurrent callers now cause 1 refresh, all callers get the new token and the session is kept (before: one refresh per caller, all callers got nothing, session deleted). Single refresh, failed refresh and valid-token paths are unchanged.

The lock is per process, so deployments with several workers or replicas can still race across processes.

Fixes #30416
2026-09-23 08:46:31 -05:00
Classic298
2f92635409
fix: keep the model system prompt on Ollama tool-call follow-ups (#30375)
With native function calling on an Ollama model, every request sent after a tool result was missing the model's system prompt, so the final answer ignored the model's instructions. Only other system content, such as the attached knowledge tag, was left. OpenAI connections were not affected.

Tool-call follow-ups are rebuilt from the chat's message list and skip the router's system prompt step, because the first request is expected to have already added it to that list. The OpenAI path does add it there, but the Ollama path converts the messages into a copy first and adds the prompt only to the copy, so the follow-ups never see it.

The model system prompt is now applied to the messages before the Ollama conversion, and the Ollama router is told to skip it for that request so it is not added twice. Ollama now behaves the same as the OpenAI path. Direct calls to /ollama/api/chat still get the prompt from the router as before.

Checked baseline against patched: first request and follow-up for plain, custom and arena Ollama models, with and without a chat system prompt, with template variables and on the OpenAI path. The prompt is now present exactly once on every Ollama follow-up, and nothing else changed.

Fixes #30161
2026-09-22 16:47:20 -04:00
Classic298
1a74a9f46c
perf: per-room channel delivery for the socket.io Redis manager (#28818)
Reworked from the ground up after the feedback that the registry implementation did not land. The Redis room registry and its whole recovery protocol (heartbeats, liveness keys, pruning, distrust windows, cache invalidation) are gone; the change is now ~105 lines with no state kept outside the process.

With WEBSOCKET_MANAGER=redis every emit is published on one shared channel and every instance JSON-decodes every message: a 16 instance fleet decodes each streamed token delta 16 times and 15 discard it. py-spy across a loaded fleet (16 instances, ~4000 users) puts ~31% of all active CPU samples in the pubsub listener parse chain, the largest bucket.

Room-targeted emits are now published on a per-room channel instead; every instance keeps one static pattern subscription covering all room channels and drops messages for rooms without local members by channel name, paying a set lookup instead of a JSON parse. No state leaves the process, so recovery paths and loss windows are identical to the stock manager; acks and control messages stay on the shared channel and sio.call works across instances unchanged. This is the delivery scheme the official socket.io Redis adapter for Node.js ships by default.

Enabled by default; WEBSOCKET_REDIS_ROOM_CHANNELS=false restores shared-channel-only delivery. All instances must run the same mode, so the switch rides the full-stop upgrade this release already requires for its migration; in a mixed fleet, room emits from updated instances would not reach not-yet-updated ones. Verified end to end with two instances on a real Redis: cross-instance token streams delivered with the shared channel completely silent. Ref #28173.

<!--
🚨 DO NOT DELETE THE TEXT BELOW 🚨
Keep the "Contributor License Agreement" confirmation text intact.
Deleting it will trigger the CLA-Bot to INVALIDATE your PR.

Your PR will NOT be reviewed or merged until you check the box below confirming that you have read and agree to the terms of the CLA.
-->

- [x] By submitting this pull request, I confirm that I have read and fully agree to the [Contributor License Agreement (CLA)](https://github.com/open-webui/open-webui/blob/main/CONTRIBUTOR_LICENSE_AGREEMENT), and I am providing my contributions under its terms.

> [!NOTE]
> Deleting the CLA section will lead to immediate closure of your PR and it will not be merged in.
2026-09-22 14:48:56 -04:00
Classic298
fe56ab24f3
fix: page Chroma get() so hybrid search works on collections over 32k chunks (#30368)
With Chroma as the vector DB, hybrid search on a knowledge base with more than 32766 chunks fails with HTTP 400 "Error querying knowledge base". The legacy hybrid path fetches the whole collection to build the BM25 index, and Chroma's unbounded collection.get() binds one SQLite variable per row, so any collection above SQLite's 32766 variable limit raises "too many SQL variables" (reproduced on chromadb 1.5.9 with both PersistentClient and HttpClient). Vector-only search on the same collection works, which makes it look like a hybrid-search bug.

The Chroma adapter now reads the collection in pages of 10000 rows via limit/offset and concatenates them into the same GetResult shape as before.

Verified on a 90000-row collection: every row returned exactly once with documents and metadata aligned to ids, page order stable across page sizes, empty and exactly-one-page collections unchanged, and query_doc_with_hybrid_search returns results where it previously raised. The tests repo unit suite is identical before and after.

Fixes #30351
2026-09-22 14:48:45 -04:00
Timothy Jaeryang Baek
e93a59f4dd refac
Co-Authored-By: Classic298 <27028174+Classic298@users.noreply.github.com>
2026-09-22 13:13:22 -04:00
Classic298
0a2e9a42e7
fix: detect the real image type of bare base64 generated images (#30359)
Generated images that come back as bare base64 (OpenAI b64_json, Gemini
bytesBase64Encoded and inlineData, Automatic1111) were always stored as
generated-image.png with content type image/png, even when the provider
returned JPEG or WebP, for example with {"output_format": "jpeg"} in the
OpenAI extra params. The image still rendered because browsers read the
bytes, but the download name, the served Content-Type and the type sent
along on a later image edit were wrong.

Bare base64 carries no format, so the type is now read from the bytes with
Pillow, the same way the file already inspects images elsewhere. A response
that is not an image at all now fails the generation instead of storing a
broken png. The file extension comes from the module's own extension map
first, because the Python 3.11 Docker image has no mime database entry for
WebP and would otherwise name the file generated-imageNone.

Fixes #29948
2026-09-22 11:35:59 -04:00
Classic298
6b36f620cf
fix: stop KeyError 'model' traceback on every new chat from initial title generation (#30356)
Every new saved chat logged "Error generating initial chat title" with a
KeyError: 'model' traceback. The title itself was already generated and
saved by then, so the log was misleading, and the memory settings played no
part in it. The error was silently logged at debug level since v0.10.0 and
became visible in v0.11.4 when the title path switched to log.exception.

The initial title task runs the shared background handler with a context
that has no resolved model, which the memory review step read
unconditionally. It now reads it with a default of None, which the memory
review already accepts. The title path never carries an assistant message,
so the review stops before doing any work there, and the main completion
path keeps reviewing memory exactly once per turn with the real model.

Fixes #30339
2026-09-22 11:35:14 -04:00
Classic298
f956f7adc0
fix: send only the file name to Docling instead of the full storage path (#30357)
With the Docling content extraction engine, every conversion request
carried the server's full internal upload path (for example
/app/backend/data/uploads/<id>_report.pdf) as the multipart file name.
Docling only needs a bare file name, and a hosted Docling instance has
no business learning where Open WebUI keeps its files on disk.

The loader now sends the base name of the stored file, which is what
the MinerU, Datalab and Mistral loaders already do. Nothing else in
the request or the parsed result changes, verified against a capturing
mock server before and after.

Fixes #30352
2026-09-22 11:35:05 -04:00
Classic298
6f6792d484
fix: pass MCP tool images to the model, not only to the UI (#30358)
When an MCP tool returns an image (e.g. a Home Assistant camera snapshot), the
snapshot shows up in the tool call section but the model never sees it: it
answers that there is no image. Only images arriving as inline data URIs were
attached to the model request; MCP images are uploaded to Files first and their
file URL was treated as display-only.

Now an image file item with a file URL is attached to the model request as an
input_image part in addition to staying in the tool call's displayed files. The
existing URL-to-base64 step already resolves file URLs, so the model receives
the image bytes; verified on a running instance with a mock MCP server and a
mock upstream (the second upstream request carries the byte-identical JPEG).
Inline data-URI images keep their existing model-only handling.

Fixes #30327
2026-09-22 11:34:57 -04:00
Timothy Jaeryang Baek
ee3ece1e2b refac 2026-09-21 14:43:04 -04:00
Timothy Jaeryang Baek
bf476452a8 chore: format 2026-09-21 11:35:30 -04:00
Classic298
e8c26f8394
fix: stop the background memory review when memory is switched off (#30309)
With memories disabled instance-wide, or for a user barred from the feature,
the background review still ran every interval turn: it spent a task-model
call drafting memory operations and only then failed at the write, because
the router's permission check rejected it. The review now checks the
'memories.enable' switch and re-checks the 'features.memories' permission
the same way the context-injection path already does, so a model whose memory
capability is on no longer triggers memory work that can never land.

The permission lookup costs a groups query, so it runs last, after the free
config and interval gates; those stay on every turn's hot path.
2026-09-21 11:22:21 -04:00
Timothy Jaeryang Baek
478d1785fd refac
Co-Authored-By: Classic298 <27028174+Classic298@users.noreply.github.com>
2026-09-21 11:21:42 -04:00
Timothy Jaeryang Baek
30881dbcc9 refac 2026-09-21 11:09:09 -04:00
Classic298
419d248093
refac: check the timer owner's role before running a due timer (#30220)
The due-timer executor rehydrates the owner from the database and now
verifies that the owner is still a user or an admin before entering the
chat completion pipeline, mirroring the check the scheduled-automation
executor already performs. A timer whose owner no longer qualifies is
recorded as an error instead of being run.
2026-09-21 10:46:48 -04:00
Timothy Jaeryang Baek
3fc1146c13 refac 2026-09-21 10:44:37 -04:00
Classic298
94eea41a75
fix: surface searchapi errors, news results and redirect links (#30308)
Web search via searchapi.io could come back empty or near-empty with no
hint of why: an invalid or expired API key turned into an empty result
set instead of an error, the google_news engine splits its results
between organic_results and top_stories and only the first block was
read, and google links came back as google.com/goto redirects the web
loader cannot fetch, so citations pointed at a redirect blob.

The search now reads both result blocks, asks google engines for
resolved destination links, raises on HTTP errors, carries a 30s request
timeout, skips result rows without a link, and logs the response body at
debug instead of dumping every search at info.

Fixes #30305
2026-09-21 10:44:06 -04:00
Timothy Jaeryang Baek
e8bd0661d3 refac 2026-09-21 10:30:53 -04:00
Timothy Jaeryang Baek
754c4b5762 refac
Co-Authored-By: Classic298 <27028174+Classic298@users.noreply.github.com>
2026-09-21 10:25:20 -04:00
G30
c864e3ae7c
fix: stop asking for chat variables a model's system prompt no longer declares (#30173) 2026-09-21 10:23:21 -04:00
Timothy Jaeryang Baek
a9541c18ca refac 2026-09-21 10:19:59 -04:00
Classic298
aceda892bd
perf: serve the shared model pool from a per-worker cache (#28176)
With the Redis websocket manager the shared model pool is a `RedisDict`, so resolving a model id fetches from Redis, and more than one of those fetches pulls the whole pool. Every chat request pays that latency and that traffic, and the cost grows with the number of models configured: at 120 models a request moves several hundred kilobytes to ask about models that have not changed since the last request.

The pool now keeps a per-worker cache of the hash and refetches it only when the signature key that `set()` already maintains changes. A cache may only trust a signature that describes the bytes it fetched, so every write invalidates the signature and no signature value is ever issued twice. Without a fresh token each time, a pool that flaps back to an earlier state returns to an earlier digest, so a reader that races the write keeps serving the old pool. `delete_many()` was a second hole in that: it updated a dead attribute and never touched the signature, so readers kept serving models it had already removed. A TTL cache would have been simpler, but it serves a pool it already knows may be stale and costs the same single round trip this check costs.

Replaying the real access pattern, a request that finds the pool unchanged moves about 1 KB no matter how many models are configured, over the same number of round trips as before, except after a write that leaves no signature, which holds reads at two commands until the next non-empty `set()` writes one. A request that first sees a changed pool costs one extra command, and so does rewriting the pool. Values stay cached serialized, so `items()` and `values()` still decode on every call, and each worker holds the whole pool in memory.
2026-09-21 10:15:49 -04:00
Classic298
49b25506dd
perf: stop round-tripping the whole chat to read or write one message (#28184)
Long conversations get progressively more expensive to stream into. Every message-level write reloads the entire chat JSON, walks every string in it for a null-byte sanitisation pass and rewrites the whole column, and reading a single message loads and validates the whole chat too. The websocket event emitter reads and then writes, so one message event round-trips the full conversation twice to change a few hundred bytes.

Reading one message now selects only history.messages out of the JSON column and indexes it in Python, so the rest of the chat is neither loaded nor model-validated. Writing one message now sanitises only what is actually entering the chat, plus the title column that is mirrored off the blob, instead of re-walking a conversation that was already sanitised when it was written. Legacy rows are still healed on read by get_chat_by_id, which is unchanged.

One behaviour change: chats written before the null-byte sanitisation existed can still hold null bytes in the stored JSON. Those rows used to be rewritten clean as a side effect of any message write, and are now cleaned when the chat is read instead. The visible consequence is that the message write and delete endpoints echo back the updated chat, and for such a legacy row that echo now carries the raw null bytes rather than stripped ones, until the next read of that chat heals it. A GET of the chat is unaffected, and title, the one text column that PostgreSQL cannot store a null byte in, is still sanitised on every write.

Measured on SQLite with a 10.8 MB chat (3000 messages): a single-message upsert is 227 ms before the branch and 173 ms after, and the legacy single-message read is 135 ms before and 79 ms after. The whole-blob scan the delete path used to run costs 5.40 ms on a 1.8 MB chat and 38.79 ms on the 10.8 MB one, and is gone.

Ref https://github.com/open-webui/open-webui/issues/28169

### Contributor License Agreement

<!--
🚨 DO NOT DELETE THE TEXT BELOW 🚨
Keep the "Contributor License Agreement" confirmation text intact.
Deleting it will trigger the CLA-Bot to INVALIDATE your PR.

Your PR will NOT be reviewed or merged until you check the box below confirming that you have read and agree to the terms of the CLA.
-->

- [x] By submitting this pull request, I confirm that I have read and fully agree to the [Contributor License Agreement (CLA)](https://github.com/open-webui/open-webui/blob/main/CONTRIBUTOR_LICENSE_AGREEMENT), and I am providing my contributions under its terms.

> [!NOTE]
> Deleting the CLA section will lead to immediate closure of your PR and it will not be merged in.
2026-09-21 10:11:38 -04:00
Timothy Jaeryang Baek
5fb869db22 refac 2026-09-21 08:59:39 -04:00
Classic298
488b3c571c
perf: stop note co-editing echoing every remote update back to the server (#28185)
Every client in a note re-broadcasts each update it receives, with a full content snapshot attached, and the server appends each echo to the document log and writes the note again. Traffic and note writes therefore scale with the number of people who have the note open: every extra participant adds one more full echo of every keystroke.

Remote updates are now applied with the `'server'` origin the state path already uses, which the local listener ignores, so the echo stops. The echo did carry one thing worth keeping: the receiving client holds the merged document, which the sender had not seen yet, so each receiver now sends a content-only message once the edits settle, debounced 500ms, and flushes a pending one when the editor is torn down. The backend accepts an update message with no `update` field for that case, where it previously raised and dropped the save.

Replaying keystrokes at 120ms with a real Yjs document, socket messages fall 48.8% with two clients, 65.6% with three and 79.2% with five, with byte counts tracking the same on notes up to 50KB. Server-side update appends drop by a factor of the client count. The cost is one snapshot upload per receiving client per typing pause, which the server's own debounce then collapses into a single extra note write however many people are watching. A snapshot has to come from a client because the server cannot rebuild the markdown, HTML and JSON shape the note record stores. It also moves the merge 500ms later than the echo delivered it, so if two edits cross on the wire and every editor then loses its connection and closes inside that window, the note keeps what it was last sent and one of the two edits is lost. A client still connected at teardown flushes its pending snapshot, and any other editor left in the note closes the gap.
2026-09-21 08:53:27 -04:00
Classic298
6fb68e43cb
refac(images): request an unencoded body when fetching remote chat images (#29623)
The remote chat-image fetch now asks for an identity-encoded response and skips one that comes back content-encoded.

An image whose host stores and echoes a `Content-Encoding` regardless of what the client asks for (an S3 or MinIO object uploaded with that metadata) is no longer inlined; the message is forwarded with the original URL instead, the same way an unreachable image already behaves.
2026-09-21 08:53:06 -04:00
Classic298
438d9db8db
fix: correct recurrence rule parsing for schedules and calendar events (#29262)
* fix: correct recurrence rule parsing for schedules and calendar events

An automation set to repeat a limited number of times, say ten or a hundred, was treated as a one-shot and reported no repeat interval, because any count whose digits began with a one matched a text check for the one-shot case. The scheduler already answers that question correctly by asking the rule for its next two occurrences, so the text check is gone and the count is read as the number it is.

A recurrence rule that carries its start date on the same line as the repeat text kept that date when the automation was parsed, so the schedule ran from whatever date the rule happened to carry and ignored the start the user picked. The filter that drops the start date now splits the rule on any whitespace, the same way the rule parser itself does, so both agree on where one part of the rule ends and the next begins.

The same mismatch on the calendar path anchored a recurring event to the date inside its rule, so occurrences showed up before the event had begun and at the wrong time of day. That filter splits the rule the same way now, and the series starts at the event's own start.

All three come from one place, recurrence rules being matched and cut as text. Rules written across several lines, which is what the schedule and calendar editors produce, behave exactly as before.

* refac: name rrule token vars parts to match calendar.py
2026-09-21 08:44:13 -04:00
Timothy Jaeryang Baek
0180efecf3 refac 2026-09-21 08:43:45 -04:00
Classic298
7fa8673296
fix: resolve this instance's own file URLs in edit_image regardless of the URL host (#29691)
The native edit_image tool fails with "400: [ERROR: Error loading image]" whenever the model hands it an absolute URL for an image Open WebUI already stores. Such a URL is treated as local only when its host string matches the incoming request's host exactly, so a default-port form, a container name or any host the model composed itself falls through to an outbound HTTP fetch instead. That fetch asks /api/v1/files/{id}/content without a session, gets a 401, and the user sees the generic 400.

Match the file URL on its path and let the existing local branch resolve it. Fetching that endpoint over the network can never succeed for a local or a remote instance, because it requires an authenticated user, so the host comparison only decided which way the request failed. Access control is unchanged: the local branch still goes through get_file_content_by_id, which enforces owner, admin or shared access.

Fixes #29220
2026-09-21 08:32:38 -04:00
G30
f40318c1ff
fix: keep the live prompt unchanged when a version is saved without Set as Production (#30231) 2026-09-21 08:32:02 -04:00
Timothy Jaeryang Baek
7f703dcc98 refac 2026-09-21 08:31:27 -04:00
Timothy Jaeryang Baek
dc4f5da5a0 refac 2026-09-21 08:28:36 -04:00
Classic298
9012bd153d
fix: stop sending count to the Staan search API (#30303)
Staan rejects any count other than its fixed page size of 10, so every search 400ed with the default result count of 3. The API always returns 10 results; the local results[:count] slice already applies the configured count, so the request parameter is simply dropped.
2026-09-21 08:04:26 -04:00
G30
702da1e471
fix: stop injecting memories and memory tools when memory is switched off (#30228) 2026-09-21 01:00:08 -04:00
Timothy Jaeryang Baek
97e013a661 refac 2026-09-21 00:51:28 -04:00
G30
402a187e82
fix: return no chats when a folder search term matches no folder (#30273) 2026-09-20 23:39:54 -05:00
Classic298
8e4cc946ce
fix: emit the resolved file path in terminal file events (#30282)
When a model calls display_file, write_file or replace_file_content with a relative path, Open Terminal resolves it against the session working directory and returns the absolute path, but the event sent to the browser carried the raw argument instead. The file panel matches that string against the file browser root, a relative path never matches, so the preview never opens, the panel jumps to the root and the session working directory is rewritten to the root. With the root turned off (OPEN_TERMINAL_FILE_BROWSER_ROOT=filesystem) there is nothing to clamp to and the relative string is sent to the terminal as the new working directory, moving it silently. The tool call itself succeeds either way, so the failure only shows up as a panel that will not open the file the model just wrote.

Both events now carry the path from the tool result and fall back to the argument when the result cannot be read, which is what build_terminal_file_tool_result already does for the chat file attachment. The same one-line rule is applied to the direct tool server path in the frontend, where the browser runs the tool itself and the write_file branch beside it was already correct.

Checked against a live Open Terminal: relative arguments now emit the absolute path, absolute ones are unchanged, non-existent files and inline displays still emit nothing, unreadable or error results still fall back to the argument, and run_command is untouched.

Related to #30051
2026-09-21 00:31:14 -04:00
Classic298
7fa705f3b8
feat: let operators expose chosen file metadata to the model in retrieved sources (#29696)
Custom metadata attached to a file upload now reaches the vector DB, but the model still never sees it. Both prompt-assembly paths build their output from a fixed field set: the classic RAG <source> tag carries only id, name and resource type, and the retrieval tools return only content, source and file id per chunk. A scraper that records where each document came from therefore cannot get that origin in front of the model, so answers cannot state it.

RAG_SOURCE_METADATA_KEYS names the chunk metadata keys allowed through to the model. Configured keys are emitted as extra attributes on the <source> tag and as extra fields on tool result chunks, covering both retrieval paths. It is empty by default, so nothing changes for existing deployments.

An allowlist instead of passing everything through, because chunk metadata also carries file hashes, collection names, embedding config and relevance scores, which would then be added to every retrieved chunk of every request. Values are attacker-controllable through an uploaded file, so they are escaped before they go into the tag, and a configured key can never displace a field the tag or the chunk already defines.

Reported in open-webui/open-webui#29486.
2026-09-19 17:02:59 -05:00