Commit Graph
18555 Commits
Author SHA1 Message Date
Classic298 fbc4897269 refac: resolve knowledge file access from the file association (#29937)
`has_access_to_file` now derives knowledge-base access purely from the knowledge-file association, instead of also consulting the `collection_name` value stored on the file record.
2026-09-16 15:59:25 -04:00
Classic298 ff7f35a30d fix: stop logging a traceback twice per failed vector search (#29981)
A vector DB outage renders a full traceback twice for every collection and query pair. query_doc and query_doc_with_hybrid_search each log and then re-raise, and the fan-out handler that catches them logs the same exception again, so twenty knowledge bases and five expanded queries turn one outage into hundreds of identical stack traces per message on every replica.

Both helpers lose the try/except that only logged before re-raising. Each fan-out keeps reporting failures through its return value and now carries the collection name back, so one record after the gather names every collection that failed and attaches a single traceback. The hybrid path logs before its existing raise, so a total failure is still reported before the caller falls back to vector search.

Measured with every collection failing, the vector fan-out drops from 1000 traceback records for fifty collections across ten queries to one, and the hybrid fan-out from 100 for ten collections across five queries to one. Results are byte-identical across eighteen and twenty-six scenarios covering healthy, failing, partial and degenerate inputs.

Three limits worth stating. Nine of the fifteen vector clients swallow the error inside search() and return None, chroma and pgvector among them, so those backends see neither the old tracebacks nor the new record. get_sources_from_items queries one attached item at a time, so a chat with twenty knowledge bases still logs twenty records. And with hybrid search on a total outage logs once from each fan-out as the caller falls back, down from two per pair plus one.

The old "All collection queries failed" warning goes with this. The replacement cannot express that case, because a collection can fail one query and succeed another, so naming every failed collection no longer implies an empty payload.
2026-09-16 15:59:05 -04:00
Classic298 556e48e058 fix: honor false defaults on checkbox prompt variables (#30037)
A prompt template with `{{flag | checkbox:default=false}}` opened the input variables form with the checkbox already ticked, and the same happened for `False`, `"false"`, `0` and `"0"`. Template defaults arrive as strings and the checkbox was bound straight to that value, so any non-empty string counted as checked and there was no way to define a checkbox that starts unchecked once a default was given.

The checkbox now reads its ticked state from the value explicitly: it is checked only for `true` (boolean or string) and stays unchecked for anything else, while clicking it still writes a boolean like before. The seeded value itself is left alone, so what gets substituted into the prompt for an untouched form is unchanged, and typing `true` or `false` into the text field next to the checkbox now moves the tick accordingly.

Fixes #30036
2026-09-16 15:44:29 -04:00
Classic298 48fb2b84ba refac: derive a forked chat's folder from the caller's write access (#30069)
A fork copied the source chat's folder id unchanged. It now keeps that folder only when the caller has write access to it, matching what chat creation and chat moves already do, and is created outside any folder otherwise.
2026-09-16 15:42:03 -04:00
Classic298 befdd86eb1 refac: share one guarded helper for chat branch descent (#30070)
The branch navigation handlers each carried their own copy of the loop that walks a message's childrenIds down to the deepest child, eleven copies in total across the single-response view, the multi-response view and Chat.svelte. They now call one getDeepestChildId helper in the frontend utils, which tracks the ids it has already visited and stops when one repeats, the same way the message list build already does.

The helper also absorbs the "start from the last root message" fallback that two of the call sites repeated inline, so every site is now a single call.
2026-09-16 15:41:56 -04:00
Timothy Jaeryang Baek d9c8de9c39 refac 2026-09-16 10:38:46 -04:00
Timothy Jaeryang Baek fd4fc80536 refac 2026-09-16 10:27:38 -04:00
qwist1233-cpuandqwist1233-cpu a0c8593c92 i18n(tr-TR): translate 1056 missing Turkish strings (#30034)
Co-authored-by: qwist1233-cpu <267938209+qwist1233-cpu@users.noreply.github.com>
2026-09-16 10:26:16 -04:00
tsumon e669f8aefe i18n: improve Simplified Chinese translations (#30043) 2026-09-16 10:19:44 -04:00
Classic298 3348f68778 fix: keep attachment-only messages intact when skills are bound (#30045)
Sending an image or file with no typed text to a model with skills
bound replaced the (empty) message text with the list of skill names,
so the model answered with its skill catalog instead of handling the
attachment. The empty-message guard added for #24929 fired on any
empty last user text without checking for attachments.

The guard now skips the fallback when the current message carries
files, read from the user_message object the frontend sends with the
request. Attachment-only messages then go out exactly as they do on
models without skills. A bare skill selection with no attachment still
gets the fallback, so the provider 400 from #24929 stays fixed.

Request-level files were not usable as the signal: that list carries
the model's knowledge and folder files, so a bare skill selection on a
knowledge model would have gone out empty again.

Fixes #30040
2026-09-16 10:19:28 -04:00
Classic298 82da9093aa fix: default converted Responses API function tools to strict=false (#30046)
Fixes #27750

When a model runs on the Responses API, every Chat Completions style tool (workspace tools, MCP and OpenAPI servers) is converted to the Responses tool shape. Only an explicit strict value was carried over, so tools without one were sent with strict omitted. Chat Completions defaults strict to false, the Responses API defaults it to true, so the conversion silently switched loose schemas into strict mode. The model then filled every optional property (empty strings, zeros, false, empty arrays) and combined mutually exclusive filters, and those arguments went to the tool unchanged. Search and filter tools received meaningful values where omission was intended and rejected the call or returned wrong results.

The converter now defaults strict to false when the source tool does not set it. Explicit true or false values are still preserved and already native Responses tools are passed through untouched, matching what the same tool definition gets on Chat Completions.
2026-09-16 10:19:08 -04:00
Classic298 4611394fa6 refac: correct the ENABLE_CHAT_RESPONSE_STREAM_INPLACE_APPEND comments (#30066)
The comments on the opt-in in-place append made it sound like the fast path
can lose streamed text under normal operation. The only way the field can be
emptied is an allocation failure, which means the host is already out of
memory, and the default path (which copies the whole accumulated string on
every chunk) raises in that situation as well. Reword both comments to state
that condition so the flag is not read as unsafe.
2026-09-16 10:08:46 -04:00
Timothy Jaeryang Baek 3394a10b76 refac 2026-09-16 08:51:02 -04:00
Timothy Jaeryang Baek 88e78b7819 refac 2026-09-16 08:35:01 -04:00
Timothy Jaeryang Baek e9a0164690 refac 2026-09-16 00:34:24 -04:00
Timothy Jaeryang Baek 6d8e63e366 refac 2026-09-16 00:02:48 -04:00
Timothy Jaeryang Baek dfde08aa73 refac 2026-09-15 23:53:41 -04:00
Classic298 3d6598fccb fix: steer models to replace_range in the replace_note_content tool description (#30048)
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.
2026-09-15 22:51:49 -04:00
Classic298 06d9d2e7c7 fix: sync Playwright loader spins a CPU core when a page opens a WebSocket (#30050)
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
2026-09-15 22:51:37 -04:00
Timothy Jaeryang Baek 1cdd7aa459 refac 2026-09-15 22:51:15 -04:00
Timothy Jaeryang Baek 66addbd6b4 refac 2026-09-15 22:47:03 -04:00
Timothy Jaeryang Baek a096961a31 refac 2026-09-14 17:57:09 -04:00
Timothy Jaeryang Baek d25f6c7135 refac 2026-09-14 17:19:00 -04:00
Timothy Jaeryang Baek 9a93b44495 refac 2026-09-14 17:18:34 -04:00
Timothy Jaeryang Baek cc5479d16d refac 2026-09-14 17:18:26 -04:00
Timothy Jaeryang Baek dc98e3023f refac 2026-09-14 17:18:19 -04:00
Timothy Jaeryang Baek 58078ab304 refac 2026-09-14 15:47:50 -04:00
Timothy Jaeryang Baek 924a4a10fb refac 2026-09-14 15:46:31 -04:00
Timothy Jaeryang Baek 113c56fc8c refac 2026-09-14 15:46:23 -04:00
Timothy Jaeryang Baek b6cf23f332 refac 2026-09-13 23:34:35 -04:00
Timothy Jaeryang Baek e69236bccb refac 2026-09-13 23:33:22 -04:00
Classic298 55b7343be8 fix: keep a failed lock release from masking cancellation (#29979)
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.
2026-09-13 20:37:51 -05:00
Timothy Jaeryang BaekandClassic298 d372bec704 refac
Co-Authored-By: Classic298 <27028174+Classic298@users.noreply.github.com>
2026-09-13 21:37:12 -04:00
Classic298 601e0e4345 fix: stop tracking tasks under an empty item id (#29980)
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.
2026-09-13 20:28:54 -05:00
Classic298 d2e62db69b fix: stop the sign-in rate limiter blocking the loop and leaking memory (#29977)
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.
2026-09-13 20:28:41 -05:00
Classic298 263e56e272 fix: keep the session pool reaper alive through a Redis error (#29976)
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.
2026-09-13 20:28:10 -05:00
Classic298 38a8dc9f32 fix: stop retaining every rejected profile image URL in memory (#29971)
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.
2026-09-13 20:22:18 -04:00
Classic298 22d522c558 fix: drop a deleted plugin's source from the content cache (#29983)
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.
2026-09-13 20:21:40 -04:00
Classic298 5af01fe604 refac: make markdown embed rendering opt-in (#29985)
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.
2026-09-13 20:21:00 -04:00
Timothy Jaeryang Baek 6786ae1797 refac 2026-09-13 20:00:29 -04:00
Timothy Jaeryang Baek c0fb36c9b8 refac 2026-09-12 20:23:50 -04:00
Timothy Jaeryang Baek 05484aa055 refac 2026-09-12 20:09:21 -04:00
Timothy Jaeryang Baek fed94c9f5a refac 2026-09-12 19:46:26 -04:00
Timothy Jaeryang Baek 6a85abb3f5 refac 2026-09-12 19:45:42 -04:00
Timothy Jaeryang Baek daf2295932 refac 2026-09-12 19:23:27 -04:00
Timothy Jaeryang Baek 15e2259a2a refac 2026-09-12 19:18:36 -04:00
Timothy Jaeryang Baek 5d66dd6ad2 refac 2026-09-12 19:17:11 -04:00
Classic298 f0a383af0f refac: run the admin check for an explicit backend index inside the listing handlers (#29622)
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.
2026-09-12 18:10:07 -05:00
Classic298 ee25b7d42d fix: cancelled responses no longer keep rendering as still running (#29495)
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
2026-09-12 17:57:58 -05:00
Timothy Jaeryang Baek c1615bec2f refac 2026-09-12 18:57:18 -04:00