Compare commits

...

1758 commits
v0.9.3 ... main

Author SHA1 Message Date
Tim Baek
d3e8bf3405
Merge pull request #29027 from open-webui/dev
Some checks failed
Python CI / Ruff Format (3.11) (push) Has been cancelled
Python CI / Ruff Format (3.12) (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_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/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
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
Frontend Build / Format & Build (push) Has been cancelled
Frontend Build / Unit Tests (push) Has been cancelled
Release to PyPI / release (push) Has been cancelled
Release / publish (push) Has been cancelled
Create and publish Docker images with specific build args / merge (map[name:cuda suffix:-cuda]) (push) Has been cancelled
Create and publish Docker images with specific build args / merge (map[name:cuda126 suffix:-cuda126]) (push) Has been cancelled
Create and publish Docker images with specific build args / merge (map[name:main suffix:]) (push) Has been cancelled
Create and publish Docker images with specific build args / merge (map[name:ollama suffix:-ollama]) (push) Has been cancelled
Create and publish Docker images with specific build args / merge (map[name:slim suffix:-slim]) (push) Has been cancelled
Create and publish Docker images with specific build args / notify-helm-charts (push) Has been cancelled
Create and publish Docker images with specific build args / copy-to-dockerhub (, main) (push) Has been cancelled
Create and publish Docker images with specific build args / copy-to-dockerhub (-cuda, cuda) (push) Has been cancelled
Create and publish Docker images with specific build args / copy-to-dockerhub (-cuda126, cuda126) (push) Has been cancelled
Create and publish Docker images with specific build args / copy-to-dockerhub (-ollama, ollama) (push) Has been cancelled
Create and publish Docker images with specific build args / copy-to-dockerhub (-slim, slim) (push) Has been cancelled
0.11.1
2026-08-25 17:17:36 -04:00
Timothy Jaeryang Baek
db22d89cab refac 2026-08-25 16:57:22 -04:00
Timothy Jaeryang Baek
5c62cc0517 chore: format 2026-08-25 16:53:53 -04:00
Timothy Jaeryang Baek
f71e9570c0 refac 2026-08-25 16:52:31 -04:00
Classic298
0366f5d3d8
chore: Update CHANGELOG.md (#27839)
* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* doc: changelog entries for the terminal preview same-origin setting and the automations bulk toggle
2026-08-25 16:43:59 -04:00
Timothy Jaeryang Baek
54d7a22370 refac 2026-08-25 16:34:49 -04:00
Timothy Jaeryang Baek
f4a0d3c973 refac 2026-08-25 16:33:03 -04:00
Timothy Jaeryang Baek
1d6d4e6e66 refac
Co-Authored-By: Classic298 <27028174+Classic298@users.noreply.github.com>
2026-08-25 16:27:17 -04:00
Timothy Jaeryang Baek
067114c280 refac 2026-08-25 16:04:43 -04:00
Classic298
b1bfc18762
perf: cache the serialized builtin tool spec instead of deep-copying it per request (#28860)
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.
2026-08-25 16:00:34 -04:00
Classic298
45f4a87e85
fix: bound extracted document metadata by the upload size limit by default (#29025)
"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.
2026-08-25 15:50:27 -04:00
Timothy Jaeryang Baek
6dcc2d5269 refac 2026-08-25 15:48:30 -04:00
Timothy Jaeryang Baek
140d2cf4b5 refac 2026-08-25 15:47:55 -04:00
Classic298
d198d950c6
perf: stop re-copying the response text on every stream save (#28821)
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.
2026-08-25 15:41:54 -04:00
Classic298
ac85b0f2a2
refac: gate code interpreter tag detection to legacy tool-calling mode (#29024)
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.
2026-08-25 15:33:55 -04:00
Timothy Jaeryang Baek
28f2965934 refac 2026-08-25 15:26:45 -04:00
Timothy Jaeryang Baek
e3a7a64d82 refac 2026-08-25 15:25:07 -04:00
Timothy Jaeryang Baek
278e97589e refac 2026-08-25 15:22:19 -04:00
Timothy Jaeryang Baek
02400c7a50 refac 2026-08-25 15:15:18 -04:00
Timothy Jaeryang Baek
f158f892f4 refac 2026-08-25 15:14:11 -04:00
Timothy Jaeryang Baek
bc4c91e6d6 refac 2026-08-25 15:11:06 -04:00
Timothy Jaeryang Baek
a610d77137 refac
Co-Authored-By: Fares <26122914+faqeel@users.noreply.github.com>
2026-08-25 15:05:02 -04:00
Timothy Jaeryang Baek
35fbde0a3f refac 2026-08-25 15:00:53 -04:00
Timothy Jaeryang Baek
684111715f refac 2026-08-25 14:56:23 -04:00
Timothy Jaeryang Baek
a21c8d15ee refac 2026-08-25 14:48:38 -04:00
Timothy Jaeryang Baek
20fe43d9da refac 2026-08-25 14:48:01 -04:00
Timothy Jaeryang Baek
3374b21a7d refac 2026-08-25 14:39:25 -04:00
Timothy Jaeryang Baek
8be4c5fa6a refac
Co-Authored-By: Classic298 <27028174+Classic298@users.noreply.github.com>
2026-08-25 14:35:22 -04:00
Timothy Jaeryang Baek
92a1502126 refac 2026-08-25 14:22:20 -04:00
Timothy Jaeryang Baek
5ce198b1d7 refac 2026-08-25 14:20:28 -04:00
Timothy Jaeryang Baek
21366fa1b1 refac 2026-08-25 14:19:58 -04:00
Timothy Jaeryang Baek
20f35d157b refac 2026-08-25 14:14:13 -04:00
Timothy Jaeryang Baek
c4b3e6840f refac
Co-Authored-By: Classic298 <27028174+Classic298@users.noreply.github.com>
2026-08-25 14:07:50 -04:00
Timothy Jaeryang Baek
7198e9d0df refac 2026-08-25 14:06:25 -04:00
Timothy Jaeryang Baek
2e6d61e6d0 refac 2026-08-25 13:51:58 -04:00
Timothy Jaeryang Baek
d241df8d79 refac 2026-08-25 13:39:54 -04:00
Timothy Jaeryang Baek
1c13fedb16 refac 2026-08-25 13:18:38 -04:00
Timothy Jaeryang Baek
a4738e0459 refac 2026-08-25 12:45:12 -04:00
Timothy Jaeryang Baek
3c1017f6c3 refac 2026-08-25 12:22:19 -04:00
Classic298
2d2bcb5332
fix: long streamed lines no longer abort the response (#28114)
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 |
2026-08-25 12:16:37 -04:00
Timothy Jaeryang Baek
beb3c114d3 chore: python-docx dep 2026-08-25 11:59:37 -04:00
Classic298
e3e4bd87df
refac: consolidate the web fetch address checks onto the request path (#27823)
* 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.
2026-08-25 11:15:48 -04:00
Timothy Jaeryang Baek
ca4e07a40b refac 2026-08-25 11:07:54 -04:00
Timothy Jaeryang Baek
6330350a40 refac
Co-Authored-By: Classic298 <27028174+Classic298@users.noreply.github.com>
2026-08-25 10:39:49 -04:00
Timothy Jaeryang Baek
176fa46212 refac
Co-Authored-By: Classic298 <27028174+Classic298@users.noreply.github.com>
2026-08-25 10:03:53 -04:00
Timothy Jaeryang Baek
2f97c9fce3 refac 2026-08-25 10:00:43 -04:00
Timothy Jaeryang Baek
aa3d569610 refac 2026-08-25 09:59:07 -04:00
Timothy Jaeryang Baek
eadce55e34 refac 2026-08-25 09:55:18 -04:00
Timothy Jaeryang Baek
5078d987f8 refac
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 / copy-to-dockerhub (-slim, slim) (push) Blocked by required conditions
Create and publish Docker images with specific build args / copy-to-dockerhub (-ollama, ollama) (push) Blocked by required conditions
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 / 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 / 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
Frontend Build / Format & Build (push) Waiting to run
Frontend Build / Unit Tests (push) Waiting to run
2026-08-24 21:15:54 -04:00
Timothy Jaeryang Baek
2a0274a0a0 refac 2026-08-24 20:34:59 -04:00
Timothy Jaeryang Baek
f3f7659da7 refac 2026-08-24 19:56:07 -04:00
Timothy Jaeryang Baek
170ad0595d refac 2026-08-24 19:48:49 -04:00
Timothy Jaeryang Baek
a9a802d95f refac 2026-08-24 19:48:12 -04:00
Timothy Jaeryang Baek
18bf0ade7b refac 2026-08-24 19:25:03 -04:00
Timothy Jaeryang Baek
11db926a7b refac 2026-08-24 19:20:24 -04:00
Timothy Jaeryang Baek
0169b10979 refac 2026-08-24 19:15:10 -04:00
Classic298
dc03e7e595
refac: keep external connections until their last knowledge base is removed (#28113)
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.
2026-08-24 19:11:40 -04:00
Classic298
6b438f1a79
refac: stream web-fetched documents to disk instead of buffering them (#28945)
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.
2026-08-24 19:06:13 -04:00
Timothy Jaeryang Baek
06e7aac219 refac 2026-08-24 19:05:21 -04:00
G30
2978a03c68
fix: restore the model avatar reset option in the model editor (#29007) 2026-08-24 19:02:22 -04:00
Timothy Jaeryang Baek
d5b66533e7 refac 2026-08-24 18:56:39 -04:00
G30
e6648aefb5
fix: honor the terminal file browser filesystem root instead of clamping to home (#29006) 2026-08-24 17:55:51 -05:00
Sven Horvath
5735123f50
fix: keep the pending note save when an update carries no content snapshot (#28669)
stop_item_tasks() ran unconditionally while create_task() only ran when the
update carried data, so an update without a content snapshot cancelled the
pending save without scheduling a replacement and the edits were never
written.
2026-08-24 18:47:43 -04:00
Classic298
bf08835d6f
perf: allow disabling websocket per-message-deflate (#28613)
With delta streaming most websocket frames are tiny per-token deltas, and
per-message-deflate pays zlib work on every outgoing frame per subscriber for
near-zero gain there; under heavy streaming that shows up as measurable server
CPU. The frames that still benefit are the rare large ones (final message,
sources), and even a 100k token message is only a few hundred KB uncompressed,
which any network delivers without noticeable delay.

UVICORN_WS_PER_MESSAGE_DEFLATE=false (default true, current behavior) disables
the extension in every entry point: open-webui serve and dev, start.sh both
invocations, start_windows.bat and dev.sh. Verified against a running
instance: with the flag off the server declines the client-offered
permessage-deflate extension, with defaults it still negotiates it.
2026-08-24 18:46:07 -04:00
Timothy Jaeryang Baek
97466deea1 refac 2026-08-24 18:38:29 -04:00
Timothy Jaeryang Baek
9dff5e9327 refac 2026-08-24 18:37:39 -04:00
Timothy Jaeryang Baek
536b9edec0 refac 2026-08-24 18:35:04 -04:00
Timothy Jaeryang Baek
b96d2b12da refac 2026-08-24 18:29:36 -04:00
Timothy Jaeryang Baek
e3e82b1471 refac 2026-08-24 18:29:19 -04:00
Timothy Jaeryang Baek
e9efb95a9c refac 2026-08-24 18:17:11 -04:00
Timothy Jaeryang Baek
7a533d0d5b refac 2026-08-24 18:14:30 -04:00
Timothy Jaeryang Baek
6cb2449ab7 refac 2026-08-24 18:10:08 -04:00
Timothy Jaeryang Baek
cf4ac9c8db refac 2026-08-24 18:07:03 -04:00
Timothy Jaeryang Baek
fd8cc2ba4a refac 2026-08-24 18:06:59 -04:00
Timothy Jaeryang Baek
98ee2bdfd3 refac 2026-08-24 17:56:11 -04:00
Classic298
043cf330d2
perf: throttle last_active_at writes by default (#28177)
Presence tracking writes each user's last_active_at on every authenticated request, every API key request and every websocket heartbeat. The throttle for it already exists but ships unset, and unset means no throttle at all, so a stock deployment pays one UPDATE plus COMMIT per user per request. The 30 second frontend heartbeat alone is 2 write transactions per minute per open tab, before any actual UI traffic.

Defaulting the throttle to 60 seconds collapses that to at most one write per user per worker per minute. Presence is only ever read at minute granularity, so nothing visible changes.

60 rather than the 300 to 500 the docs currently suggest, because a user counts as active for 3 minutes after their last write and that window is hardcoded in the backend and again in the frontend. Any interval at or above 180 seconds makes people who are actively using the instance drop out of the active user count. Letting the window follow the interval instead would need the value shipped to the client, so that is a separate change.

0 still disables the throttle, and now costs nothing at all: the decorator returns the undecorated function instead of a wrapper that re-checks a constant on every call.

Closes #28165
2026-08-24 17:53:31 -04:00
G30
da9245626e
fix: keep the viewport in place when older messages load above it (#28657) 2026-08-24 17:51:20 -04:00
G30
683c92d064
fix: fetch sidebar folders once per refresh instead of three times (#28662) 2026-08-24 17:50:46 -04:00
Timothy Jaeryang Baek
a6834f089b refac 2026-08-24 17:47:10 -04:00
Timothy Jaeryang Baek
9e7c9360b7 refac 2026-08-24 17:43:43 -04:00
Timothy Jaeryang Baek
8c1f3d3824 refac 2026-08-24 17:39:30 -04:00
G30
f2313d0c72
fix: keep the workspace tab counts in sync with each section's list (#28983) 2026-08-24 17:36:12 -04:00
Timothy Jaeryang Baek
a914868e3c refac 2026-08-24 17:25:33 -04:00
G30
83d049a465
fix: skip the sidebar refresh when a chat is dropped back where it already is (#28664) 2026-08-24 17:20:44 -04:00
Sebastian
4d5084025f
fix: download nltk data somewhere a non-root UID can read (#28866)
nltk.download picks the first entry of nltk.data.path that already exists and is
writable. None do here, so punkt_tab lands in /root/nltk_data, and /root is mode
0700. The corpus is then unreachable whenever the container does not run as
root:

    nltk.data.find('tokenizers/punkt_tab')
    LookupError: Resource punkt_tab not found.

/usr/local/share/nltk_data is already on nltk.data.path, so nothing changes at
the read side.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-24 17:19:15 -04:00
Timothy Jaeryang Baek
ecad20b77f refac 2026-08-24 17:17:53 -04:00
Timothy Jaeryang Baek
91917b2395 refac 2026-08-24 17:16:01 -04:00
Timothy Jaeryang Baek
363ad352fe refac 2026-08-24 17:12:56 -04:00
Classic298
23b3a69bc2
fix: keep folder parent references acyclic (#28748)
Moving a folder under one of its own subfolders was accepted. A folder in a parent loop is never a root, so it and everything under it silently disappeared from the sidebar, and there was no way to get it back from the UI.

The move is now rejected with a 400, folders whose parent chain loops are put back at the root on the next folder list, and the folder tree traversals skip ids they have already visited so existing data in that state stays workable.
2026-08-24 17:06:19 -04:00
G30
18edfff2d6
fix: discard unsaved group settings and key the groups list (#28076)
* fix: stop unsaved group settings from persisting into the group list state

* fix: key the groups list so an open modal cannot rebind to another group
2026-08-24 16:31:39 -04:00
Timothy Jaeryang Baek
aeda6ff13a refac 2026-08-24 16:29:57 -04:00
G30
336d8841f4
fix: make embedded message content inert in the sidebar chat hover preview (#27770) 2026-08-24 07:36:44 -04:00
G30
fd7024f198
fix: log tool server connectivity failures without a traceback (#27757) 2026-08-24 07:35:57 -04:00
G30
6b4131d1d7
fix: log terminal proxy connectivity failures as single lines and handle client disconnects (#27755) 2026-08-24 07:35:38 -04:00
Classic298
baeb2dfb83
fix: apply RDS IAM token auth to the pgvector engine (#27754)
With `DATABASE_ENABLE_IAM_TOKEN_AUTH=true` and `VECTOR_DB=pgvector`, startup failed at vector store initialisation with `fe_sendauth: no password supplied`, so the two features could not be used together.

`PgvectorClient` builds its own engine and never got the `do_connect` listener that refreshes the RDS IAM token, and the `ScopedSession` branch that would have reused the instrumented main engine is unreachable because `PGVECTOR_DB_URL` defaults to `DATABASE_URL` and is therefore never falsy.

The pgvector engine now goes through `enable_iam_token_auth()` like the main and Alembic engines. Since a token authenticates exactly one host/port/user, that function now attaches the listener only to engines pointing at the same target, so a `PGVECTOR_DB_URL` aimed at a separate database keeps the password from its own URL instead of having it overwritten; the skip is logged with both identities.

Fixes #27752
2026-08-24 07:35:19 -04:00
G30
16b20c651d
fix: restore per-item retrieval settings in the model editor knowledge section (#27686) 2026-08-24 07:34:30 -04:00
G30
01452ff62f
fix: mount the code editor in its own container so duplicated message renders don't collide (#27740) 2026-08-24 07:34:07 -04:00
Classic298
9cf1a07960
fix: use the pooled client timeout for the Anthropic Messages passthrough (#27675)
* fix: use the pooled client timeout for the Anthropic Messages passthrough

The native `/api/v1/messages` passthrough still referenced `openai.AIOHTTP_CLIENT_TIMEOUT`, which stopped existing when `routers/openai.py` moved onto `session_pool.get_client_timeout()`. Every passthrough request therefore raised `AttributeError: module 'open_webui.routers.openai' has no attribute 'AIOHTTP_CLIENT_TIMEOUT'` before it was sent, and the surrounding handler turned that into a 502 "Open WebUI: Server Connection Error", so Anthropic-format clients such as Cline could not reach any model at all.

Use `get_client_timeout(stream=...)` like the OpenAI and Ollama proxies do, so the configured `AIOHTTP_CLIENT_TIMEOUT` applies and streaming requests additionally get the idle-read timeout.

Fixes #27595

* fix: authenticate native Anthropic requests with x-api-key

The Anthropic Messages passthrough and the token-count forwarding both build their upstream request through `get_anthropic_request_target`, which sends the connection key as `Authorization: Bearer <key>`. Anthropic's OpenAI-compatible `/chat/completions` endpoint accepts that, which is why the model works in the chat UI, but the native `/v1/messages` and `/v1/messages/count_tokens` endpoints do not: they require the key in `x-api-key` and reject a bearer token with 401 `Invalid bearer token` (and `jwt auth is not yet supported on count_tokens`). They also require an `anthropic-version` header, which was never sent.

For `api.anthropic.com` connections, send `anthropic-version` and move the key into `x-api-key`, dropping the bearer header. Connections using session, OAuth or Entra ID auth keep their token untouched, LiteLLM passthrough connections are unaffected, and admin-configured custom headers still win over both defaults.

Fixes #27695
2026-08-24 07:33:33 -04:00
Timothy Jaeryang Baek
8a170897ba refac 2026-08-24 07:28:09 -04:00
Timothy Jaeryang Baek
495296346e refac 2026-08-24 06:26:49 -04:00
Timothy Jaeryang Baek
ef455fcef9 refac 2026-08-24 06:25:09 -04:00
Classic298
091c44c621
perf: stop rescanning the whole response for tag boundaries on every streamed chunk (#28861)
Streamed responses are scanned for reasoning and code interpreter tags. To work out where the last complete tag ended, the scanner searched backwards from the start of the accumulated text on every chunk, once per tag set. Ordinary prose contains no angle bracket, so that search never stopped early and read the entire response back every time. The cost grows with the square of the response length, and this scanning is on unless a model turns it off.

The two positions are now carried forward as the text grows, so each chunk only scans the characters it added.

Measured on CPython 3.12, a 270 KB response streamed in 27000 chunks:

| response text | before | after |
|---|---|---|
| no newlines | 7690 ms | 40.6 ms |
| with newlines | 5695 ms | 41.7 ms |

The carried positions match a full rescan at every step of 36282 randomized replays, covering text with no markers, newlines only, dense markers, real tags and truncation part way through.
2026-08-24 05:11:32 -05:00
Timothy Jaeryang Baek
978d257214 refac 2026-08-24 05:40:53 -04:00
Classic298
16c2a9eda4
fix: index the chat queries that make large SQLite instances unusable (#27663)
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: 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
Create and publish Docker images with specific build args / copy-to-dockerhub (-ollama, ollama) (push) Blocked by required conditions
Create and publish Docker images with specific build args / copy-to-dockerhub (-slim, slim) (push) Blocked by required conditions
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
Frontend Build / Format & Build (push) Waiting to run
Frontend Build / Unit Tests (push) Waiting to run
The timer scheduler polls once a second and cancels on every message send and chat open, the sidebar lists chats ordered by `updated_at`, and the folder badges count unread chats per folder. None of those could be served by an index, so each call read most of the `chat` table, and because `meta` sits after the chat payload column SQLite had to walk every row's overflow pages to get there. On a large history that stalls the sidebar, every chat switch and every send, and the idle poll alone burns about a quarter of a CPU core.

Timers now keep their due time in a dedicated `chat.timer_at` column behind a partial index, and the chat list, unread and unfinished-reply queries each get an index matching their filter and ordering. Existing pending timers are backfilled from their meta by the migration. Dropping the `internal` and `type` checks also makes a forked timer chat inert, where a fork used to copy `meta` verbatim and become a second claim target that could fire a duplicate timer.

Measured on SQLite, same rows returned:

| query | before | after |
|---|---|---|
| idle timer poll (2000 chats, 0.43 GB) | 170 ms | 0.04 ms |
| cancel on send and chat open (4000 chats, 377 MB) | 200 ms | 0.04 ms |
| sidebar chat list (15000 chats, 1.26 GB) | 157 ms | 1.8 ms |
| folder unread badges (15000 chats, 1.4 GB) | 54 ms | 0.2 ms |

PostgreSQL 17 serves all of them as index-only scans with no sort node. Exercised through fresh install, upgrade with seeded data, downgrade and re-upgrade on SQLite and PostgreSQL 17.

Fixes #27622
2026-08-23 16:11:54 -05:00
enixCode
29e8d7db67
i18n: fill missing fr-FR translations (#28951)
75 entries in the French locale had an empty value. i18next falls back to the
key when a value is empty, so French users were shown raw English strings
across permission settings, empty states, form placeholders and error toasts.

- fill those 75 entries; no key is added, removed or reordered
- follow the conventions already present in the file: "Entrez ..." for input
  hints, "Échec de ..." for failures, Chat -> Conversation, Token left as is
- interpolation placeholders preserved; no existing translation modified
2026-08-23 16:02:27 -04:00
Classic298
f73f09a3e0
refac: drop the redundant .keys() from two dict membership tests (#28859)
`x in d` and `x in d.keys()` are identical for a plain dict, so the `.keys()` call builds a throwaway view and reads as if it were doing something. Both sites operate on a plain dict: `combined` in `merge_and_sort_query_results` is a local `dict()`, and `ui_settings` comes from `UserSettings.model_dump()` where `ui` is annotated `dict | None` and is already guarded against None on the preceding line.

No behaviour change, and no measurable speedup either, so this is a readability cleanup rather than a performance one.

Sites where `.keys()` is load-bearing are left alone: the `list(d.keys())` snapshots taken before mutating during iteration, and the places where `.keys()` is the iteration or comprehension source rather than a membership test.
2026-08-23 16:02:09 -04:00
Timothy Jaeryang Baek
b30b11d4c9 refac 2026-08-23 16:01:36 -04:00
Classic298
ac091273b7
fix: keep streamed text when a filter or provider sends non-string content (#28840)
A stream filter function, or a provider that puts something other than a string in a delta, makes the streaming handler concatenate a string with a non-string. That raises TypeError, and the broad handler wrapped around the whole per-chunk block swallows it at debug level and moves on. The chunk's text never reaches the message the user sees, and nothing above debug level says why.

The content and reasoning fields are now coerced to text once, where they are read off the delta, ahead of every consumer. The coercion is guarded on truthiness, so falsy values such as an empty list still skip the block exactly as before, and the accumulated content receives byte for byte what it received previously.

Checked against 14 delta shapes covering strings, empty values, numbers, booleans, None, lists, dicts and a content array: the truthiness gate and the accumulated content are identical before and after.
2026-08-23 15:35:53 -04:00
G30
603e85c569
fix: apply connection prefix id to the model display name as well as the id (#28950) 2026-08-23 15:27:09 -04:00
Timothy Jaeryang Baek
f64c0c87e8 refac 2026-08-23 15:14:43 -04:00
Timothy Jaeryang Baek
e623c02acc refac 2026-08-23 15:06:30 -04:00
Timothy Jaeryang Baek
78f48a21ee refac 2026-08-23 14:40:48 -04:00
Timothy Jaeryang Baek
fb4f476316 refac 2026-08-23 13:49:50 -04:00
Timothy Jaeryang Baek
2578174637 refac 2026-08-23 13:47:02 -04:00
Solaris-star
5ee7140b4c
fix: render code editor drawer above settings modal (#27648)
The ComfyUI workflow.json Edit drawer (CodeEditorModal -> Drawer) used
z-999, while the admin Settings dialog (Modal) uses z-9999. Since both
are appended to <body>, the drawer rendered behind the settings dialog
and appeared to open 'in the background' (#27647).

Add an optional zIndexClass prop to Drawer (default z-999, preserving
existing behaviour for all other callers) and pass z-99999 from
CodeEditorModal so the editor surfaces above any enclosing modal.
2026-08-23 13:44:41 -04:00
Timothy Jaeryang Baek
bf3a58dbcd refac 2026-08-23 13:40:13 -04:00
Timothy Jaeryang Baek
5093a99389 refac 2026-08-23 13:36:53 -04:00
Timothy Jaeryang Baek
886248de36 refac 2026-08-23 13:33:51 -04:00
Classic298
d16d62d1f1
fix: duplicate checkbox markers when serializing note task lists (#27671)
Task lists in Notes serialized to markdown as `- [ ] [ ]` with the item text pushed onto a separate line after a blank line, so previewing or downloading a note produced a broken checklist, and checking an item left the second `[ ]` behind as plain text.

TipTap renders each task item as a checkbox inside a label plus a block-wrapped body. The GFM turndown plugin matches that checkbox and emits its own `[ ]`, which landed next to the marker the task item rule already writes, and the block wrapper left blank lines around the text that the old leading-whitespace strip could not remove.

Register a rule that drops the checkbox so the task item rule is the only source of the marker, and trim the block wrapper while indenting continuation lines so nested lists and code fences stay inside the item.

Fixes #26067
2026-08-23 13:31:46 -04:00
Classic298
945c521ed2
refac: make the web search error message a plain constant (#28948)
The web search error message was a lambda with a passthrough branch that returned whatever it was handed. Since #28942 both call sites pass no arguments, so that branch is unreachable, and it is the trap that let a caller drop a raw exception object into an HTTP response body and turn an intended 400 into an unserialisable 500.

A plain string constant removes the trap and lines the message up with every other fixed message in that file. Behaviour is unchanged: the response detail comes out byte for byte identical, because the enum already overrides __str__ to render members as their value. Verified on Python 3.11 and 3.12, both producing the same string and the same JSON body.
2026-08-23 13:27:02 -04:00
Timothy Jaeryang Baek
842c1f9d67 refac 2026-08-23 13:10:07 -04:00
Timothy Jaeryang Baek
0fb542b376 refac 2026-08-23 12:59:59 -04:00
Classic298
fca3be5416
fix: web search failures return HTTP 500 with an empty body instead of 400 (#28942)
Any failure during a web search comes back to the client as a bare HTTP 500 with nothing in it. The handler tries to build a 400 whose detail is the caught exception object itself, FastAPI cannot serialise that into a response body, so rendering the error response fails and the request falls through to the generic 500 handler. In chat this surfaces as a web search that fails with no explanation at all, and the most common trigger is simply selecting a search engine without configuring its API key.

This routes the failure through the standard error formatter, which is what the sibling handler for content loading failures in the same function already does. Web search failures now return 400 with a readable message, and the exception itself keeps going to the server log exactly as before.

Passing str(e) into the response was the other option and was rejected: the rest of the backend deliberately keeps provider exception text out of client responses and in the log, and provider exceptions here can carry request details that should not be echoed back.
2026-08-23 12:59:25 -04:00
Classic298
069f49fcd2
refac: remove unreachable rate limit handler from DuckDuckGo web search (#28943)
The DuckDuckGo search path catches RatelimitException from the ddgs library. That exception is defined by the library but never raised anywhere in it, checked against the pinned 9.14.4 and against 9.11.3, so the handler could never run. The two fallbacks around it were dead for the same reason: ddgs.text() returns a non-empty list or raises, so None and an empty list are not outcomes it can produce.

Removing all three leaves one call and changes nothing observable. A refused or rate limited search already came out as a failed search, with the error shown to the user and the traceback in the log, and it still does.

The backend argument is now passed as backend or 'auto' rather than conditionally omitted, because 'auto' is the library's own default for that parameter, so every configured value including unset and empty resolves exactly as before. Verified by running the old and the new function side by side against a stubbed library covering normal results, the domain filter, all four backend settings and a failing search, with identical results in every case.
2026-08-23 12:59:05 -04:00
Timothy Jaeryang Baek
3c66d639e3 refac 2026-08-23 12:33:34 -04:00
Timothy Jaeryang Baek
c1c81f8127 refac 2026-08-23 03:40:40 -04:00
Timothy Jaeryang Baek
f3f76095d1 refac 2026-08-23 02:34:08 -04:00
Timothy Jaeryang Baek
4807866a1c refac
Co-Authored-By: Classic298 <27028174+Classic298@users.noreply.github.com>
2026-08-23 01:31:30 -04:00
Timothy Jaeryang Baek
7abe11346a refac
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: 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
Frontend Build / Format & Build (push) Waiting to run
Frontend Build / Unit Tests (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 / 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 (-ollama, ollama) (push) Blocked by required conditions
Create and publish Docker images with specific build args / copy-to-dockerhub (-slim, slim) (push) Blocked by required conditions
2026-08-22 09:13:13 -04:00
Timothy Jaeryang Baek
d17f06a235 refac 2026-08-22 08:43:46 -04:00
Timothy Jaeryang Baek
1b3b9375bb refac 2026-08-22 08:21:53 -04:00
Timothy Jaeryang Baek
d7d935275a refac 2026-08-22 08:18:38 -04:00
G30
302ffc8b7e
fix: skip the autocompletion debounce when the document shrank past the captured position (#28824) 2026-08-22 08:12:40 -04:00
G30
d1c207d091
fix: give docx and pptx previews in the file modal a definite frame height (#28878) 2026-08-22 08:11:29 -04:00
Timothy Jaeryang Baek
ccbb3303f2 refac 2026-08-22 08:05:52 -04:00
G30
883c7434fb
fix: tolerate reasoning items without started_at when closing them at stream end (#28872)
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: 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
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 (-ollama, ollama) (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 / Format & Build (push) Waiting to run
Frontend Build / Unit Tests (push) Waiting to run
2026-08-21 15:51:21 -07:00
Timothy Jaeryang Baek
dcff244f9e refac 2026-08-21 15:50:50 -07:00
Aleix Dorca
6a999f357b
i18n: Update catalan translation.json (#28826) 2026-08-21 12:08:53 -07:00
G30
d2bc98eaeb
fix: let the composer's model selector shrink so narrow containers keep every control visible (#28912) 2026-08-21 12:04:59 -07:00
Classic298
7229fac0c4
refactor: remove the orphaned admin settings components (#28855)
Some checks are pending
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 / merge (map[name:cuda suffix:-cuda]) (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:cuda126 suffix:-cuda126]) (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 (-ollama, ollama) (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 / Format & Build (push) Waiting to run
Frontend Build / Unit Tests (push) Waiting to run
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: 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
Two components under the admin settings area are not imported by any route or component. The admin settings shell went dead when the admin settings route became a redirect into the settings modal, which imports every admin tab directly and carries its own tab list and search. The model selector beside it lost its last importer in a separate models refactor. Every child component the shell used is still imported by the settings modal, so nothing goes with them.

This removes around 590 lines that still turn up in every search across the admin area.
2026-08-20 13:37:53 -07:00
Classic298
c7f306031d
refactor: remove the unreachable async half of the Mistral loader and an unused Datalab helper (#28839)
The Mistral OCR loader has a full async pipeline beside its synchronous one: an async load, its own upload, signed URL, OCR, delete and retry helpers, a pooled session and a batch loader on top. The only way in was the batch loader, which nothing calls, so the entire async half was unreachable. Everything that loads documents goes through the synchronous path, and the shared loader entry point runs it in a worker thread. The Datalab loader carries a public request status poller with no caller either, since its own load inlines the polling it needs.

With the async half gone, the retry classifier's two aiohttp branches can no longer be reached, since the only retried calls are synchronous, so those go with it along with the aiohttp import that existed solely to feed them, and a timeout attribute that nothing reads any more. The class docstring loses the three bullets that only described the removed pipeline, and four docstrings stop calling themselves the sync version of something that no longer has an async counterpart.

This removes around 350 lines and leaves one code path per loader instead of one live path and one that cannot be entered.
2026-08-20 13:14:14 -07:00
Timothy Jaeryang Baek
7d4747dfd7 refac 2026-08-20 13:13:51 -07:00
Classic298
18c604baa9
fix: voice mode produces no audio when the task model returns empty content (#28724)
With "show emoji in call" enabled, voice mode stayed completely silent and no request ever reached the configured TTS server. Reasoning models served with a reasoning parser return `message.content` as null and put the text in `reasoning_content`, and the emoji helper called `.replace()` on that null value and threw.

The call overlay ran the emoji request first, inside the same `try` block as speech synthesis, so that error skipped the entire TTS section. The audio cache was never filled, and the playback loop kept re-queueing the same content every 200 ms without ever playing it. Read aloud was unaffected because it synthesizes speech directly, which is why the failure looked specific to voice mode.

Fixed on both sides: the optional chain in `generateEmoji` now covers `content`, and the emoji request in the call overlay gets its own catch, matching the speech synthesis call directly below it. An emoji failure now costs the emoji instead of the whole reply.
2026-08-20 13:03:55 -07:00
Classic298
5586964bb2
fix: keep the usage pool cleanup task alive across lock loss and Redis errors (#28834)
With WEBSOCKET_MANAGER=redis on a multi-node deployment, the usage pool cleanup task could stop permanently for the whole cluster. Nodes that lost the startup lock race gave up for good after three attempts, and the winner died on a single failed renew or on any Redis connection error, releasing the lock with nobody left to take it over. From then on expired entries accumulated in the usage pool until a node restarted, so /api/usage over-reported models in use and every disconnect handler walked an ever-growing pool.

The task now retries lock acquisition forever like the session pool cleanup does, and any error is logged and answered by releasing the lock and returning to acquisition, so a transient failure costs one cleanup cycle and every node stays a takeover candidate. The delete of an emptied model entry is KeyError-guarded because a disconnect handler on another node can remove the same key between the sweep's snapshot and its delete; unguarded, that race was a permanent task killer that needed nothing rarer than a chat finishing while its tab closed.
2026-08-20 12:59:38 -07:00
Classic298
b0fdc00452
perf: write task payloads to Redis as bytes (#28833)
Saving a streaming response serialized the payload with orjson, decoded it to
str, scanned it for the three Unicode line separators and let redis-py encode
it straight back to UTF-8: on an 8 MB non-ASCII chat that is 6.9 ms and ~22 MB
of transient buffers per write, synchronously on the event loop.

json_codec now exposes dumps_bytes, which returns the serialized payload as
UTF-8 bytes without the line-separator escaping, and the two Redis writes in
tasks.py use it. That escaping only protects line-framed protocols such as
SSE; every reader of these Redis values re-parses them before anything is
served, and the escaped and raw forms parse identically, so mixed versions
during a rolling deploy interoperate both ways. The same write drops to
0.9 ms and one 8 MB buffer (7.5x), with 31-66% saved on KB-sized writes.
With ENABLE_ORJSON off, dumps_bytes wraps stdlib json, behaviour unchanged.

The str path keeps the escaping but applies it with chained str.replace
instead of a translate table, cutting a separator-containing 8 MB payload
from 312 ms to 5.7 ms with byte-identical output.
2026-08-20 12:58:52 -07:00
G30
a0e7d0e3a4
fix: send the model id when ejecting from the model selector (#28766) 2026-08-20 12:58:14 -07:00
G30
528259695c
fix: key selected item lists in model editor selectors to stop checkbox state reuse (#28837) 2026-08-20 12:57:39 -07:00
Timothy Jaeryang Baek
f822605b35 refac 2026-08-20 12:56:45 -07:00
Timothy Jaeryang Baek
8a42aa53e8 refac 2026-08-19 22:48:32 -07:00
Timothy Jaeryang Baek
46dce79eb0 refac 2026-08-19 22:28:41 -07:00
Classic298
4df2d9a7aa
perf: filter workspace models by access in SQL instead of loading every model (#28795)
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 / copy-to-dockerhub (-ollama, ollama) (push) Blocked by required conditions
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 / Format & Build (push) Waiting to run
Frontend Build / Unit Tests (push) Waiting to run
Exporting workspace models loaded every model row, built a full response object with its owner for each, and only then dropped the ones the caller may not see. On a large model table that made the export endpoint slow in proportion to models the user cannot even access.

The owner-or-grant check now happens in the query itself, reusing the permission filter this file already applies to the paginated list endpoint, so only visible rows are ever hydrated. The by-user wrapper had one caller left and is gone with it.

Measured with 500 workspace models of which 3 are visible to the caller: 5 queries and ~12.7 ms before, 4 queries and ~2.8 ms after. The resulting set is unchanged for owner, public, direct-user, group and multi-grant entries, and base model entries stay excluded as before.
2026-08-19 18:15:01 -07:00
Classic298
ce22e0bb15
perf: stop rescanning the whole chat JSON on every streamed event write (#28820)
Every streamed event that persists to a chat (status updates, citations,
file attachments, message content) serialized the entire conversation JSON
three times: a null-byte check of the stored row, a second sanitize of the
whole blob after merging in the event payload and the flush of the UPDATE
itself. The middle pass rescans megabytes of already-clean history for null
bytes that can only come from the small incoming payload, so long chats pay
for their full history on every single event.

The write paths now sanitize just the incoming message, message id and
status dict and keep the row-level sanitize, so legacy rows with null bytes
still self-heal as before. Median per-event write time (sqlite, orjson):
1 MB chat 15.0 ms to 11.1 ms, 4 MB 65.2 ms to 52.4 ms, 10 MB 159.9 ms to
124.5 ms, roughly 20 percent less per event. As a side effect the
chat_message dual write now receives the sanitized message; previously null
bytes in non-content fields were cleaned in the blob but written raw to
chat_message, which failed that insert on PostgreSQL. Verified byte-identical
rows against the previous implementation across nine scenarios covering null
bytes in every input, legacy dirty rows, a missing title and a NULL chat
column.
2026-08-19 17:52:05 -07:00
Timothy Jaeryang Baek
33dff414e8 refac 2026-08-19 16:35:49 -07:00
G30
dd7158b45f
fix: clear the integrations search when leaving a section (#28812) 2026-08-19 16:14:53 -07:00
Timothy Jaeryang Baek
ea55d38793 refac 2026-08-19 16:14:27 -07:00
Classic298
7cf6051a74
perf: resolve group membership once per folder listing instead of once per entry (#28810)
Listing a user's folders re-checks which entries they may still see, and it resolved their group membership again for every folder, then again inside the collection and note branches for every entry. A comment in that helper claims one membership fetch for the whole listing, but the caller invokes it once per folder, so the claim never held.

The listing now resolves membership once, and only when some folder actually carries entries, then threads it through the file, collection and note checks. Callers that do not supply it are unchanged and still resolve for themselves.

Measured with twenty folders holding six files, two knowledge bases and two notes each: 245 queries and ~145 ms before, 186 and ~117 ms after. The folders returned, and the entries the integrity pass writes back, are unchanged. That was checked against entries the caller owns, entries shared through a group, entries shared with nobody, another user's files, and an unrecognised entry type.
2026-08-19 11:16:29 -07:00
Classic298
767a1157f1
refac: bind tool server cookies per connection (#28707)
The tool callable now takes its connection's cookie jar as a parameter, matching how its headers are already passed and how the terminal tool factory in the same module builds its callables.
2026-08-19 11:16:16 -07:00
Classic298
abc69000b3
fix: treat a non-numeric calendar alert_minutes as unset (#28790)
A calendar event's `meta` is a free-form dict, so `meta.alert_minutes` can hold any JSON type, while the upcoming-events lookup assumed it was a number and compared it directly. It now ignores a value that is not numeric and falls back to the default alert window for that event.

Handled on the read side rather than on the write path so events already stored with a non-numeric value are covered too. Numeric values are untouched, including the negative "no alert" sentinel.
2026-08-19 11:16:00 -07:00
Classic298
2e7df54673
fix: surface attached chat references in <attached_files> (#28788)
A chat attached via the "+" menu or dropped from the sidebar references an
existing chat by id and carries no url. add_file_context() filtered on
`file.get('url')`, so the reference was dropped from <attached_files>
entirely and the model was never told it existed.

When the RAG file-context path is enabled the chat content still reaches
the model as <source> context, which masked this. With file_context
disabled that path is skipped, and get_attached_knowledge() only promotes
collection/note items into <attached_knowledge> - so an attached chat was
visible in the UI but invisible to the model, which then reported having
no chat attachments despite having a view_chat tool available.

Keep chat references and emit their id so the model can resolve them with
view_chat. The url attribute is now conditional, since a chat has none;
the id guard it replaces was dead once the filter guarantees a url or a
chat id.

Co-authored-by: Claude <noreply@anthropic.com>
2026-08-19 11:15:22 -07:00
Classic298
ebd4d9c6cc
fix: honor bypass_system_prompt on the pipe route (#28739)
The tool-call continuation re-submits with bypass_system_prompt=True, but only
routers/openai.py and routers/ollama.py checked it, so pipe and manifold models
had the system prompt applied again on every continuation. Since
add_or_update_system_message() prepends rather than replaces, N tool-call rounds
left N+1 copies of the system prompt in the payload.
2026-08-19 11:08:02 -07:00
Classic298
4f98a5184f
perf: resolve model-attached file access with a targeted query (#28802)
Checking whether a user may reach a file loaded and validated every workspace model that user can access, then scanned each model's knowledge list in Python for one file id. Folder listings run that check once per file, so opening a folder of twenty files rebuilt the whole accessible-model set twenty times, and the same check sits on every retrieval and download path.

The lookup now runs the other way round: the database returns the models that attach the file, and only those are access-checked. The text match on the metadata column is a prefilter and the knowledge entries still decide, so a file id that merely appears in a description grants nothing; file ids are server-generated uuids, so the match can only be too wide, never too narrow.

Measured with 500 accessible workspace models: a single check drops from 9 queries and ~20 ms to 6 and ~2.6 ms, and a twenty-file folder listing from 180 queries and ~680 ms to 120 and ~56 ms. A 72-case matrix over owner, public, direct-user and group grants, for both read and write, returns exactly what it returned before, and write still requires the model owner to own the file. The check also no longer writes to the database while answering a read-only question.
2026-08-19 11:07:43 -07:00
Classic298
dbf715cb63
perf: stop scanning every skill on each listing and chat turn (#28798)
Listing skills ran one database query per skill in the instance. A non-admin opening the list on a workspace with 500 skills issued over 500 queries, the paginated list re-resolved the caller's group membership once per row, and every chat message carrying a skill loaded every skill the user can read, full body and owner included, to use the two or three it actually referenced.

Skills now arrive already filtered: the owner-or-grant check runs in the query as an EXISTS subquery, the same way prompts and the search endpoints already do it, the per-item write flag uses the existing batch grant lookup, and the chat path asks only for the skill ids the request names.

Measured with 500 skills of which 3 are visible to the caller: 504 queries and ~300 ms before, 4 queries and ~2.6 ms after. The resulting set is unchanged for owner, public, direct-user, group and multi-grant entries, for both read and write.
2026-08-19 11:07:33 -07:00
Classic298
9bdb072690
refac: remove unused knowledge base accessors (#28794)
Three methods on KnowledgeTable have no callers anywhere in the repository. get_knowledge_bases_by_user_id loaded every knowledge base and filtered them in Python, which search_knowledge_bases already does in SQL with pagination. get_knowledge_by_id_and_user_id duplicates check_access_by_user_id with the permission hardcoded to write. update_knowledge_data_by_id writes a data column that a migration dropped, so it could only ever raise and return None through its own except block.

What remains is one per-entry access helper and one SQL-filtered list path, so nobody reaches for the slower or the broken variant by accident.

No behaviour change.
2026-08-19 11:07:21 -07:00
Classic298
284da2ae49
perf: reuse the already loaded chat when assembling builtin tools (#28809)
Assembling the builtin tools for a chat message fetched the chat row a second time to answer one question: whether this is a note chat. The caller had loaded that same row a few lines earlier, from the same id in the same metadata dict, and had already evaluated the same predicate for its own note handling. So every message with builtin tools enabled read the whole conversation blob twice.

The caller now works the flag out once and passes it down. Tool assembly no longer touches a chat model at all, so the two files cannot drift apart when the shape of that metadata changes.

Measured with a stub request across five chat shapes, a note chat, a plain chat, an internal chat that is not a note, a chat id with no row behind it, and an unsaved chat id: the returned tool set is identical in every case and the query count drops from six to five. The note tools are still enabled for a note chat with the notes feature switched off, which is the only thing that predicate decides.
2026-08-19 11:06:49 -07:00
Classic298
6db64c4855
perf: batch the shared folder listing instead of fetching one folder at a time (#28804)
Opening the shared folder list fetched every shared folder in its own query, fetched a chunk of them a second time to walk their children, and looked up each distinct owner separately. With forty folders shared with a user that is over a hundred queries before any subtree work starts.

The folders and their owners now come back in one query each, and the inheritance pass reuses the rows already in hand. Both folder listings also gained an explicit order: the sidebar merges shared subfolders in response order without sorting them, and neither query had an ORDER BY, so on Postgres a folder rename could reshuffle its siblings.

Measured with forty shared folders and no subtrees: 181 queries and ~105 ms before, 92 and ~66 ms after. With subtrees attached, 203 folders in total, it is 341 queries before against 252 after; the remainder is the recursive child walk, which this change deliberately leaves alone. The returned set, permissions and owner names are unchanged, including for a grant pointing at a deleted folder row, a folder the caller owns that is also shared with them, a folder whose owner record is gone, and a child folder that is itself directly shared.
2026-08-19 11:06:37 -07:00
Classic298
81fe43f210
perf: write a chat's messages in one transaction instead of one per message (#28806)
Saving a chat rewrote its message rows one at a time. Each message took its own session out of the pool and committed on its own, and the save endpoint hands over the entire merged history rather than only what changed, so a two hundred message chat cost two hundred sessions and two hundred commits on every save.

The messages now go through a single select and a single commit. The field mapping for the insert and the update branch moved into two small helpers, so the batch and the single-message path cannot drift apart.

Measured on a two hundred message chat with one message edited: 201 queries and 200 transactions before, 2 queries and 1 transaction after, ~149 ms against ~6 ms. Re-saving an unchanged history now costs one select and no writes at all.

One behaviour change worth stating: a message the database cannot store used to be skipped on its own, and now costs the rest of that same save. This table is a rebuildable fast path, so the reader falls back to the history on the chat row and re-triggers the backfill, and the next save reconciles everything still present. A per-message retry was tried and dropped, because a commit that lands but still raises would re-apply the usage merge and double the recorded token counts.
2026-08-19 12:46:49 -05:00
G30
bcb50fe7b0
fix: derive integrations toggle state from the selected ids (#28807) 2026-08-19 12:46:17 -05:00
Timothy Jaeryang Baek
5ea9ff3ed9 refac 2026-08-18 22:09:42 -07:00
Timothy Jaeryang Baek
3fdfbd5138 refac
Some checks are pending
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 / 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 / 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 (-ollama, ollama) (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 / Format & Build (push) Waiting to run
Frontend Build / Unit Tests (push) Waiting to run
2026-08-18 19:37:20 -07:00
Timothy Jaeryang Baek
b838860dc0 refac 2026-08-18 19:30:42 -07:00
Classic298
21e390561d
fix: revoke existing sessions when a password changes (#28725)
Some checks failed
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 / 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 / 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 (-ollama, ollama) (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 / Format & Build (push) Waiting to run
Frontend Build / Unit Tests (push) Waiting to run
Python CI / Ruff Format (3.11) (push) Has been cancelled
Python CI / Ruff Format (3.12) (push) Has been cancelled
Changing a password left every other logged-in device working until the JWT expired on its own, up to four weeks with the default settings. The hardening docs already promise the opposite: with Redis configured a password change is supposed to put the user's tokens on the revocation list, but only sign-out and OIDC back-channel logout ever wrote to it.

Both password-change paths, self-service and an admin resetting someone's password, now stamp the per-user revocation marker that token validation already checks, so every session issued before the change stops working. The acting device is signed out as well and asked to sign in again, which is the safer default when the password is being changed precisely because the old one may be compromised. Without Redis nothing can be revoked, as before, and the backend now logs a warning saying so.

The marker is written through one shared helper, so its lifetime follows the configured JWT lifetime instead of a fixed 30 days and never expires at all when JWT_EXPIRES_IN disables expiry. Back-channel logout picks that up too, where a long or disabled JWT lifetime previously let the marker expire while the tokens it revoked were still valid. API keys keep working, they are separate credentials with their own lifecycle.

Discussed in #28647.
2026-08-17 13:56:29 -07:00
Classic298
3fc491d22f
chore: drop test-only dependencies from the Docker image and the published package (#28726)
The Python test suite was deleted in 4527c747b but its dependencies stayed behind, so pytest, pytest-docker and the docker SDK still install into every image variant, and moto joins them for anyone running pip install open-webui[all]. No Python test file remains in the repository, nothing imports these packages, and no CI job runs pytest. They are removed from backend/requirements.txt and from the all extra, which are the only two channels they ship through.

netcat-openbsd goes for the same reason. It was added in January 2024 without a consumer and nc has never been invoked anywhere in the repository, in any script, workflow or compose file. Both the readiness wait and the healthcheck use curl, and the Ollama install script does not ask for it either.

uv.lock is regenerated output, not hand-edited. It drops three of the four packages plus three transitives that nothing else needs, with no version changes and no additions. pytest stays locked because pytest-asyncio in the dev group still requires it. The dependency markers it adds on the CUDA and numpy entries are inert: each one is a superset of the condition its parent already installs under, and the resolved default install set is identical before and after.

This saves roughly 2 MB uncompressed, which is nothing next to the image as a whole. The point is that a production image stops shipping a test framework and a Docker socket client it never uses.

Everything else stays and is load-bearing. The container installs pip packages at runtime for user-authored tools and functions, so it needs git and a working compiler for anything that is not a prebuilt wheel, and libmariadb-dev for the manual MariaDB install. zstd is required for updating Ollama inside the bundled image. black looks dev-only but backs the code formatting endpoint.

Ref: https://github.com/open-webui/open-webui/discussions/28716
2026-08-17 13:56:08 -07:00
Timothy Jaeryang Baek
ffda8aea80 refac 2026-08-17 01:34:37 -07:00
Timothy Jaeryang Baek
f67875ec57 refac 2026-08-17 01:30:21 -07:00
G30
88c55b86b1
feat: emit auth.login on SSO logins and attribute SSO logouts (#27619)
* feat: emit the auth.login event on SSO logins

* feat: attribute SSO logouts in the auth.logout event payload
2026-08-17 02:22:25 -06:00
Timothy Jaeryang Baek
a3a81fee03 refac 2026-08-17 01:21:58 -07:00
Classic298
646a568ae6
fix: enforce global web search and image generation switches on the legacy function-calling path (#27669)
The legacy function-calling path acted on the client-supplied `features` dict after checking only the per-user permission, so a user who still held `features.web_search` or `features.image_generation` could keep triggering web searches and image generation after an administrator had switched those off instance-wide. The native function-calling path already gates the equivalent builtin tools on `web.search.enable` and `image_generation.enable` in `get_builtin_tools`, so the two paths disagreed and the admin-level switch did not actually stop the outbound provider calls it was turned off to stop.

Gate the legacy web search handler on `web.search.enable` at its call site, and gate `chat_image_generation_handler` on the two image switches internally. The image handler needs the check inside it because `image_generation.enable` and `images.edit.enable` are independent: editing stays available when generation is disabled, matching the `/images/generations` and `/images/edit` routes and the native `generate_image`/`edit_image` tools. The handler calls `image_generations`/`image_edits` directly and so bypasses the route guards, which is why the check has to live at the caller.

The "Creating image" status event moves below the new guard so a disabled configuration returns without leaving an unresolved progress indicator in the chat.
2026-08-17 02:15:05 -06:00
G30
7ea46a37d0
fix: clip settings modal contents to its rounded corners (#27617) 2026-08-17 02:12:26 -06:00
Timothy Jaeryang Baek
b1dc945bd6 refac 2026-08-17 00:59:13 -07:00
Timothy Jaeryang Baek
0b27fa5e87 refac 2026-08-17 00:57:57 -07:00
Classic298
017075a2d7
perf: drop unused database session dependencies from seven endpoints (#28178)
Seven route handlers declare a request-scoped database session as a FastAPI dependency and then never touch it. Three of them are `GET /api/v1/users/user/settings`, `/user/status` and `/user/info`, which the frontend hits on every page load, and all three carry a comment saying the user object is already available, so the parameter is leftover from the refactor that removed the refetch. The other four are admin-only external-knowledge connection endpoints that read their data from the config store.

Measured on a route with and without the dependency, 20k requests, best of 5:

| | µs per request |
| --- | --- |
| no dependency | 16.18 |
| unused session dependency | 62.85 |

The dependency costs about three times as much as everything else the request does put together. It is worth being precise about why, because the obvious guess is wrong: this is not database I/O and not connection pool pressure. SQLAlchemy connects lazily, so a session that is never used checks out zero connections, verified by watching the pool's counter stay at zero across the request. The cost is FastAPI resolving an extra async-generator dependency onto the request's exit stack, plus constructing and closing the session object.

Deleting the seven parameters is the whole change. An AST scan over the backend finds exactly these seven handlers before and none after.
2026-08-17 01:53:00 -06:00
Timothy Jaeryang Baek
87d9b7e84e refac 2026-08-17 00:51:04 -07:00
Timothy Jaeryang Baek
6e468c5b95 refac 2026-08-17 00:50:54 -07:00
Timothy Jaeryang Baek
4ec6ee1441 refac
Co-Authored-By: Classic298 <27028174+Classic298@users.noreply.github.com>
2026-08-17 00:47:32 -07:00
Classic298
ba0c4b3932
fix: don't hold a database connection for the lifetime of an SSE stream (#28183)
With database session sharing enabled, which the docs recommend for PostgreSQL and for multi-replica deployments, the knowledge pending-files and file process-status endpoints each pinned one pooled connection for as long as their SSE stream stayed open, up to one and two hours respectively. A file wedged in processing keeps a stream open for the full duration, so a handful of users sitting on that page can consume every connection in the pool, and the held transactions sit idle and block autovacuum on those tables.

Both handlers took a request-scoped session for their access checks, and FastAPI only releases a yield dependency once the response body has finished streaming, so the session outlived the handler by the whole life of the stream. Neither generator ever used it. They no longer take that dependency, and the queries they run already open their own short-lived sessions when none is passed. This is the approach the chat completion endpoints already use for the same long-response problem.

Measured against a pool with capacity 11: before, at most 11 concurrent streams could ever be open and every further attempt failed, deterministically across repeat runs. After, 25 of 25 opened. Non-stream latency is unchanged, within run-to-run noise, and behaviour is identical whether session sharing is on or off.
2026-08-17 01:46:54 -06:00
Timothy Jaeryang Baek
e968445812 refac 2026-08-17 00:43:47 -07:00
Timothy Jaeryang Baek
d799e81edb refac 2026-08-17 00:42:16 -07:00
G30
c0d09a5de9
fix: list publicly shared read-only notes in the Read Only view (#27637) 2026-08-17 01:40:56 -06:00
James Kerrane
44f4d5b94f
chore: refresh outdated version examples in bug report template (#28188)
* refactor: remove unused optional assignees key

According to the GitHub docs (https://docs.github.com/en/communities/using-templates-to-encourage-useful-issues-and-pull-requests/syntax-for-issue-forms#top-level-syntax) this key is optional. Since it's unused, it is fine to remove.

* chore: bump version examples for software

Older versions might confuse people filing new issues, so newer versions of mentioned software are used as examples.
2026-08-17 01:39:13 -06:00
Timothy Jaeryang Baek
f1a64ccfc2 refac 2026-08-17 00:35:12 -07:00
Timothy Jaeryang Baek
76d0160295 refac 2026-08-17 00:31:01 -07:00
Timothy Jaeryang Baek
9550731cc1 refac 2026-08-17 00:24:47 -07:00
Classic298
189c14fc4d
fix: match both JSON text spellings when searching serialised JSON columns (#28399)
Three searches LIKE against cast(json_col AS text), which means they have to match
bytes a JSON encoder wrote. Encoders disagree on non-ASCII: stdlib escapes it to
\uXXXX, orjson writes it raw. Which one produced a row depends on the codec in force
when it was written, so any single pattern finds only half the table.

models.py hard-codes the stdlib spelling, with a comment asserting SQLite stores
JSON via json.dumps(ensure_ascii=True). Model.meta is a JSONField, which has
serialised through JSONCodec since ENABLE_ORJSON was introduced, so on that setting
it stores raw UTF-8 and the escaped pattern matches nothing: non-ASCII workspace
model tag search is broken today. prompts.py and automations.py hard-code the
opposite spelling and miss rows written the other way.

json_text_variants returns both spellings a string can take inside serialised JSON,
collapsing to one for ASCII, and the three call sites OR over them. Rows written
under either setting are now found under either setting, which also covers a
database holding a mix of the two.

Case handling is unchanged. models.py keeps matching non-ASCII tags case-sensitively
on SQLite, whose LOWER() is ASCII-only and would not fold the stored text the way
str.lower() folds the tag. ASCII tags collapse to a single variant and take exactly
the query they took before.

Verified on SQLite across every combination of codec-that-wrote-the-row and
codec-the-app-is-running, for an ASCII and a CJK tag, over all three call sites: 24
of 24 match, against 12 of 24 before. Quoting still bounds whole-tag matches, so
searching "weather" does not match a row tagged "weathervane".

Co-authored-by: Claude <noreply@anthropic.com>
2026-08-17 01:24:05 -06:00
G30
695d33aa7c
fix: let the chat column shrink so the collapsed sidebar rail is not pushed off screen (#28501) 2026-08-17 01:23:25 -06:00
G30
f5a5a434b9
fix: record an error state when a timer's chat completion raises (#27785) 2026-08-17 01:22:47 -06:00
G30
31897b7e34
fix: stop channel message hover actions overlapping code and table toolbars (#27737) 2026-08-17 01:20:54 -06:00
Timothy Jaeryang Baek
ad8c79f686 refac 2026-08-17 00:18:35 -07:00
Lin Junrong
211906d799
fix: keep references to lifespan background tasks (#28053)
periodic_usage_pool_cleanup, periodic_session_pool_cleanup and
scheduler_worker_loop were started with asyncio.create_task and their
handles discarded. The event loop keeps only a weak reference to a task,
so a task with no other referent can be garbage collected while it is
suspended at an await. All three are while True loops meant to run for
the process lifetime, and if one is collected the failure is silent:
pool entries stop being cleaned up, or automations and calendar alerts
stop firing, with nothing logged.

Six lines above, redis_task_command_listener is already stored on
app.state and cancelled on shutdown. This applies the same treatment to
the other three.

Closes #28052
2026-08-17 01:18:22 -06:00
Timothy Jaeryang Baek
0007369f4e refac 2026-08-17 00:16:11 -07:00
Timothy Jaeryang Baek
b6dc70c93b refac 2026-08-17 00:16:07 -07:00
Timothy Jaeryang Baek
d2af19ae3c refac 2026-08-17 00:15:36 -07:00
Classic298
3d630491c6
fix: re-syncing an existing model no longer fails silently (#28036)
POST /api/v1/models/sync only worked when every model in the payload was new. As soon as one id already existed, the whole call blew up and the endpoint still answered HTTP 200 with an empty list, so nothing was updated and well-behaved clients saw a success. Only a first-ever sync into an empty catalogue went through.

The update branch splatted the model dump (which already carries user_id and updated_at) and then passed both again as explicit keyword arguments, which is a duplicate-keyword TypeError before SQLAlchemy ever sees it. The insert branch right below merged the same values into a dict first, so it never collided.

Fixed by building that dict once and using it for both branches, matching how sync_functions already does it. Left the broad exception handler alone: it is the reason the failure was silent, but changing the error contract of sync_models is a separate call.

Fixes #28033
2026-08-17 01:14:11 -06:00
Classic298
b933292d63
refactor: track visited ids when resolving a chat's current message (#28035)
`delete_message_from_history` follows `childrenIds` down to the deepest leaf without recording where it has been. Record it.
2026-08-17 01:13:51 -06:00
Timothy Jaeryang Baek
f100edb708 refac 2026-08-17 00:12:13 -07:00
Classic298
27402ff210
refac: issue Playwright web loader requests from the shared HTTP clients (#28634)
The Playwright loader's route interceptor now performs each intercepted request with the same requests/aiohttp clients the other web loader paths already use and fulfills the page with that response, rather than having the browser issue it. Redirect handling, header forwarding and cookie delivery to the browser are unchanged.

Two consequences worth knowing. Page requests now leave from the backend instead of the browser, so with PLAYWRIGHT_WS_URL set they originate from a different host, and TLS is verified against certifi plus AIOHTTP_CLIENT_SSL_CERT_FILE rather than the browser's own trust store. And because the synchronous interceptor blocks, sub-resources on that path fetch one at a time: 30 assets at 40ms went from 2.01s to 3.01s, and 8 assets at 500ms from 1.05s to 4.50s. The asynchronous path is unaffected, at 0.65s and 1.05s respectively.
2026-08-17 01:06:12 -06:00
Classic298
73c1f5806a
refactor: match provider identity lookups via JSON subscript (#28624)
Both the OAuth and SCIM user lookups now compare the nested JSON value with SQLAlchemy's subscript operator, which emits the correct SQL for each supported database on its own. This replaces the hand-written sqlite and postgresql branches and the column-level contains() call they used.
2026-08-17 01:05:16 -06:00
G30
90724cdee0
fix: drop the white backdrop behind model icons in the admin Models list (#27612) 2026-08-17 01:03:37 -06:00
G30
8fc5ffe26e
fix: persist the Open Sharing permission in default user permissions (#27609) 2026-08-17 01:03:07 -06:00
Timothy Jaeryang Baek
0480ca9653 refac
Co-Authored-By: Solaris-star <67425364+solaris-star@users.noreply.github.com>
2026-08-17 00:01:38 -07:00
Timothy Jaeryang Baek
927ce0eae6 refac
Co-Authored-By: Classic298 <27028174+Classic298@users.noreply.github.com>
2026-08-16 23:58:56 -07:00
Damien S
54cefd2b99
fix: preserve complete user context in agentic retrieval (#27642)
* fix: preserve user info in agentic RAG tools

* fix: preserve user info in file access checks

---------

Co-authored-by: Damien SPINELLI <damien.spinelli@external.list.lu>
2026-08-17 00:56:48 -06:00
xyonium
686d8dc54c
fix: strip prefix id from model name in /responses endpoint (#28575)
The /openai/responses endpoint forwarded the prefixed model id (e.g.
"myprovider.gpt-4o") to the upstream provider instead of the stripped
native name, causing "model not found" errors when a connection has a
Prefix ID configured.

generate_chat_completion() already strips the prefix before forwarding;
apply the same strip_provider_model_prefix() call in responses() after
the urlIdx routing (which needs the prefixed id) and re-serialize the
body afterwards.

Also fixes the Azure non-v1 deployment path, which built the deployment
URL from the prefixed model name.

Co-authored-by: Claude <noreply@anthropic.com>
2026-08-17 00:53:00 -06:00
Classic298
3df485582d
fix: reject skill IDs that are not URL path safe (#27660)
A skill ID goes straight into the path of every mutating skill endpoint (/api/v1/skills/id/{id}/...), but create only replaced spaces with hyphens. An ID containing a "/" was stored verbatim as the primary key, so the route never matched, the request fell through to the SPA static mount and the client got 405 Method Not Allowed. The skill could not be opened, edited, toggled or deleted, by admins either, and since skill.name is UNIQUE it could not be recreated under a corrected ID. Percent-encoding does not help: uvicorn decodes the path before Starlette routes it, so the only remaining fix was a direct database write.

Create now rejects any ID outside [a-z0-9_-] with 400 instead of silently storing an unreachable one. Two frontend paths that fed unsanitized IDs into it are fixed as well: the manual "Skill ID" field, which was bound with no sanitization at all and is the path that reproduces on every version, and the markdown import, which put the raw frontmatter name into the ID before opening the editor in clone mode, where the reactive slugify is disabled.

Existing rows with an unreachable ID are not repaired here; rewriting a primary key would also have to re-point the access grants keyed on it.

Fixes #27655
2026-08-17 00:52:16 -06:00
Timothy Jaeryang Baek
954613944b refac 2026-08-16 23:51:38 -07:00
Classic298
805bfca5af
feat: emit group events on OAuth group sync (#27657)
With ENABLE_OAUTH_GROUP_MANAGEMENT enabled, every SSO login reconciles the user's group membership against the IdP claims, adding and removing them from groups and, with ENABLE_OAUTH_GROUP_CREATION, creating groups that do not exist yet. None of it emitted an event, so the same membership change was observable when an admin made it through the UI or when it arrived over SCIM, but invisible when the IdP drove it. That is the path that changes membership most often.

Emits group.member_added and group.member_removed per membership transition and group.created for each auto-created group, using the same payload keys as the groups router. The member events are published only when the write returned a group, so a failed or no-op write emits nothing, and both loops already run only on an actual transition. update_user_groups takes the request so the events can be published; it has a single caller.
2026-08-17 00:50:47 -06:00
Timothy Jaeryang Baek
d5d50169f4 refac 2026-08-16 23:48:05 -07:00
Timothy Jaeryang Baek
75df30c0ea refac 2026-08-16 23:47:02 -07:00
Timothy Jaeryang Baek
3e186abdd9 refac
Co-Authored-By: Classic298 <27028174+Classic298@users.noreply.github.com>
2026-08-16 23:41:58 -07:00
G30
2813eb44f2
fix: stop the What's New modal drawing two bullets per changelog entry (#28676) 2026-08-17 00:40:57 -06:00
G30
ad72dc6658
fix: scroll to the top of a chat on the first click of Scroll to Top (#28659) 2026-08-17 00:40:14 -06:00
Timothy Jaeryang Baek
16f118d77a refac 2026-08-16 23:38:34 -07:00
G30
884388cac3
fix(search): wire up mark as unread in the search chats modal (#28136) 2026-08-17 00:34:49 -06:00
Timothy Jaeryang Baek
736e38338e refac 2026-08-16 23:32:23 -07:00
Timothy Jaeryang Baek
a40f6f2860 refac 2026-08-16 23:28:48 -07:00
Timothy Jaeryang Baek
b5da50f3df refac 2026-08-16 23:26:24 -07:00
Classic298
cd9db21c52
refac: bind tool server cookies per connection (#28630)
The tool callable now takes its connection's cookie jar as a parameter, matching how its headers are already passed and how the terminal tool factory in the same module builds its callables.
2026-08-17 00:25:07 -06:00
Classic298
7d392bedc9
refac: align the channel completion gate with the message update route (#28631)
The gate now applies the same authorship condition the channel message update route already uses, so both paths agree on which messages a caller may modify.
2026-08-17 00:24:49 -06:00
Classic298
1756c9d5d2
fix: default pinned models stop applying after a user's first page load (#28069)
Changing "Default Pinned Models" in admin settings had no effect for anyone who had already opened Open WebUI once. The sidebar copied the admin default into that user's own settings the first time it rendered and saved it to the server, which marked them as having customized their pins, so every later change to the default was ignored for them. Merely loading the page was enough, the user never had to touch a pin.

The default is now resolved for display only, through a shared store that falls back to the admin list while the user has no pins of their own, the same way default models already work. Nothing is written to the user's settings until they actually pin, unpin or reorder something, at which point their choice takes over for good. Unpinning everything still persists an empty list rather than snapping back to the default.

Users whose settings were already overwritten by the old behaviour keep that copy, since a stored pin list cannot be told apart from a deliberate one.

Fixes a drag-reorder path that mixed sidebar positions with stored ones, and stops the sidebar section reopening itself after any unrelated settings change.
2026-08-17 00:22:51 -06:00
Timothy Jaeryang Baek
1a376ac17f refac 2026-08-16 23:21:00 -07:00
Timothy Jaeryang Baek
b211c407f4 refac 2026-08-16 23:14:08 -07:00
G30
1d1c14bd77
fix: compare folder names for uniqueness with lower() equality instead of a LIKE pattern (#28695) 2026-08-17 00:00:02 -06:00
Timothy Jaeryang Baek
e4694d6c3f refac 2026-08-16 22:59:10 -07:00
Timothy Jaeryang Baek
f7a533cda6 refac 2026-08-16 22:57:01 -07:00
Timothy Jaeryang Baek
3258330729 refac
Co-Authored-By: Classic298 <27028174+Classic298@users.noreply.github.com>
2026-08-16 22:56:19 -07:00
Timothy Jaeryang Baek
62fc436999 refac 2026-08-16 22:52:24 -07:00
Timothy Jaeryang Baek
31d08d592c refac
Some checks failed
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_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/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
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
Frontend Build / Format & Build (push) Has been cancelled
Frontend Build / Unit Tests (push) Has been cancelled
Create and publish Docker images with specific build args / merge (map[name:cuda suffix:-cuda]) (push) Has been cancelled
Create and publish Docker images with specific build args / merge (map[name:cuda126 suffix:-cuda126]) (push) Has been cancelled
Create and publish Docker images with specific build args / merge (map[name:main suffix:]) (push) Has been cancelled
Create and publish Docker images with specific build args / merge (map[name:ollama suffix:-ollama]) (push) Has been cancelled
Create and publish Docker images with specific build args / merge (map[name:slim suffix:-slim]) (push) Has been cancelled
Create and publish Docker images with specific build args / notify-helm-charts (push) Has been cancelled
Create and publish Docker images with specific build args / copy-to-dockerhub (, main) (push) Has been cancelled
Create and publish Docker images with specific build args / copy-to-dockerhub (-cuda, cuda) (push) Has been cancelled
Create and publish Docker images with specific build args / copy-to-dockerhub (-cuda126, cuda126) (push) Has been cancelled
Create and publish Docker images with specific build args / copy-to-dockerhub (-ollama, ollama) (push) Has been cancelled
Create and publish Docker images with specific build args / copy-to-dockerhub (-slim, slim) (push) Has been cancelled
2026-08-15 01:10:14 -06:00
Timothy Jaeryang Baek
467be93e6d refac 2026-08-15 01:09:39 -06:00
Timothy Jaeryang Baek
7fc5fa1ff3 refac 2026-08-15 01:02:06 -06:00
Timothy Jaeryang Baek
3eb65f4715 refac 2026-08-15 00:06:38 -06:00
Timothy Jaeryang Baek
30f82788bc refac 2026-08-15 00:05:45 -06:00
Timothy Jaeryang Baek
bbfdbd59f2 refac 2026-08-15 00:05:37 -06:00
Timothy Jaeryang Baek
98b9df0398 refac 2026-08-15 00:05:11 -06:00
Timothy Jaeryang Baek
0f821398ca refac 2026-08-14 23:52:13 -06:00
Timothy Jaeryang Baek
1b39ff352a refac 2026-08-14 23:50:34 -06:00
Timothy Jaeryang Baek
a5ea732c1e refac 2026-08-14 23:47:00 -06:00
Timothy Jaeryang Baek
516cf1a9a6 refac 2026-08-14 21:44:13 -06:00
Timothy Jaeryang Baek
d02b6a21fc refac
Some checks failed
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
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 (-ollama, ollama) (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 / Format & Build (push) Waiting to run
Frontend Build / Unit Tests (push) Waiting to run
Python CI / Ruff Format (3.11) (push) Has been cancelled
Python CI / Ruff Format (3.12) (push) Has been cancelled
2026-08-14 00:55:21 -06:00
Timothy Jaeryang Baek
c755ef60c6 refac 2026-08-14 00:54:33 -06:00
Timothy Jaeryang Baek
a1579a01ff refac 2026-08-14 00:22:17 -06:00
Timothy Jaeryang Baek
f7767d6be7 refac 2026-08-13 22:08:42 -06:00
Timothy Jaeryang Baek
d9014b3483 refac 2026-08-13 22:01:33 -06:00
Timothy Jaeryang Baek
57bd08304e refac 2026-08-13 22:01:17 -06:00
Timothy Jaeryang Baek
14e4d72d9a refac 2026-08-13 22:01:15 -06:00
Timothy Jaeryang Baek
256cce505b refac 2026-08-13 21:51:04 -06:00
Timothy Jaeryang Baek
55c202e841 refac 2026-08-13 21:48:01 -06:00
Timothy Jaeryang Baek
fa94a5ab24 refac 2026-08-13 21:36:41 -06:00
Timothy Jaeryang Baek
b4738d1a2e refac 2026-08-13 21:31:49 -06:00
Timothy Jaeryang Baek
653562d660 refac 2026-08-13 21:15:26 -06:00
Timothy Jaeryang Baek
a32a17965c refac 2026-08-13 21:06:51 -06:00
Timothy Jaeryang Baek
083e351441 refac
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 (-ollama, ollama) (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 / Format & Build (push) Waiting to run
Frontend Build / Unit Tests (push) Waiting to run
2026-08-13 21:02:17 -06:00
Timothy Jaeryang Baek
ec36972c2b refac 2026-08-13 20:02:08 -06:00
Timothy Jaeryang Baek
7d99b2716a refac 2026-08-13 19:59:11 -06:00
Timothy Jaeryang Baek
b018feb741 refac 2026-08-13 18:33:51 -06:00
Timothy Jaeryang Baek
133549a87e refac 2026-08-13 18:19:25 -06:00
Timothy Jaeryang Baek
4465f52a3e refac 2026-08-13 18:13:43 -06:00
Timothy Jaeryang Baek
2c01d59335 refac 2026-08-13 17:26:38 -06:00
Timothy Jaeryang Baek
7593698080 refac 2026-08-13 16:42:51 -06:00
Timothy Jaeryang Baek
2649e3305c refac 2026-08-13 16:42:10 -06:00
Timothy Jaeryang Baek
794671a988 refac 2026-08-13 16:16:10 -06:00
Timothy Jaeryang Baek
b7292890cc refac 2026-08-13 16:13:47 -06:00
Timothy Jaeryang Baek
f96b717566 refac 2026-08-13 16:04:53 -06:00
Timothy Jaeryang Baek
b3a5fd3875 refac 2026-08-13 15:57:42 -06:00
Timothy Jaeryang Baek
76583749ed refac 2026-08-13 15:52:44 -06:00
Timothy Jaeryang Baek
ec9bf5a64f refac 2026-08-13 15:49:50 -06:00
Timothy Jaeryang Baek
52c5e3b20d refac 2026-08-13 15:43:27 -06:00
Timothy Jaeryang Baek
c93c6d6fc4 refac 2026-08-13 15:42:54 -06:00
Timothy Jaeryang Baek
c1f914a626 refac 2026-08-13 15:38:25 -06:00
Timothy Jaeryang Baek
bfb68feea7 refac 2026-08-13 15:33:42 -06:00
Timothy Jaeryang Baek
2befa8f796 refac 2026-08-13 15:26:19 -06:00
Timothy Jaeryang Baek
7dfbdd221a refac 2026-08-13 15:20:59 -06:00
Timothy Jaeryang Baek
5c05608e3a refac 2026-08-13 15:02:18 -06:00
Timothy Jaeryang Baek
8d25ad00e2 refac 2026-08-13 14:52:30 -06:00
Timothy Jaeryang Baek
60feca71a6 refac 2026-08-13 14:50:43 -06:00
Timothy Jaeryang Baek
a17cb174ad refac 2026-08-13 14:46:08 -06:00
Classic298
5cb87e6630
fix: announce toggle state of integrations menu rows to screen readers (#27667)
Every toggle row in the chat integrations menu (filters, Web Search, Image, Code Interpreter, Tools, Skills) is a button whose on/off state was carried only by the decorative Switch inside it. Screen readers announced the row name and nothing else, so there was no way to tell whether a tool or feature was active without looking at it.

Each row button now carries aria-pressed, and the Switch wrapper is marked inert so the nested role=switch stops competing with the row for the announcement and stops adding a nameless tab stop. Hit testing skips inert content, so clicking the switch still toggles the row.

Tool rows that are not yet authenticated omit aria-pressed: activating those starts an OAuth redirect rather than toggling, so announcing them as an unpressed toggle would be wrong.

The Web Search, Image and Code Interpreter rows also had a state-flipping aria-label ("Disable Web Search") on top of aria-pressed, which announces as "Disable Web Search, pressed" and reads as the opposite of the truth. Removed: the visible row text already names each control.

Fixes #17150
2026-08-13 13:04:13 -06:00
Timothy Jaeryang Baek
ba885d0026 refac 2026-08-13 13:03:56 -06:00
G30
5c8d9e69c4
ci: auto-label feature requests with the enhancement label (#28531) 2026-08-13 12:55:03 -06:00
Timothy Jaeryang Baek
17c190bf99 refac 2026-08-13 02:07:02 -06:00
Timothy Jaeryang Baek
f8c5fda283 refac 2026-08-13 02:03:13 -06:00
Timothy Jaeryang Baek
31c1ffd55a refac 2026-08-13 00:45:02 -06:00
Timothy Jaeryang Baek
e1acd7e7ca refac 2026-08-13 00:38:32 -06:00
Timothy Jaeryang Baek
25802c048e refac 2026-08-13 00:34:10 -06:00
Timothy Jaeryang Baek
8260d527ee refac 2026-08-13 00:22:58 -06:00
Timothy Jaeryang Baek
85c3d0ae2f refac 2026-08-13 00:07:31 -06:00
Timothy Jaeryang Baek
3813fd4cdc refac 2026-08-12 23:20:19 -06:00
Timothy Jaeryang Baek
4e03d89414 refac 2026-08-12 23:17:37 -06:00
Timothy Jaeryang Baek
90bb94abf9 refac
Some checks are pending
Python CI / Ruff Format (3.11) (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
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 / 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 (-ollama, ollama) (push) Blocked by required conditions
Python CI / Ruff Format (3.12) (push) Waiting to run
Create and publish Docker images with specific build args / copy-to-dockerhub (-slim, slim) (push) Blocked by required conditions
Frontend Build / Format & Build (push) Waiting to run
Frontend Build / Unit Tests (push) Waiting to run
2026-08-12 18:13:47 -06:00
Timothy Jaeryang Baek
86b7bf1f7e refac 2026-08-12 18:10:51 -06:00
Timothy Jaeryang Baek
2e93987490 refac 2026-08-12 17:54:10 -06:00
Timothy Jaeryang Baek
939bcdb79e refac
Co-Authored-By: G30 <50341825+silentoplayz@users.noreply.github.com>
2026-08-12 01:13:38 -06:00
G30
1deeaf71da
fix: stop the calendar update tool from nulling every omitted field (#27777) 2026-08-12 01:09:07 -06:00
G30
2ab0311b99
fix: reject COUNT rules without DTSTART so limited automations cannot run forever (#27781) 2026-08-12 01:08:55 -06:00
G30
783e87c0c5
fix: dock the thread reply input below the scroll area instead of inside it (#27768) 2026-08-12 02:06:20 -05:00
G30
104a0f2f11
fix: remove vestigial api_base_url param that broke the Tavily web loader (#27636) 2026-08-12 02:05:26 -05:00
Timothy Jaeryang Baek
e44e16cb59 refac 2026-08-12 01:02:02 -06:00
Timothy Jaeryang Baek
c086b80313 refac 2026-08-12 00:51:12 -06:00
Timothy Jaeryang Baek
ab41dcc487 refac
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 (-ollama, ollama) (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 / Format & Build (push) Waiting to run
Frontend Build / Unit Tests (push) Waiting to run
2026-08-11 17:44:34 -06:00
Timothy Jaeryang Baek
9c21d4ed3b refac 2026-08-11 17:42:25 -06:00
G30
e17dfae72e
fix: enforce the image generation flag on the legacy chat feature path (#27759)
* fix: enforce the image generation flag on the legacy chat feature path

* fix: refresh the config store when image generation is disabled in admin settings

* fix: hide active feature pills when the feature is no longer available
2026-08-11 17:38:10 -06:00
Timothy Jaeryang Baek
1744b63f16 refac 2026-08-11 17:35:24 -06:00
Timothy Jaeryang Baek
4f9a0ebf71 refac 2026-08-11 17:35:05 -06:00
Timothy Jaeryang Baek
65c3539666 refac 2026-08-11 15:13:59 -06:00
Timothy Jaeryang Baek
0a598e6ff7 refac 2026-08-11 15:09:48 -06:00
Timothy Jaeryang Baek
1674e5a9ef refac 2026-08-11 12:50:50 -06:00
Timothy Jaeryang Baek
724d2ebbf1 refac 2026-08-11 11:46:12 -06:00
Timothy Jaeryang Baek
ac0368b4ab refac 2026-08-11 11:40:29 -06:00
Timothy Jaeryang Baek
f79b443c22 refac 2026-08-11 11:40:24 -06:00
G30
e58245d5b4
fix: place model editor capability checkboxes directly before their labels (#27788) 2026-08-11 11:13:04 -06:00
G30
25455e943d
fix(ui): resolve owner avatars against the API base url and add a fallback (#28272) 2026-08-11 11:05:35 -06:00
Timothy Jaeryang Baek
29541cbb52 refac 2026-08-11 01:27:50 -06:00
Timothy Jaeryang Baek
c8f8fa451a a11y
Co-Authored-By: Classic298 <27028174+Classic298@users.noreply.github.com>
2026-08-11 01:25:20 -06:00
Timothy Jaeryang Baek
865c80c160 refac 2026-08-11 01:17:19 -06:00
Timothy Jaeryang Baek
f0bfcd4097 refac 2026-08-11 01:15:05 -06:00
Timothy Jaeryang Baek
be4afd7545 refac 2026-08-11 01:11:56 -06:00
Timothy Jaeryang Baek
e963d36e39 refac 2026-08-11 01:08:58 -06:00
KastenMonster
2ac01a42be
feat: Add openserp as a provider in the ui (#27594)
* add OpenSerp Option

* fix: change input label to the docs label

---------

Co-authored-by: KastenMonster <kastenmonster@groundonline.de>
2026-08-11 00:21:05 -06:00
Timothy Jaeryang Baek
9122c24ea2 refac 2026-08-11 00:06:21 -06:00
G30
79382d1b19
fix: stop profile preview avatars from squashing in narrow flex rows (#28000) 2026-08-10 23:49:45 -06:00
Timothy Jaeryang Baek
68c67a1e0f refac
Co-Authored-By: G30 <50341825+silentoplayz@users.noreply.github.com>
2026-08-10 23:48:02 -06:00
Timothy Jaeryang Baek
5cd9a39534 refac 2026-08-10 23:40:31 -06:00
Timothy Jaeryang Baek
bd250a0e24 refac 2026-08-10 23:37:45 -06:00
Classic298
934802e186
fix: enforce features.memories permission on the legacy memory context path (#27668)
Revoking a user's `features.memories` permission removed their access to the memories API and to the native function-calling memory tools, but their stored memories were still injected into the system context on the legacy function-calling path.

The branch in `process_chat_payload` only checked the client-supplied `features['memory']` flag plus the global `memories.system_context.enable` switch, with no user-permission check. `add_memory_context` did not compensate: it only checks `model_allows_memory`, which is a model capability rather than a permission, and the one call inside it that does check the permission (`query_memory`) has its 403 swallowed by a `try/except`, so `Memories.get_memories_by_user_id` and the neighbourhood scan still fed the system prompt.

Gate the branch with the same permission check the native path already performs in `get_builtin_tools`, matching the neighbouring `web_search` and `image_generation` branches.

Only the caller's own memories were injected into the caller's own context, so there was no cross-user exposure. The practical effect was that the permission toggle did not do what its name implies: an admin who revoked it still got memory content injected for that user.
2026-08-10 23:36:27 -06:00
G30
d959e3e312
fix: scale the settings modal height with tall viewports instead of capping at 54rem (#27615) 2026-08-10 23:34:04 -06:00
Classic298
4eb0394511
fix: merged response receiving empty model responses after reload (#27673)
Clicking "Merged Response" in a multi-model chat often made the merging model answer with "It appears that the responses provided from the other models were empty".

The merge handler collected each model's answer via `history.messages[id].content`. Assistant messages are persisted by the backend with `output` only (`upsert_message_to_chat_by_id_and_message_id` writes `done`/`role`/`output`, never `content`), so `content` is only populated in the browser session that generated the responses, where the streaming handler mirrors it. Once the chat is reloaded from the database, every assistant message has `content: ''` and the merge request is sent with a list of empty strings, which is exactly what the merging model then reports. That is why the failure looks random: merging works right after generating, and fails after a refresh or when reopening the chat.

Read the responses through `getOutputText(message.output) || message.content`, the same fallback already used by every other read site (`ResponseMessage`, `Overview/Node`, `SearchModal`, `ChatItem`, `ChatMenu`, `Navbar/Menu`).

Fixes #26962
2026-08-10 23:31:21 -06:00
Classic298
fcc130c9bb
fix: send stream_options.include_usage for backend-initiated chats (#27661)
Only the frontend added `stream_options: {include_usage: true}` to the completion payload, gated on the model's `usage` capability. Every backend-initiated run builds its own payload (automations, timers, subagents, channels) and omitted it, so those responses came back without token counts and never rendered the usage block, even with the capability enabled on the model.

Set it in `chat_completion` instead, the single handler all of those callers go through, and drop the two duplicate copies (the Anthropic-compat handler and the frontend). Capabilities are read from the resolved model before the custom-model fallback can rebind it, and the flag is applied after the model's `stream_response` override so a non-streaming model is unaffected.

Fixes #27653
2026-08-10 23:30:46 -06:00
G30
085f1b5ca4
feat: add a user setting to toggle sidebar chat hover previews (#27632)
Co-authored-by: Tim Baek <tim@openwebui.com>
2026-08-10 23:29:51 -06:00
G30
8e83d83324
fix: left-edge clipping of the terminal cloud icon and account profile avatar (#27691) 2026-08-10 23:28:17 -06:00
Solaris-star
2387ce63cb
fix(chat): keep regenerate button visible in High Contrast mode (#27644)
Every action button on a response message gates its visibility on
'isLastMessage || ($settings?.highContrastMode ?? false)' so that buttons
stay visible (not hover-only) when High Contrast mode is on. The two
Regenerate buttons — the one inside RegenerateMenu and the fallback in the
{:else} branch — were missed and still gated on isLastMessage alone, so on
any non-last message the Regenerate icon was invisible until mouse-over even
with High Contrast enabled (#27638).

Add the same highContrastMode clause to both buttons, matching the other
action buttons in this file.
2026-08-10 23:27:42 -06:00
Classic298
be9af1653e
fix: anchor chat input expand button to the input row (#27676)
The "expand input" button was positioned with `fixed top-0 right-0`. That only kept it near the composer by accident: `#message-input-container` sets `backdrop-blur-sm`, and a backdrop-filter makes an element the containing block for fixed descendants, so the button resolved to the top-right corner of the entire composer instead of the text area it belongs to.

That corner is already taken. The `@`-tagged model chip renders as the first row of the same container with its dismiss button at the right end, so with a multi-line prompt and a tagged model the two controls are drawn on top of each other. The attached-files row has the same problem: the button paints over the first thumbnail and its remove button.

Anchor the button to the wrapper that holds the text area instead, using `relative`/`absolute`, so it always sits at the top-right of the input row and below whatever rows precede it. With no chip and no files the position is unchanged. As a side effect the button is no longer a child of the `overflow-auto` scroller, so it can no longer be clipped or scrolled out of view on long prompts.

Fixes #26736
2026-08-10 23:26:53 -06:00
G30
b91bb67b55
fix: hide chat fork actions when chat import permission is disabled (#27711)
Co-authored-by: Nameless-Monster-Nerd <Nameless-Monster-Nerd@users.noreply.github.com>
2026-08-10 23:24:59 -06:00
Classic298
5c79ccc9e5
refactor: walk chat message history by map key (#28034)
`get_message_list` moves through `messages_map` by key but tracked each message's own `id` field, which the message body does not have to carry. Track the key instead.
2026-08-10 23:24:41 -06:00
G30
80d2f4154a
fix: align public_tools and public_notes sharing defaults with config (#27716)
The SharingPermissions model defaulted public_tools and public_notes to
True while the config defaults (USER_PERMISSIONS_WORKSPACE_TOOLS_ALLOW_PUBLIC_SHARING
and USER_PERMISSIONS_NOTES_ALLOW_PUBLIC_SHARING) are both False.

On an instance whose stored user.permissions config predates these keys,
GET /api/v1/users/default/permissions fills the gap from the model and
reports both as enabled, while has_permission fills it from
DEFAULT_USER_PERMISSIONS and denies. Saving any unrelated permission then
persists the model's True, granting public tool and note sharing the admin
never enabled.
2026-08-10 23:24:18 -06:00
G30
a3d33b4cf3
fix: hide chat delete actions when the chat delete permission is disabled (#27714)
The chat delete endpoints gate on chat.delete for non-admins, but none of
the UI affordances that reach them were conditioned on it, so a user
without the permission is offered controls that always fail with
"Access prohibited".

Gates every entry point on the same condition the backend enforces,
matching the existing chat.share / chat.export / chat.import gates in the
same components:

- Sidebar chat menu (ChatMenu) Delete item — also covers the Search
  modal's menu, which reuses ChatMenu
- Sidebar Shift+hover inline Delete button (ChatItem)
- Sidebar hidden #delete-chat-button keyboard-shortcut target (ChatItem)
- Chat navbar menu Delete item (Navbar/Menu)
- Search modal Shift-held inline Delete button (SearchModal)
- Settings -> Data Controls -> Delete All Chats
- Settings -> Archived Chats per-row Delete
2026-08-10 23:24:02 -06:00
G30
fe62934be7
fix: derive overview node spacing from rem so branches never overlap (#27995) 2026-08-10 23:23:31 -06:00
Classic298
2207876ae7
fix: generate valid WEBUI_SECRET_KEY in start_windows.bat (#28061)
The key generation loop redirected input from a non-existent file
(`SET /p WEBUI_SECRET_KEY=<!random!>>%KEY_FILE%`), printing "The system
cannot find the file specified." once per iteration and leaving the key
file empty, so startup failed with "WEBUI_SECRET_KEY is not set".

Build a fixed-length alphanumeric key by indexing into a charset with
%RANDOM% and write it once with `<nul set /p`. Also quote the key file
path and use delayed expansion so paths with spaces work.


Claude-Session: https://claude.ai/code/session_01CmgBivWjad68mX4yBVWMi2

Co-authored-by: Claude <noreply@anthropic.com>
2026-08-10 23:22:43 -06:00
Timothy Jaeryang Baek
bd8378f643 refac 2026-08-10 23:22:08 -06:00
Classic298
1f22cccd22
perf: stop formatting every exported log record twice under OTEL log export (#27840)
With ENABLE_OTEL and ENABLE_OTEL_LOGS set, InterceptHandler builds the message once for loguru and then hands the same LogRecord to the OpenTelemetry handler, whose _translate calls record.getMessage() a second time. That used to be free, because the message was already a finished f-string with nothing to substitute. Now that log calls pass lazy %-args, the second call re-runs the whole interpolation, so every exported record is formatted twice.

The two getMessage() calls on a 78 kB retrieval record:

    before  373.0 us
    after     0.1 us

Stamping the built message back onto the record makes the second call a plain string return. msg and args are both in OpenTelemetry's _RESERVED_ATTRS, so neither ever reaches the exported attributes. The isinstance guard matters: _translate exports a non-str msg such as the dicts routers/audio.py logs as a typed body rather than a string, so those records are left untouched, and they have no %-args to format twice anyway. Body, attributes and severity were compared against LoggingHandler._translate for str, dict, list, int, None, exception and exc_info records.
2026-08-10 23:13:52 -06:00
Timothy Jaeryang Baek
ce3c175e26 refac 2026-08-10 23:13:10 -06:00
Timothy Jaeryang Baek
de289eb1aa refac 2026-08-10 23:07:47 -06:00
Timothy Jaeryang Baek
d8ae7ed405 refac 2026-08-10 23:01:41 -06:00
Cyp
bee1ded5ab
i18n: update Korean translations (#27681)
Co-authored-by: bglee <bglee@hct.co.kr>
2026-08-10 23:01:24 -06:00
Timothy Jaeryang Baek
629cdcb530 refac 2026-08-10 22:57:21 -06:00
Timothy Jaeryang Baek
89922cc9d5 refac 2026-08-10 22:53:37 -06:00
Timothy Jaeryang Baek
2a6e671f54 refac 2026-08-10 22:47:39 -06:00
Timothy Jaeryang Baek
c2107e5bb3 refac 2026-08-10 22:36:42 -06:00
Kylapaallikko
b7de04da14
Update fi-FI translation.json (#27700)
Added missing translations and improved existing ones.
2026-08-10 22:27:44 -06:00
G30
38a03830c2
fix: show a placeholder when an image cannot be loaded (#27730)
A message referencing a file that no longer exists rendered a broken
image, because nothing anywhere noticed the failed load. What the user saw
depended on the caller's alt text: in a response the alt is the message
content, so the entire reply was rendered inside the image frame, and the
preview still opened full-screen on a dead image.

Image.svelte now tracks a failed load and renders an 'Image unavailable'
placeholder instead, suppressing the preview for a source that cannot be
shown. The failed state resets when the source changes, so a replaced or
corrected URL is retried. An onError callback lets a caller react without
changing behaviour for the eight existing call sites, which are untouched.

Responses now pass the file name (or 'Generated Image') as alt rather than
the message content, which was never a description of the image.
2026-08-10 22:27:13 -06:00
Timothy Jaeryang Baek
0b4b7ae5ff refac 2026-08-10 22:20:53 -06:00
G30
04c22f0c41
fix: use the local date for the event modal's default start and end (#27779) 2026-08-10 22:19:54 -06:00
Classic298
3dbb4078b3
fix: repair two broken logging calls, one of which makes VECTOR_DB=opengauss unusable (#27838)
SRC_LOG_LEVELS became an empty dict when per-module log levels were dropped, and env.py keeps it only as a legacy name. opengauss.py is the last thing in the tree that still indexes it, at module scope, so importing the module raises KeyError: 'RAG' and any deployment on VECTOR_DB=opengauss dies the first time it touches the vector store. The factory imports it lazily, which is why nothing else trips over it. Deleting the line is the whole fix: every other vector backend takes getLogger(__name__) and inherits the root level.

colbert.py passes an argument to a message with no placeholder to consume it:

    log.info('ColBERT: Loading model', name)

At INFO, which is the default, logging evaluates 'ColBERT: Loading model' % ('colbert-ir/colbertv2.0',) and raises TypeError: not all arguments converted during string formatting. The record is swallowed by handleError, so loading a ColBERT reranker prints '--- Logging error ---' plus a traceback to stderr instead of the model name. Adding %s prints the name and drops the traceback.
2026-08-10 22:19:41 -06:00
Timothy Jaeryang Baek
3793b0c886 refac 2026-08-10 22:15:54 -06:00
Timothy Jaeryang Baek
3e9b075954 refac 2026-08-10 22:11:05 -06:00
Timothy Jaeryang Baek
2a45fa04cb refac 2026-08-10 21:58:12 -06:00
Teitur Bendtsen
b74c144c96
i18n: add Faroese translation (#28159)
Co-authored-by: T209211 <tbe@betri.fo>
2026-08-10 21:51:45 -06:00
Adam I. Horvath
cbd47eaa67
i18n: improve hungarian translations (#28214)
adds missing hungarian translations and improves existing hungarian ui labels.
2026-08-10 21:44:40 -06:00
Classic298
38fcee7f21
Update pull_request_template.md (#28246) 2026-08-10 21:43:33 -06:00
developersorli
f44647e251
feat(i18n): add complete Slovenian (sl-SI) translations (#28268) 2026-08-10 21:43:02 -06:00
Classic298
a680f21e12
feat: make OAuth admin settings read-only when ENABLE_OAUTH_PERSISTENT_CONFIG is off (#28276)
When ENABLE_OAUTH_PERSISTENT_CONFIG is off (the default), oauth.* config is
never persisted and is read from environment variables, but the admin panel
still let admins edit the OAuth/OIDC fields and silently dropped every save on
restart, which kept confusing users who missed the docs warning
(open-webui/open-webui#28247).

The OAuth/OIDC section is now read-only in that case: the admin oauth config
endpoint reports the flag and the UI wraps the section in a disabled fieldset,
slightly dimmed with every control inert but all values still visible, plus a
note naming the env var. Saving skips the OAuth POST since nothing can change.
With the flag enabled the section behaves exactly as before.

Known limits: the guard is UI-side only (the POST endpoint keeps accepting
writes, unchanged), and disabled fields mean values cannot be selected and the
masked client secret cannot be revealed while read-only. Switch.svelte gains a
disabled:cursor-not-allowed style that applies to any disabled switch app-wide.

Co-authored-by: Tim Baek <tim@openwebui.com>
2026-08-10 21:41:07 -06:00
G30
a3e5d0b362
fix(chat): attribute shared chat messages to their author, not the viewer (#28274) 2026-08-10 21:40:54 -06:00
Timothy Jaeryang Baek
8edab5020e refac 2026-08-10 21:39:27 -06:00
Timothy Jaeryang Baek
3c010951db refac 2026-08-10 21:21:52 -06:00
Timothy Jaeryang Baek
48a5696042 refac 2026-08-10 21:06:40 -06:00
Timothy Jaeryang Baek
d6679082e5 refac 2026-08-10 21:00:01 -06:00
Timothy Jaeryang Baek
943294df9a refac 2026-08-10 20:46:38 -06:00
G30
d661fb49b8
ci: relabel bug reports when the title is corrected after opening (#28333)
The labeler only listened for issues.opened, so it judged a title exactly
once. Reporters who omit the prefix are asked by the triage bot to add one,
and the title they then fix is never looked at again, leaving a genuine bug
report unlabelled until somebody notices it by hand.

It now also runs on issues.edited. Body only edits return immediately, so
the extra runs are limited to titles actually changing, and a bug label a
maintainer has already removed is not restored: on an edit the issue events
are checked for a previous removal first. The opened path is unchanged and
makes no additional API call.

The title pattern also required a delimiter after the bracket, so the
bracketed form the comment advertises, "[Bug] something is broken", never
matched unless it happened to be written "[Bug]: something is broken".
Both forms match now, while "[bug/perf]" keeps matching and titles that
merely mention the word, such as "[UI Bug]" or "fixed a typo", still do not.
2026-08-10 20:45:02 -06:00
G30
d10d552117
fix(chats): surface the error message instead of [object Object] (#28260) 2026-08-10 20:44:32 -06:00
Timothy Jaeryang Baek
178ccb30e1 refac 2026-08-10 20:37:53 -06:00
Timothy Jaeryang Baek
5cecb7dbfa refac
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: 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
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 (-ollama, ollama) (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 / Format & Build (push) Waiting to run
Frontend Build / Unit Tests (push) Waiting to run
2026-08-10 20:30:56 -06:00
G30
148283f974
fix: release a queued message once an attached URL finishes (#28381)
A message sent while an attachment is still uploading is held in the chat
queue until the file settles. For files that release works, because the
message input calls onUpdate once the upload completes, which refreshes the
queued entry and asks the queue to run.

Attaching a URL takes a different path. uploadWeb sets the item to uploaded
but calls neither onUpdate nor processNextInQueue, and the queue is
otherwise only revisited when a generation finishes or when the chat is
mounted while idle. In a new chat with nothing generating, none of those
happen, so a message queued behind a URL waits with no event able to
release it until the chat is reloaded.

uploadWeb now asks the queue to run when it is done, the same way the file
path already does.
2026-08-10 20:24:35 -06:00
Timothy Jaeryang Baek
b20bcdbba7 refac 2026-08-10 20:22:29 -06:00
Timothy Jaeryang Baek
5ec16e76e6 refac 2026-08-10 20:13:03 -06:00
Classic298
5462c02af0
fix: OIDC login fails when the provider adds a private JOSE header (#28065)
Logging in through CyberArk Identity dies at the callback with "Unsupported {'app_id'} in header" and the user sees "The email or password provided is incorrect". Any provider that puts a vendor-specific parameter in the ID token header hits this; CAS was already patched by name, CyberArk is the next one.

Authlib 1.7 verifies ID tokens with joserfc, which rejects header parameters it does not recognise. The old fix registered `client_id` so CAS would work, which only ever fixes one provider at a time. This turns off the unknown-header rejection instead, so any private header parameter is ignored rather than fatal. Signature verification, the algorithm allowlist, `crit` handling and value validation of registered headers all still run, so nothing that actually protects the token is relaxed.

Fixes #28062
2026-08-10 20:06:31 -06:00
Timothy Jaeryang Baek
a41faa3c22 refac 2026-08-10 20:00:43 -06:00
Classic298
c5ec01b1f9
fix: make the aiodns resolver opt-in and pin aiodns to 3.6.1 (#28242)
Since v0.11.0 shipped aiodns, aiohttp silently switched every outbound request from the OS resolver to c-ares. On some Windows hosts the bundled c-ares 1.34.6 (pycares 5) discovers only 127.0.0.1:53 as nameserver, so every external provider lookup fails (#28013). In Docker the long-lived c-ares channel intermittently stops resolving container names while Docker's embedded DNS keeps answering, which wipes the Ollama model list and fails all in-flight chats with a misleading "Model not found" (#28215).

This restores the pre-0.11 ThreadedResolver (OS resolver) by default and gates the c-ares path behind a new env var, AIOHTTP_CLIENT_ASYNC_DNS_RESOLVER, off by default. The event-loop DNS perf improvement is now opt-in for deployments whose resolver setup is known to work with c-ares, instead of a process-wide side effect of the package being installed.

aiodns is also downgraded and pinned to 3.6.1 (pycares<5), the last release before the broken c-ares 1.34.6 build, so opting in does not hit the Windows regression. The hardcoded AsyncResolver in the Mistral OCR loader now follows the same switch. Simply removing aiodns instead was not an option because opting in would then be impossible, and #28215 showed the Docker failure is c-ares itself, not aiodns 4.x.
2026-08-10 19:52:29 -06:00
Timothy Jaeryang Baek
8d1c205d8e refac 2026-08-10 19:46:46 -06:00
Timothy Jaeryang Baek
b606e13da3 refac 2026-08-10 19:44:56 -06:00
Classic298
eff5c4a2d9
feat: parse :::writing block metadata and use the subject as the block title (#28280)
Newer OpenAI chat models put metadata on the opening line of a colon fence block, like :::writing{variant="email" id="48173" subject="Short question" recipient="mail@example.com"}. The tokenizer matched that line and discarded it, so every block rendered under the same generic "Writing" heading no matter what it contained.

The opening line is now parsed into an attributes map on the token and the header uses it: the subject becomes the title, the recipient follows it and the full string is reachable on hover when the row is too narrow for it. Blocks without metadata render exactly as before, and the other fence types get the parsed attributes for free.

Attributes are read only from inside the {...} braces, not from the whole opening line. Scanning the whole line turned ordinary prose containing key="value" into metadata, and it backtracked quadratically: a 40k character opening line took 586ms to parse, and that runs again on every re-lex while the message streams. Anchored to the braces it is 0.0ms.

Nothing here turns the recipient into a link or a send action. That metadata is model output and can be steered by whatever is in the context, so a prefilled mail action is a separate decision rather than a side effect of parsing.
2026-08-10 19:44:06 -06:00
Timothy Jaeryang Baek
385d08bea5 refac 2026-08-10 19:42:05 -06:00
Timothy Jaeryang Baek
11739a2de8 refac 2026-08-10 19:41:05 -06:00
Classic298
92f9f36c69
Update CODE_OF_CONDUCT.md (#28349) 2026-08-10 19:34:33 -06:00
Timothy Jaeryang Baek
e4dd6c4bf1 refac 2026-08-10 19:33:48 -06:00
Timothy Jaeryang Baek
d22bb6703f refac 2026-08-10 19:28:21 -06:00
Classic298
e5b24a22d0
fix: model ID whitelists accepting duplicate entries (#28251)
Adding a model ID that was already on the list in the connection settings modal simply appended it again, so the same model could sit in the whitelist any number of times. The arena model modal had the same flaw, its dropdown kept offering models that were already selected.

The connection modal now rejects a duplicate with a toast and trims the input first; surrounding whitespace renders invisibly in the list, so an untrimmed ID would slip past the duplicate check and still show up as a visually identical row. The arena modal instead filters already-added models out of the dropdown, matching the existing model selector in the admin settings, so a duplicate can no longer be picked at all. Both modals also drop duplicates when loading a stored list, so configs that already contain them are cleaned on their next save.

Until such a config is re-saved, one residual effect of old data remains: a duplicated ID in an arena model's stored list keeps double weight in the random model draw. New duplicates can no longer be created through the UI.

Fixes #28249
2026-08-10 19:27:12 -06:00
Timothy Jaeryang Baek
ff74bfa6a1 refac 2026-08-10 19:25:26 -06:00
Timothy Jaeryang Baek
f8ac75d188 refac 2026-08-10 19:21:52 -06:00
G30
8836dcb59f
fix(ui): fall back to the default avatar when a profile image fails to load (#28270) 2026-08-10 19:18:32 -06:00
G30
121f2404ee
fix(retrieval): report why a URL could not be read instead of blaming the knowledge base (#28362)
Fetching a URL and saving it were reported as one thing. Everything from
reading the URL to writing the vector database sat inside a single try,
whose handler blamed the knowledge base, so a page that could not be
fetched, parsed or resolved was reported as a knowledge base error even
though nothing had reached the knowledge base yet. Reading the URL now has
its own handler that names the URL, and the knowledge base message is left
to the step that actually touches it.

When YouTube refused a transcript the reason was discarded earlier still:
the loader caught the error, logged it, and returned an empty document
list, so the empty result failed downstream and even the salvageable
explanation was gone before a message was produced. The loader now raises
YoutubeTranscriptError carrying a readable reason, mapped from the
transcript library's own exception types. Blocked requests mention that a
proxy can be configured, and disabled, age restricted, unavailable and
missing language cases each say what actually happened.

URLs that attach successfully are unaffected.
2026-08-10 19:18:04 -06:00
Classic298
d9e23b90c1
refac: share one folder write-access check across chat folder_id paths (#28366)
Chat creation and chat moves each carried their own copy of the same folder_id validation, resolving the folder and checking ownership and shared write access in slightly different ways. Both now call a single has_folder_write_access helper, which the chat-completions creation path uses as well, so ownership, inherited write grants and nonexistent or malformed ids behave identically everywhere a chat folder_id is set. The owner case also costs one query fewer than before.
2026-08-10 19:17:18 -06:00
Timothy Jaeryang Baek
72a909fd2f refac 2026-08-10 19:16:21 -06:00
Timothy Jaeryang Baek
ec03e88144 refac 2026-08-10 19:08:46 -06:00
Timothy Jaeryang Baek
b5f86e6a43 refac 2026-08-10 19:08:06 -06:00
Timothy Jaeryang Baek
30d08a42f8 refac 2026-08-10 18:52:58 -06:00
Timothy Jaeryang Baek
407c40f72c refac 2026-08-10 18:52:34 -06:00
Timothy Jaeryang Baek
b4d3b27caf refac 2026-08-10 18:52:27 -06:00
Timothy Jaeryang Baek
d4461bd6f3 refac 2026-08-10 18:52:18 -06:00
Timothy Jaeryang Baek
eeaf1a1df0 refac 2026-08-10 18:51:38 -06:00
Timothy Jaeryang Baek
90a0e61cef refac 2026-08-10 18:50:43 -06:00
Timothy Jaeryang Baek
37f2548155 refac 2026-08-10 18:46:36 -06:00
Timothy Jaeryang Baek
13346c5f16 refac 2026-08-10 18:43:55 -06:00
Classic298
8d6a7c8308
perf: route native JSON columns through JSONCodec instead of stdlib json (#28396)
JSONField serializes with JSONCodec, but columns declared as SQLAlchemy's own JSON
type go through the engine's serializer instead, and no engine set one. That left
Chat.chat - the largest blob the app stores - on stdlib json.dumps/loads no matter
what ENABLE_ORJSON was set to, while the rest of the app used the codec. SQLAlchemy
invokes it once per write and once per read, so every chat read and write paid a
full stdlib pass over the whole conversation on top of whatever the caller did.

Both engine constructors are now wrapped so the codec is wired in by default and
cannot be missed by a call site that forgets it; an explicit json_serializer still
wins. The 10 create_engine/create_async_engine calls in this module go through the
wrappers. Vector-store engines (pgvector, mariadb, opengauss) are separate databases
and are left alone.

Serializing and deserializing chat-shaped blobs, median of 11 runs:

| chat blob | write | read |
| --- | --- | --- |
| 600 msgs (2.8 MB) | 10.1 -> 1.7 ms | 8.4 -> 3.7 ms |
| 3000 msgs (14.2 MB) | 51.9 -> 8.0 ms | 48.5 -> 27.8 ms |
| 6000 msgs (28.5 MB) | 105.9 -> 29.5 ms | 112.7 -> 80.5 ms |

With ENABLE_ORJSON off JSONCodec is stdlib json, so this is a no-op until the flag
is set - the change cannot regress a default deployment.

With it on, a round-trip probe through a native JSON column returns objects equal to
the stdlib ones on all 12 shapes tried: ASCII, CJK, emoji, astral-plane, unicode
keys, null bytes, lone surrogates, floats, ints above 2**63 and 2**64, line
separators, empty and deeply nested. Stored text changes for non-ASCII, which is
written as raw UTF-8 rather than backslash-uXXXX escapes and is correspondingly
smaller. Nothing queries that text by escape except two Postgres safety filters in
chats.py, and both still hold: a null byte is escaped identically by both codecs,
and the title filter reads a text column rather than JSON. The ->> and json_extract
searches decode the string before matching, so escaping cannot reach them.

Two differences are inherent to JSONCodec and already apply to every JSONField
column: ints beyond 2**64-1 come back as float, and NaN/Infinity serialize to null
rather than the bare literals stdlib emits - the latter being invalid JSON that a
Postgres json column rejects today. Neither shape occurs in chat blobs. Alembic
builds its own engine and stays on stdlib, which is fine in both directions since
each codec reads the other's output.


Claude-Session: https://claude.ai/code/session_014BXoM6QiFJKisxcxKAXii8

Co-authored-by: Claude <noreply@anthropic.com>
2026-08-10 19:39:49 -05:00
Timothy Jaeryang Baek
8fbfd14a8b refac 2026-08-10 01:38:32 -06:00
Timothy Jaeryang Baek
060648f939 refac 2026-08-10 01:36:09 -06:00
Timothy Jaeryang Baek
61110677d4 refac 2026-08-10 01:32:34 -06:00
Timothy Jaeryang Baek
f9cd49443c refac 2026-08-10 01:32:29 -06:00
Timothy Jaeryang Baek
4e69166017 refac 2026-08-10 01:32:11 -06:00
Timothy Jaeryang Baek
048c063993 refac 2026-08-10 01:26:04 -06:00
Timothy Jaeryang Baek
ff7467b4c5 refac 2026-08-10 01:14:53 -06:00
Timothy Jaeryang Baek
8dd23f74c9 refac 2026-08-10 00:52:14 -06:00
Timothy Jaeryang Baek
1b72899f24 refac 2026-08-10 00:26:44 -06:00
Timothy Jaeryang Baek
a33fa05adc refac 2026-08-10 00:19:52 -06:00
Timothy Jaeryang Baek
5b8975b7da refac 2026-08-10 00:05:55 -06:00
Timothy Jaeryang Baek
2dadc5435a refac
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/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_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 / 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 / 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 / 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 / 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 (-ollama, ollama) (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 / Format & Build (push) Waiting to run
Frontend Build / Unit Tests (push) Waiting to run
2026-08-09 13:22:46 -06:00
Timothy Jaeryang Baek
5caa91a493 refac
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: 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
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 (-ollama, ollama) (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 / Format & Build (push) Waiting to run
Frontend Build / Unit Tests (push) Waiting to run
2026-08-08 18:47:57 -06:00
Classic298
74a7902821
fix: apply response.output_item.done instead of ignoring it (#28310)
The Responses API handler had a branch for response.output_item.done whose own comment said it was handled specifically below, but it never ran. The generic branch matching any response.*.done event came first in the chain and matched this event too, so it fell through and returned the accumulated output unchanged, leaving the dedicated branch below unreachable since the feature was added.

Moving the dedicated branch above the generic one makes the event apply. On a compliant stream this changes nothing, since response.completed replaces the whole output with the same data straight afterwards. It matters when a provider is less tidy: one that never sends response.content_part.added leaves the assistant's own reply unextractable from the next turn's context, and one that omits response.content_part.done drops the annotations that only arrive with the finished item. Both are repaired by honouring the event.

Worth knowing: the item replaces whatever the deltas accumulated, with no guard against a provider sending back less than it streamed. A reasoning item arriving without its content would therefore lose the reasoning body, which is the same shape of provider brokenness that #27800 already needed a guard for.
2026-08-08 18:33:39 -06:00
Classic298
a39126c27c
fix: catch the socket.io timeout in the event caller (#28311)
An interactive prompt raised by __event_call__ was meant to come back as an error dictionary when it timed out. It never did: sio.call raises socketio.exceptions.TimeoutError, which does not inherit from the builtin TimeoutError the handler was catching, so the exception escaped into plugin code instead. Because that exception carries no message, the call sites that wrap plugin calls in except Exception as e turned it into an empty string, so a timed-out prompt looked like an empty answer rather than a failure, and the error branches written for it were dead.

The handler now catches socketio's class alongside the builtin, so a timeout returns the intended error dictionary and a plugin can tell the two apart.

The session eviction that sat inside that handler is removed rather than switched on. It had never executed, and it is wrong in both directions: it compares the pool entry by value, which the heartbeat rewrites every thirty seconds, so it would usually not fire, and when it did fire on a short timeout it would evict a live tab whose user had simply not answered yet, with nothing to restore the entry short of a reload. Genuinely dead sessions are already reaped on missed heartbeats by periodic_session_pool_cleanup.

WEBSOCKET_EVENT_CALLER_TIMEOUT is unset by default, which means no timeout at all, so this only affects deployments that set it.
2026-08-08 18:33:12 -06:00
Classic298
fc8a9b8ed6
fix: stop the Responses delta handler falling through to a crash (#28312)
The generic response.*.delta branch could leave the streaming handler in two states that crash the caller. It bound its result only inside the guard that checks the target item exists, but returned that result outside the guard, so a delta arriving before its output item, or carrying an index past the end, raised UnboundLocalError. Separately, an event name with only two dot-separated parts failed the length check and fell off the end of the branch, so the function returned None and both call sites raised TypeError unpacking it.

Where the response is streamed to a browser both crashes were swallowed at debug level and cost a chunk. On the direct API path there is no handler between here and the server, so the caller kept its 200 while the body was cut short with no [DONE], and the outlet filters never ran.

The return now sits inside the guard with a branch-level fallback that hands back the accumulated output untouched, which is what the sibling done branch and every other skip path in this function already do. Deltas whose item exists behave exactly as before.

Dropping an orphan delta is deliberate rather than synthesizing the missing item: response.output_item.added appends without regard to output_index, so a placeholder would be duplicated when the real item arrives, and a fabricated function_call would have no name or call id.
2026-08-08 18:33:00 -06:00
Timothy Jaeryang Baek
009999f363 refac 2026-08-08 15:47:10 -06:00
Timothy Jaeryang Baek
8faaf2cd1e refac
Some checks failed
Python CI / Ruff Format (3.11) (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 free_disk:true name:cuda suffix:-cuda]) (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
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
Frontend Build / Format & Build (push) Has been cancelled
Frontend Build / Unit Tests (push) Has been cancelled
Python CI / Ruff Format (3.12) (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 / merge (map[name:cuda suffix:-cuda]) (push) Has been cancelled
Create and publish Docker images with specific build args / merge (map[name:cuda126 suffix:-cuda126]) (push) Has been cancelled
Create and publish Docker images with specific build args / merge (map[name:main suffix:]) (push) Has been cancelled
Create and publish Docker images with specific build args / merge (map[name:ollama suffix:-ollama]) (push) Has been cancelled
Create and publish Docker images with specific build args / merge (map[name:slim suffix:-slim]) (push) Has been cancelled
Create and publish Docker images with specific build args / notify-helm-charts (push) Has been cancelled
Create and publish Docker images with specific build args / copy-to-dockerhub (, main) (push) Has been cancelled
Create and publish Docker images with specific build args / copy-to-dockerhub (-cuda, cuda) (push) Has been cancelled
Create and publish Docker images with specific build args / copy-to-dockerhub (-cuda126, cuda126) (push) Has been cancelled
Create and publish Docker images with specific build args / copy-to-dockerhub (-ollama, ollama) (push) Has been cancelled
Create and publish Docker images with specific build args / copy-to-dockerhub (-slim, slim) (push) Has been cancelled
2026-08-05 10:37:38 -05:00
Timothy Jaeryang Baek
c1c07cbe0f refac 2026-08-05 07:57:55 -05:00
Timothy Jaeryang Baek
6c4d0ace16 refac 2026-08-05 07:44:06 -05:00
Timothy Jaeryang Baek
29eeda9f9a refac 2026-08-05 07:12:28 -05:00
Timothy Jaeryang Baek
9c7ce154e7 refac 2026-08-05 07:04:30 -05:00
Timothy Jaeryang Baek
cbb3aade2b refac 2026-08-05 06:41:30 -05:00
Timothy Jaeryang Baek
0800c21c64 refac 2026-08-05 00:47:49 -05:00
Timothy Jaeryang Baek
8dbbc206c5 refac
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: 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
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 (-ollama, ollama) (push) Blocked by required conditions
Create and publish Docker images with specific build args / copy-to-dockerhub (-slim, slim) (push) Blocked by required conditions
2026-08-04 00:41:26 -05:00
Classic298
2d18727ab8
perf: build info log messages lazily so raising the log level actually saves work (#27837)
Some checks failed
Python CI / Ruff Format (3.11) (push) Has been cancelled
Python CI / Ruff Format (3.12) (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_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/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
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 / merge (map[name:cuda suffix:-cuda]) (push) Has been cancelled
Create and publish Docker images with specific build args / merge (map[name:cuda126 suffix:-cuda126]) (push) Has been cancelled
Create and publish Docker images with specific build args / merge (map[name:main suffix:]) (push) Has been cancelled
Create and publish Docker images with specific build args / merge (map[name:ollama suffix:-ollama]) (push) Has been cancelled
Create and publish Docker images with specific build args / merge (map[name:slim suffix:-slim]) (push) Has been cancelled
Create and publish Docker images with specific build args / notify-helm-charts (push) Has been cancelled
Create and publish Docker images with specific build args / copy-to-dockerhub (, main) (push) Has been cancelled
Create and publish Docker images with specific build args / copy-to-dockerhub (-cuda, cuda) (push) Has been cancelled
Create and publish Docker images with specific build args / copy-to-dockerhub (-cuda126, cuda126) (push) Has been cancelled
Create and publish Docker images with specific build args / copy-to-dockerhub (-ollama, ollama) (push) Has been cancelled
Create and publish Docker images with specific build args / copy-to-dockerhub (-slim, slim) (push) Has been cancelled
Raising GLOBAL_LOG_LEVEL to WARNING buys quieter output but not less work: 241 INFO call sites interpolate their payload into an f-string before the logging call gets to drop it. The heaviest is get_doc, which logs every chunk id and metadata dict in a collection, so on the full-context retrieval path that is the entire knowledge base, once per chat request.

That one line at WARNING, CPython 3.12:

| knowledge base | payload | before   | after   |
| -------------- | ------- | -------- | ------- |
| top-k of 3     | 1.2 kB  | 3.8 us   | 0.07 us |
| 500 chunks     | 201 kB  | 583.6 us | 0.08 us |
| 5000 chunks    | 2.0 MB  | 5.8 ms   | 0.15 us |

The lazy form log.info('query_doc:result %s %s', result.ids, result.metadatas) hands the payload to record.getMessage(), which the InterceptHandler only reaches once a record has passed the level check. Output at INFO is byte-identical. Two sites that already built their message eagerly, one str concat and one % operator, move to the same lazy form.
2026-08-02 15:39:10 -05:00
Classic298
52145eede9
perf: take the orjson fast path for ensure_ascii=False callers (#27841)
Some checks are pending
Python CI / Ruff Format (3.12) (push) Waiting to run
Python CI / Ruff Format (3.11) (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
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 / 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 (-ollama, ollama) (push) Blocked by required conditions
Create and publish Docker images with specific build args / copy-to-dockerhub (-slim, slim) (push) Blocked by required conditions
2026-07-31 20:39:10 -05:00
Classic298
615807ad0b
chore: remove unused json import in models/chats.py (#27796)
Some checks failed
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
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: 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/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 / 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 (-ollama, ollama) (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 / Format & Build (push) Has been cancelled
Frontend Build / Unit Tests (push) Has been cancelled
`backend/open_webui/models/chats.py` imports `json`, but the module contains no `json.` references. The only remaining matches in the file are SQL function names such as `json_each` and `json_typeof` inside `text()` strings, which are unrelated to the import.

Noticed while profiling the chat read/write path: the import made it look as though the module serialized locally, when all of that happens in `utils/misc.py:sanitize_data_for_db`.

One-line deletion, no behaviour change.
2026-07-31 19:09:41 -05:00
Classic298
798f3935ae
fix: keep streamed Responses output when response.completed reports an empty output array (#27800)
The `response.completed` handler replaced the accumulated output with the terminal event's `output` whenever that key was present, guarded only by `is not None`. An empty array satisfies that guard, so a provider that finishes the stream with `"output": []` wiped everything collected from `response.output_item.added`, `response.output_text.delta` and `response.output_item.done`.

The assistant message was then persisted with `output: []` and empty content, which shows up as a reply that renders correctly while streaming and disappears the moment the stream ends.

Fall back to the accumulated output when the terminal array is empty. A spec-compliant `response.completed` still wins, since a populated array is truthy, and when nothing was streamed the accumulated output is empty too, so the fallback cannot invent content.

Fixes #27789
2026-07-31 19:09:32 -05:00
Classic298
ac8af4996c
perf: index group_member on (user_id, group_id) (#27822)
Permission checks are the most repeated database work in a request, and every one of them asks the same question: which groups is this user in. Today that question cannot use an index.

`group_member` has only its primary key and a `(group_id, user_id)` unique constraint. That constraint leads on `group_id`, so a lookup by `user_id` has to walk the entire membership table, every time. `Groups.get_groups_by_member_id` sits under `has_permission`, `has_access`, `check_model_access` and the `AccessGrants` fallbacks, so an ordinary chat completion pays that walk several times before the model is even called, and the admin user list pays it once per row.

The cost scales with total memberships across all users rather than with the size of any one user's, so it stays invisible on a small instance and then arrives all at once on a large one.

Measured on SQLite, timing the real join from `get_groups_by_member_id`:

| memberships | before | after |
|---|---|---|
| 5,000 | 0.04 ms | 0.03 ms |
| 50,000 | 0.10 ms | 0.04 ms |
| 200,000 | 1.33 ms | 0.04 ms |
| 500,000 | 2.94 ms | 0.04 ms |

The after column is flat because the lookup becomes a seek instead of a scan. Concretely: on a deployment with 500k memberships, say 10,000 users in 50 groups each, one chat completion currently spends roughly 15 ms of database time answering the same question over and over. Afterwards it is under 0.2 ms. On a small install you will not be able to measure the difference, and that is fine, the point is that the curve stops bending.

The index is `(user_id, group_id)`. The trailing column makes those lookups index-only, since `group_id` is the column they select. Queries that lead on `group_id`, such as `get_group_user_ids_by_id` and the `chat_messages` subqueries, are already served by the existing unique constraint and are unaffected.

What to expect when the migration runs: on PostgreSQL this is a plain `CREATE INDEX`, which takes a SHARE lock, so reads continue while writes to `group_member` block until it completes. The table holds one row per membership, so expect sub-second even on the numbers above. `CONCURRENTLY` cannot be used here because the migration runner wraps the upgrade in a transaction, and it is not warranted at this table size.
2026-07-31 19:09:15 -05:00
Classic298
52cfb02c72
perf: build debug log messages lazily so disabled debug logs cost nothing (#27834)
GLOBAL_LOG_LEVEL defaults to INFO, so every log.debug(...) in the backend is discarded, but the message is built first: 187 call sites interpolate their payload into an f-string before the logging call runs, so the work happens on every request and the result is thrown away. The worst one sits in process_chat_payload and stringifies the whole request body, full conversation history included, once per chat completion.

That one line with DEBUG disabled, CPython 3.12:

| conversation | payload | before   | after   |
| ------------ | ------- | -------- | ------- |
| 4 messages   | 1.2 kB  | 3.4 us   | 0.07 us |
| 20 messages  | 17 kB   | 24.8 us  | 0.07 us |
| 60 messages  | 123 kB  | 216.6 us | 0.07 us |

The lazy form log.debug('form_data: %s', form_data) hands the payload to record.getMessage(), which the InterceptHandler only reaches once a record has passed the level check. With DEBUG enabled the emitted lines are byte-identical, f'{x=}' sites included: those map to %r. MistralLoader._debug_log callers get the same treatment, since that wrapper already forwards *args.
2026-07-31 19:09:01 -05:00
G30
b67804f2b6
fix: do not auto-open artifacts from the sidebar chat hover preview (#27773) 2026-07-31 17:58:02 -04:00
Timothy Jaeryang Baek
78ed5a0235 refac
Co-Authored-By: Classic298 <27028174+Classic298@users.noreply.github.com>
2026-07-31 17:45:07 -04:00
Timothy Jaeryang Baek
bb0f898b43 refac 2026-07-31 17:41:14 -04:00
Timothy Jaeryang Baek
3becec6ccf refac 2026-07-31 17:35:34 -04:00
Timothy Jaeryang Baek
5b333d75c6 refac 2026-07-31 17:34:39 -04:00
Classic298
ec0e60033b
perf: use the orjson codec for the permission deep copy (#27807)
`get_permissions` deep-copies the default permission tree with a `json.loads(json.dumps(...))` round trip before merging group permissions into it. It runs on signin, signup, the permissions endpoint, OAuth, and the chat-completion middleware.

It now goes through `JSONCodec`, which selects orjson when `ENABLE_ORJSON` is set. The intermediate string never leaves the expression, so neither the escaping nor the separator differences between the two backends are observable; only the resulting object is used.

`default_permissions` always originates from `Config.get('user.permissions')`, a SQLAlchemy `JSON` column, so the tree is JSON-native by construction and the round trip is exact.

Note for anyone tempted to simplify this to `copy.deepcopy`: measured on the real `DEFAULT_USER_PERMISSIONS` shape over 200k iterations, `deepcopy` takes 3.51s against 1.45s for the stdlib round trip and 0.40s for orjson. The round trip is the fast option, not a workaround.

With `ENABLE_ORJSON` unset, which is the default, `JSONCodec` is stdlib `json` and this call site behaves exactly as before.
2026-07-31 17:32:14 -04:00
Classic298
e2221fb662
perf: use the orjson codec to parse Jupyter kernel messages (#27812)
The code interpreter parses every message from the Jupyter kernel websocket with stdlib `json`, in a loop that runs for the duration of an execution. Messages carrying large stdout or a base64 image payload are the expensive ones.

It now goes through `JSONCodec`, which selects orjson when `ENABLE_ORJSON` is set. Every consumer of the parsed message reads strings only: `content.text`, `content.data['text/plain']` and `['image/png']`, `content.traceback`, and `content.execution_state`. Jupyter renders large integers into `text/plain` as strings rather than JSON numbers, so no numeric round trip is involved.

The one-shot `execute_request` message this module sends keeps stdlib `json`; it is a small fixed-shape dict sent once per execution.

With `ENABLE_ORJSON` unset, which is the default, `JSONCodec` is stdlib `json` and this call site behaves exactly as before.
2026-07-31 17:32:05 -04:00
Classic298
d03e9af0b7
perf: use the orjson codec to parse Oracle vector metadata (#27813)
`_json_to_metadata` parses the metadata of every result row returned by search and get. It now goes through `JSONCodec`, which selects orjson when `ENABLE_ORJSON` is set.

The text it parses is produced by Oracle's own `JSON_SERIALIZE`, and the column is a native `JSON` type, so the database normalises whatever was written and the reader never depends on the writer's escaping.

The matching `_metadata_to_json` write deliberately keeps stdlib `json`: it passes `default=self._decimal_handler`, orjson accepts none of stdlib's keyword arguments, and dropping the handler would turn a currently successful insert of a `Decimal` into a hard failure. The read side has no such constraint.

With `ENABLE_ORJSON` unset, which is the default, `JSONCodec` is stdlib `json` and this call site behaves exactly as before.
2026-07-31 17:31:14 -04:00
Timothy Jaeryang Baek
d721b0d196 refac 2026-07-31 17:30:47 -04:00
Classic298
ace84b4ae9
perf: use the orjson codec for outbound Ollama request bodies (#27811)
`routers/ollama.py` serializes the outbound body with stdlib `json` on six inference paths: `/api/chat`, the OpenAI-compatible completions and chat completions proxies, embeddings, the Anthropic messages proxy, and responses. All six carry a full conversation or an embedding batch.

They now go through `JSONCodec`, which selects orjson when `ENABLE_ORJSON` is set. Every one is passed to `send_request`, which hands it to aiohttp as `data=`; aiohttp encodes `str` as UTF-8 and derives `Content-Length` from the encoded bytes. None is hashed, cached, length-measured or persisted.

Admin model management keeps stdlib: `/api/unload`, `/api/pull`, `/api/delete` and `/api/show` serialize fixed one- or two-key dicts, as do the blob download and upload progress events and the error frames. Codec dispatch on those costs about what it saves.

With `ENABLE_ORJSON` unset, which is the default, `JSONCodec` is stdlib `json` and this call site behaves exactly as before.
2026-07-31 17:26:38 -04:00
Classic298
006a63e641
perf: use the orjson codec for the Anthropic passthrough request body (#27810)
`passthrough_anthropic_messages` in `main.py` serializes the full request payload with stdlib `json` before sending it upstream. It is the largest single serialization on that path, since the body carries the whole conversation.

It now goes through `JSONCodec`, which selects orjson when `ENABLE_ORJSON` is set. The result is passed to aiohttp as `data=`, which encodes `str` as UTF-8 and sets `Content-Length` from the encoded bytes. The payload originates from a parsed request dict, so it holds only JSON-native types, and the serialized string is never hashed, compared or persisted.

The remaining stdlib `json` calls in this module are left alone: two are a debug log line and a fixed Ollama unload payload, and one parses an upstream error body.

With `ENABLE_ORJSON` unset, which is the default, `JSONCodec` is stdlib `json` and this call site behaves exactly as before.
2026-07-31 17:26:07 -04:00
Classic298
243a39dc9d
perf: read the model pool with one HGETALL instead of one HGET per model (#27821)
`request.app.state.MODELS` is a `RedisDict` when Redis is configured. Unpacking it with `{**pool}` makes Python call `keys()` and then `__getitem__` once per key, which is one HKEYS plus one HGET per model, issued sequentially through a synchronous client. At 200 models that is 201 blocking Redis round trips per call.

`RedisDict.items()` is a single HGETALL, so `dict(pool.items())` fetches the same data in one round trip. `utils/chat.py:184` already does exactly this and carries a comment explaining why; these ten call sites were missed.

They are on the direct-connection branch of the task endpoints (title, tags, follow-up, autocomplete, query generation and the rest), of `chat_completed`, and of context compaction, so they run for background tasks fired on ordinary chat turns.

Behaviour is unchanged. The merged mapping is identical, the explicitly added direct model still overrides any pool entry with the same id, and when Redis is not configured the pool is a plain dict where `dict(d.items())` and `{**d}` are equivalent.

It also closes a race. `RedisDict.set` writes with HSET and then HDELs the stale keys, so a key returned by HKEYS could be deleted before its HGET arrived, raising `KeyError` out of the dict literal and failing the request mid model refresh. The old path could likewise observe a mix of pre- and post-refresh entries. HGETALL is atomic, so the caller now always sees one coherent snapshot.
2026-07-31 17:25:53 -04:00
Classic298
6be11d4fc9
chore: remove dead json imports (#27815)
Fourteen modules import `json` without using it. Ruff flags every one with F401, and a word-boundary search for `json` in each file matches only the import line itself, including inside strings, comments and annotations.

Two exclusions, both deliberate. Migration files are left alone: the import is equally dead there, but those files are frozen history and not worth the churn. `models/chats.py` has the same dead import and is handled in its own change, so it is skipped here to avoid two changes touching the same line.

No behaviour change.
2026-07-31 17:25:40 -04:00
Classic298
1d6735ff0b
fix: escape line separators in orjson output (#27819)
orjson emits U+2028, U+2029 and U+0085 raw, where stdlib `json.dumps` escapes them under its default `ensure_ascii=True`. Python treats all three as line boundaries, so with `ENABLE_ORJSON` set, one of them inside model output splits a `data: {...}` SSE frame in half. Both halves then fail to parse and the delta is dropped with no error.

`utils/middleware.py` reassembles frames with `splitlines()`, so an affected response silently loses content on the direct API path. External clients are exposed as well: httpx's `LineDecoder` reimplements the same line-boundary semantics, so any SDK reading the OpenAI-compatible stream through `aiter_lines` breaks on a raw separator.

The three characters are escaped on the way out of `ORJSONCodec.dumps`. That restores parity with stdlib and fixes every reader at once, rather than patching one consumer and leaving external clients broken. They are the complete set: of the ten code points `splitlines()` treats as boundaries, the other seven are below U+0020, where JSON already forces an escape.

The membership guard is load bearing. Calling `translate` unconditionally costs roughly 1.5 us on a typical SSE chunk against 0.115 us for the serialization it wraps, so it would spend more than orjson saves. The three scans cost about 0.04 us.

Payloads containing none of the three are returned unchanged, byte for byte. With `ENABLE_ORJSON` unset, which is the default, none of this code runs.

U+2028 and U+2029 are common in text extracted from PDFs and word processor documents, so the realistic trigger is a model quoting an uploaded file back to the user.
2026-07-31 17:25:30 -04:00
joaoback
8d333335b9
i18n: add pt-BR translations for newly added UI items and consistency pass (#27814)
New **pt-BR** translations for items introduced in the latest releases, plus a consistency/quality pass across existing strings (grammar, tone, capitalization, pluralization). Placeholders and hotkeys preserved. No logic changes.
2026-07-31 17:25:10 -04:00
Classic298
f4c6a76651
perf: use the orjson codec for Valkey vector metadata (#27805)
The Valkey backend serializes chunk metadata on every insert and parses it back on every result row in `get` and `query`. Both directions now go through `JSONCodec`, which selects orjson when `ENABLE_ORJSON` is set.

The stored `metadata_json` field is never matched against as text. `_build_filter_expression` only emits TAG predicates, and the TAG fields are `id`, `hash`, `file_id`, `source` and `knowledge_base_id`; `metadata_json` appears only as a return field that is immediately re-parsed. So rows written with escaped non-ASCII and rows written raw are indistinguishable to every reader, and no migration is needed.

`process_metadata` already stringifies datetimes and strips null bytes and lone surrogates before the write, so the two backends cannot disagree about what is serializable here.

Both read `except` clauses widen from `(json.JSONDecodeError, TypeError)` to `(ValueError, TypeError)`. The codec falls back to engineio's codec, which installs `parse_int=_safe_int` and raises a bare `ValueError` for integer literals longer than 100 characters; the narrower clause would have let that escape and abort a search instead of yielding empty metadata. `json.JSONDecodeError` is a `ValueError` subclass, so this is a strict superset. That removes the module's last use of stdlib `json`, so the import goes with it.

With `ENABLE_ORJSON` unset, which is the default, `JSONCodec` is stdlib `json` and this call site behaves exactly as before.
2026-07-31 17:24:57 -04:00
Classic298
466e05801b
perf: stop query_collection blocking the event loop (#27824)
RAG vector search runs in a thread pool, but then calls `future.result()` on the event loop thread, so the whole worker freezes until every collection answers. Every other user's token stream stops for that long. It's the default retrieval path.

Now `asyncio.gather` over `asyncio.to_thread`, matching what `routers/retrieval.py:2779` already does for the same call.

Measured with 3 queries across 4 collections, 60 ms search, and a second request wanting a turn every 5 ms:

| | before | after |
|---|---|---|
| RAG call | 62.0 ms | 61.2 ms |
| other request's turns | 0 | 7 |
| its worst stall | 62.5 ms | 16.0 ms |

Same results, same order, same `(result, error)` contract. Cancellation now lands mid-search instead of after every thread finishes. Threads move from an unbounded per-call pool to the loop's bounded shared one.
2026-07-31 17:24:31 -04:00
Timothy Jaeryang Baek
810378c0b8 refac
Some checks failed
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
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: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/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_CUDA=true free_disk:true name:cuda suffix:-cuda]) (push) Has been cancelled
Frontend Build / Format & Build (push) Has been cancelled
Frontend Build / Unit Tests (push) Has been cancelled
Python CI / Ruff Format (3.11) (push) Has been cancelled
Python CI / Ruff Format (3.12) (push) Has been cancelled
Create and publish Docker images with specific build args / merge (map[name:cuda suffix:-cuda]) (push) Has been cancelled
Create and publish Docker images with specific build args / merge (map[name:cuda126 suffix:-cuda126]) (push) Has been cancelled
Create and publish Docker images with specific build args / merge (map[name:main suffix:]) (push) Has been cancelled
Create and publish Docker images with specific build args / merge (map[name:ollama suffix:-ollama]) (push) Has been cancelled
Create and publish Docker images with specific build args / merge (map[name:slim suffix:-slim]) (push) Has been cancelled
Create and publish Docker images with specific build args / notify-helm-charts (push) Has been cancelled
Create and publish Docker images with specific build args / copy-to-dockerhub (, main) (push) Has been cancelled
Create and publish Docker images with specific build args / copy-to-dockerhub (-cuda, cuda) (push) Has been cancelled
Create and publish Docker images with specific build args / copy-to-dockerhub (-cuda126, cuda126) (push) Has been cancelled
Create and publish Docker images with specific build args / copy-to-dockerhub (-ollama, ollama) (push) Has been cancelled
Create and publish Docker images with specific build args / copy-to-dockerhub (-slim, slim) (push) Has been cancelled
2026-07-27 19:39:36 -04:00
Timothy Jaeryang Baek
b6b16d5871 refac 2026-07-27 19:24:03 -04:00
Tim Baek
01f4282f1f
Merge pull request #27590 from open-webui/dev
Some checks failed
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
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:USE_CUDA=true USE_CUDA_VER=cu126 free_disk:true name:cuda126 suffix:-cuda126]) (push) Has been cancelled
Frontend Build / Unit Tests (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 free_disk:true name:cuda suffix:-cuda]) (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
Frontend Build / Format & Build (push) Has been cancelled
Release to PyPI / release (push) Has been cancelled
Release / publish (push) Has been cancelled
Python CI / Ruff Format (3.11) (push) Has been cancelled
Python CI / Ruff Format (3.12) (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 / merge (map[name:cuda suffix:-cuda]) (push) Has been cancelled
Create and publish Docker images with specific build args / merge (map[name:cuda126 suffix:-cuda126]) (push) Has been cancelled
Create and publish Docker images with specific build args / merge (map[name:main suffix:]) (push) Has been cancelled
Create and publish Docker images with specific build args / merge (map[name:ollama suffix:-ollama]) (push) Has been cancelled
Create and publish Docker images with specific build args / merge (map[name:slim suffix:-slim]) (push) Has been cancelled
Create and publish Docker images with specific build args / notify-helm-charts (push) Has been cancelled
Create and publish Docker images with specific build args / copy-to-dockerhub (, main) (push) Has been cancelled
Create and publish Docker images with specific build args / copy-to-dockerhub (-cuda, cuda) (push) Has been cancelled
Create and publish Docker images with specific build args / copy-to-dockerhub (-cuda126, cuda126) (push) Has been cancelled
Create and publish Docker images with specific build args / copy-to-dockerhub (-ollama, ollama) (push) Has been cancelled
Create and publish Docker images with specific build args / copy-to-dockerhub (-slim, slim) (push) Has been cancelled
refac
2026-07-27 05:47:11 -05:00
Timothy Jaeryang Baek
483adf7040 refac 2026-07-27 06:46:42 -04:00
Tim Baek
f9590b8017
Merge pull request #27362 from open-webui/dev
0.11.0
2026-07-27 04:30:03 -05:00
Timothy Jaeryang Baek
faeba7c17a refac 2026-07-27 05:28:33 -04:00
Timothy Jaeryang Baek
2beddbe49f refac 2026-07-27 04:58:46 -04:00
Timothy Jaeryang Baek
abc8889681 refac 2026-07-27 04:51:08 -04:00
Timothy Jaeryang Baek
58dc25125b refac 2026-07-27 04:50:07 -04:00
Classic298
70c04eb675
changelog: Update CHANGELOG.md (#27060)
* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md
2026-07-27 04:47:26 -04:00
Timothy Jaeryang Baek
965ef909d7 chore: format 2026-07-27 04:46:45 -04:00
Timothy Jaeryang Baek
39206602ac refac 2026-07-27 04:46:12 -04:00
Timothy Jaeryang Baek
50e050e195 refac
Co-Authored-By: djedi-knight <943190+djedi-knight@users.noreply.github.com>
2026-07-27 04:43:51 -04:00
Timothy Jaeryang Baek
c004b4ecb5 chore: format 2026-07-27 04:38:46 -04:00
Timothy Jaeryang Baek
c4332be71e refac 2026-07-27 04:36:56 -04:00
Timothy Jaeryang Baek
b190dcf3ca refac 2026-07-27 04:36:51 -04:00
Timothy Jaeryang Baek
56183fcb17 refac 2026-07-27 04:27:13 -04:00
Timothy Jaeryang Baek
4eab2550a0 refac 2026-07-27 04:17:03 -04:00
Timothy Jaeryang Baek
4493b56e42 refac 2026-07-27 04:17:00 -04:00
Timothy Jaeryang Baek
8ab44ed3b1 refac 2026-07-27 04:11:48 -04:00
Timothy Jaeryang Baek
977c793062 refac 2026-07-27 04:09:02 -04:00
Timothy Jaeryang Baek
c182a95ffd refac 2026-07-27 04:05:51 -04:00
Timothy Jaeryang Baek
11e61b69eb refac
Co-Authored-By: Classic298 <27028174+Classic298@users.noreply.github.com>
2026-07-27 04:00:46 -04:00
Timothy Jaeryang Baek
e8fda1c7a0 refac 2026-07-27 03:59:12 -04:00
Timothy Jaeryang Baek
a9a3e5b95c refac 2026-07-27 03:54:27 -04:00
Timothy Jaeryang Baek
2d928df304 refac 2026-07-27 03:54:05 -04:00
Timothy Jaeryang Baek
b7394c63fa refac 2026-07-27 03:53:54 -04:00
Timothy Jaeryang Baek
c4ae8c8678 refac 2026-07-27 03:51:32 -04:00
Timothy Jaeryang Baek
48ee357156 refac 2026-07-27 03:50:18 -04:00
Timothy Jaeryang Baek
7e31f64bc8 refac 2026-07-27 03:50:14 -04:00
Classic298
72fdf238a8
perf: optional orjson JSON codec behind ENABLE_ORJSON (#27583)
Swap the JSON encoder/decoder used across the backend from stdlib json to
orjson when ENABLE_ORJSON is set — HTTP request bodies, JSONResponse
bodies, upstream provider responses, SSE chunks, and socket.io/Redis
payloads.

The flag defaults to off, in which case the app uses stdlib json and
engineio's codec verbatim, so default behaviour is unchanged.

- json_codec exports JSONCodec (stdlib json or the orjson codec) and
  SOCKETIO_JSON (engineio's codec or the orjson codec); call sites import
  JSONCodec and stay implementation-agnostic
- apply_orjson_http_json() is a no-op when the flag is off, leaving
  starlette's Request.json / JSONResponse.render untouched
- the orjson codec falls back to the stdlib for inputs orjson rejects
  (non-str dict keys, ints beyond 64 bits, NaN literals)
- orjson is imported only when the flag is on
- FastAPI(default_response_class=...) is deliberately not used: an
  explicit default disables the Pydantic direct-to-bytes fast path for
  response_model routes
2026-07-27 03:45:37 -04:00
Timothy Jaeryang Baek
602004dd5f refac 2026-07-27 03:44:13 -04:00
Timothy Jaeryang Baek
7537989235 refac 2026-07-27 03:41:08 -04:00
G30
867006acce
fix: keep admin access to connections without access grants when admin bypass is disabled (#27581) 2026-07-27 03:39:40 -04:00
Timothy Jaeryang Baek
be1b811ce5 refac 2026-07-27 03:34:26 -04:00
Timothy Jaeryang Baek
db2d24896b refac 2026-07-27 03:32:21 -04:00
Classic298
3ab2026262
fix: bound knowledge-search matching so one pattern cannot stall the worker (#27471)
build_matcher compiled a caller-supplied pattern with Python's backtracking re and ran it over every line of every reachable file, with no timeout, no thread offload and no length caps. is_regex_pattern promotes any pattern containing a metacharacter, and a bare pipe counts, so no explicit regex flag is needed to reach the compiler. The search loop is synchronous inside an async handler, and UVICORN_WORKERS defaults to 1, so the cost lands on every other user of the instance. MAX_GREP_RESULTS bounds how many matches are reported, not how much work is done.

Backtracking cost is exponential in the length of the text being matched, so capping the pattern or the line does not bound it: the subject in the measurements below is 30 characters. `(x|x)*y` against a line of 30 x took 80 seconds, `(a+)+$` against 32 a took 169 seconds, and the same subject with a literal pattern took 0.6 microseconds.

Matching now runs on the regex module, which accepts a per-search timeout that re has no equivalent for. The timeout is the actual bound: regex resolves many classic catastrophic patterns instantly, but not all of them, and `(a|aa)+$` and `(?:a|a)*$` still need it. The budget covers a whole tool call rather than a single search, because a pipeline builds one matcher per segment and a per-search budget would multiply by segment count, and because a per-line timeout would allow timeout multiplied by line count. It is carried in a context variable so one command shares it without threading a parameter through every handler, and it is charged only for time spent inside search(), so database round-trips and other coroutines cannot consume it. Exhausting it raises, and both entry points already render that as an error for the model to read.

Note for anyone tracking search behaviour: re and the regex module define \w, \W and \b differently on non-ASCII text. re follows str.isalnum(), the regex module follows UTS#18, so \w no longer matches superscripts and fractions such as the ones in Nd-adjacent categories, and now does match combining marks. POSIX classes like [[:alpha:]] are interpreted rather than read as a literal set, and \p{...} compiles instead of erroring. Results on ASCII content are unchanged.

regex was already installed as a transitive dependency of nltk, tiktoken and transformers. It is now declared directly, pinned in pyproject.toml and requirements.txt to the version the lockfile already resolves.
2026-07-27 03:22:26 -04:00
Classic298
147c3b6ac8
fix: mark the open chat with aria-current in the sidebar (WCAG 1.4.1, 4.1.2) (#27502)
On latest `dev`, the chat that is currently open is indicated **only** by a background tint: `bg-black/[0.035]` in light mode and `dark:bg-white/[0.045]` in dark.

Against the page background that is **1.07:1** in light and **1.05:1** in dark. It is close to imperceptible for sighted users, and it carries no programmatic state at all, so assistive technology has no way to tell which entry in the list is the one being viewed.

Breaks WCAG 1.4.1 Use of Color (Level A), since the state is conveyed by colour alone, and 4.1.2 Name, Role, Value (Level A), since the state is not exposed.

Fix: set `aria-current="page"` on the chat link when it is the open chat, using the same `id === $chatId` condition that already drives the visual highlight, so the two cannot drift apart. `'page'` is the correct token because the trigger is a real navigation to `/c/{id}`.

This matches the existing pattern in `routes/(app)/workspace/+layout.svelte` and `chat/Placeholder/ChatList.svelte`, which already set `aria-current` for their active entries.

Verified that the bits-ui `LinkPreview.Trigger` forwards unknown attributes to the rendered anchor and does not set `aria-current` itself, so the attribute reaches the DOM.

This does not change the visual contrast of the highlight, which is worth addressing separately.

Severity: Serious. In a long chat list there is no reliable way to tell which chat is open.

### 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-07-27 03:20:32 -04:00
Timothy Jaeryang Baek
2f2bf38e34 refac 2026-07-27 03:19:10 -04:00
Timothy Jaeryang Baek
f59d86a10c refac 2026-07-27 03:18:12 -04:00
Classic298
71511ccd5a
fix: make sidebar folder rows keyboard operable (WCAG 2.1.1, 4.1.2) (#27509)
On latest `dev`, the sidebar folder row is a bare `<div>` carrying `on:click` (navigate into the folder) and `on:dblclick` (rename). It has **no `role`, no `tabindex` and no key handler**, so opening a folder is impossible from the keyboard.

The nested chevron `<button>` is focusable, but it only expands the folder in place, it does not navigate to it, so there is no keyboard route to the folder page at all.

Breaks WCAG 2.1.1 Keyboard (Level A) and 4.1.2 Name, Role, Value (Level A). The Svelte compiler already flags this file with `a11y_click_events_have_key_events`; after this change the component compiles with zero a11y warnings.

Fix: apply the row pattern already used elsewhere in this codebase (`workspace/Prompts.svelte`, `workspace/Knowledge.svelte`, `admin/Functions.svelte`), namely `role="button"`, `tabindex="0"` and a keydown handler for Enter and Space, with the same `e.currentTarget !== e.target` guard and the same `shouldIgnoreRowClick` helper those files use.

That guard matters more here than in the files it was copied from: the rename `<input>` is rendered **inside** this row, so without it typing a space in the rename field would be swallowed and navigate away, and Enter would both save the rename and navigate.

The navigation body is extracted to `openFolderHandler` because it now has two callers. The keyboard path calls it directly rather than through the 100ms `clickTimer`, which exists only to disambiguate single from double click and has no keyboard equivalent.

A dead `(e) => e.stopPropagation();` expression statement in the click handler is removed. It allocated an arrow function and discarded it without ever calling it.

The `…` folder menu is still `invisible group-hover:visible` and therefore unreachable, so rename, share, delete, export and new subfolder remain keyboard-inaccessible until that is addressed. That is fixed repo wide in a separate PR that replaces the `invisible group-hover:visible` pattern, so it is deliberately not touched here to avoid conflicting on the same line.

Folder reparenting by drag still has no keyboard alternative, which is a separate WCAG 2.5.7 issue needing a "Move" menu action.

Severity: Critical. Folders cannot be opened without a pointing device.

### 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.

Co-authored-by: Tim Baek <tim@openwebui.com>
2026-07-27 03:16:25 -04:00
Timothy Jaeryang Baek
9707d3a5c2 refac 2026-07-27 03:09:25 -04:00
Classic298
bb928b0dfe
fix: fetch the terminal system prompt per request (#27242)
* fix: fetch terminal system prompt per request with TTL cache

The system prompt was only fetched once in set_terminal_servers (startup
or connection save) with a 3s timeout, using a synthetic 'system' user.
That snapshot silently stays empty when the fetch races a cold-started
orchestrator instance, and goes stale when instances are reprovisioned
with a changed OPEN_TERMINAL_SYSTEM_PROMPT — recovering only after a
restart or a manual connection re-save.

- Fetch /system during get_terminal_tools with the user's own
  credentials via a central TTL-cached method (5 min per server+user;
  failures cached 60s so a dead instance doesn't stall every request),
  falling back to the cached snapshot.
- Raise the fetch timeout from 3s to 30s so cold-provisioned instances
  can answer.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KjnQJNKozp47vTB13pRyYs

* refac: fetch the terminal system prompt per request without a cache

Drop the module-level TTL cache and fetch the system prompt directly in
get_terminal_tools, gathered with the existing uncached per-request cwd
fetch that already follows this pattern. The fetch uses the user's own
credentials and falls back to the set_terminal_servers snapshot, so a
cold or unreachable instance degrades to the previous behaviour instead
of needing an error cache. Also restore the 3s timeout: on the request
path a 30s wait would stall chat completions, and a cold instance is
covered by the snapshot fallback until it warms up.

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-07-27 03:07:47 -04:00
Timothy Jaeryang Baek
69e449e318 refac 2026-07-27 03:05:26 -04:00
Classic298
5278eb906e
fix: block external resource loading in Vega chart rendering to prevent client-side SSRF (#26806)
* fix: block external resource loading in Vega chart rendering to prevent client-side SSRF

renderVegaVisualization renders vega/vega-lite chart specs that appear in untrusted chat content (shared chats, channel messages, assistant/RAG/tool output) by constructing a Vega View with no restricted loader, so a crafted spec could make a viewer's browser issue arbitrary outbound requests. There are two paths: data.url (and topojson/geo data) is fetched via loader.load at view construction, and image-mark urls are resolved via loader.sanitize and emitted as <image href> into the output SVG, fetched by the browser when the SVG is displayed. Both are client-side SSRF, and against same-origin or CORS-permissive targets allow reading the response back into the page. Pass a loader that rejects external resource loads on both paths, load throws and sanitize rejects http(s)/protocol-relative URIs, so rendered charts can only use inline data. Inline data.values charts are unaffected.

Co-authored-by: Zureno <Zureno@users.noreply.github.com>

* fix: resolve Vega image urls with the URL parser before blocking external loads

The previous scheme regex could be bypassed with encodings the browser URL parser
normalizes away, such as a leading tab or newline before the scheme and backslash
variants of protocol-relative urls like /\evil.com, which would still be emitted
into the rendered SVG and fetched externally on display. Resolve the uri against
document.baseURI with the browser's own URL parser and only allow data: uris and
same-origin results, so the check cannot diverge from what the browser would
actually fetch. Also shortens the explanatory comments.

---------

Co-authored-by: Zureno <Zureno@users.noreply.github.com>
2026-07-27 03:03:30 -04:00
Timothy Jaeryang Baek
4c2d864b3f refac 2026-07-27 03:01:19 -04:00
Timothy Jaeryang Baek
44f4f9dce4 refac 2026-07-27 03:00:37 -04:00
Timothy Jaeryang Baek
15688686af refac 2026-07-27 03:00:04 -04:00
Timothy Jaeryang Baek
93a34bb25b refac 2026-07-27 02:59:58 -04:00
Timothy Jaeryang Baek
70549c5c8a refac 2026-07-27 02:59:07 -04:00
Timothy Jaeryang Baek
ba556bd8f0 refac 2026-07-27 02:49:08 -04:00
Timothy Jaeryang Baek
7d77efe0f1 refac 2026-07-27 02:47:09 -04:00
Timothy Jaeryang Baek
8e74cac8de refac 2026-07-27 02:44:40 -04:00
Timothy Jaeryang Baek
f1409266fe refac 2026-07-27 02:44:21 -04:00
Timothy Jaeryang Baek
0576e8eeb5 refac 2026-07-27 02:42:36 -04:00
Timothy Jaeryang Baek
12974c9e4e refac 2026-07-27 02:41:05 -04:00
Timothy Jaeryang Baek
3fe03583a3 refac 2026-07-27 02:39:11 -04:00
Timothy Jaeryang Baek
d727ee4d1f refac 2026-07-27 02:38:33 -04:00
Timothy Jaeryang Baek
9acbe3aa0f refac 2026-07-27 02:36:15 -04:00
Timothy Jaeryang Baek
76aae64c7b refac 2026-07-27 02:34:04 -04:00
Timothy Jaeryang Baek
e28b391e51 refac 2026-07-27 02:29:56 -04:00
Timothy Jaeryang Baek
98656b7c5e refac 2026-07-27 02:28:21 -04:00
Timothy Jaeryang Baek
f578d8d67e refac 2026-07-27 02:27:27 -04:00
Timothy Jaeryang Baek
6aebfd88e9 refac 2026-07-27 02:24:41 -04:00
Timothy Jaeryang Baek
498cdab9a5 refac 2026-07-27 02:23:33 -04:00
Classic298
2e4c232807
perf: update chat tags via the meta column instead of round-tripping the blob (#27382)
update_chat_tags_by_id runs at the end of every completion when tag generation is enabled (the default). It loaded the full chat row including the multi-megabyte blob, mutated only meta.tags, committed, then refreshed the row, which re-fetched and re-parsed the entire blob a second time, and finally validated the whole thing into a ChatModel that its only caller (the auto-tagging handler) discards. add_chat_tag_by_id_and_user_id_and_tag_name had the same shape for a one-tag append, and orphan cleanup issued one COUNT query per removed tag.

Both tag writers now select only the meta column and issue a column-level UPDATE, never touching the blob; the single-tag path also skips the write entirely when the tag is already present. Orphan detection batches all per-tag counts into one round trip using one scalar subquery per tag with the exact same dialect-specific EXISTS filters as before; the existing single-tag count delegates to the batch helper so there is one implementation.

Benchmark (real SQLite DB, 200-message chat, ~600 KB blob):

| metric | before | after |
| --- | --- | --- |
| auto-tag update, 3 tags replaced | 12.85 ms | 7.36 ms |

The absolute saving grows with chat size since the blob no longer gets fetched, parsed, re-fetched and validated at all.

Functionally verified against a fresh database: tag replacement normalizes and filters the none placeholder, creates missing tag rows and leaves the blob untouched; orphaned tags are deleted while tags still referenced by other chats survive; single-tag add is idempotent; batch counts agree with the single count including unknown tags; unknown chat ids return None.
2026-07-27 02:21:59 -04:00
Classic298
707efeaed7
fix: scope knowledge sync cleanup deletions to the target knowledge base (#26722)
POST /knowledge/{id}/sync/cleanup verified write access to the knowledge base in the URL but then acted on the caller-supplied file_ids and dir_ids without checking they belong to that knowledge base. A user with write access to any knowledge base could pass another knowledge base's directory id to delete its directory subtree and knowledge_file associations, or another file's id to drop its file-{file_id} vector collection. Fetch each directory and skip any whose knowledge_id does not match the URL id (matching the explicit directory-delete endpoint), and gate the per-file vector cleanup on Knowledges.has_file(id, file_id) so a foreign file id cannot trigger collection deletion. Legitimate same-knowledge-base cleanup is unchanged.

Co-authored-by: whyiug <whyiug@users.noreply.github.com>
2026-07-27 02:18:38 -04:00
Timothy Jaeryang Baek
8710c448a9 refac 2026-07-27 02:17:16 -04:00
Timothy Jaeryang Baek
312d8a8e7f refac
Co-Authored-By: Jacob Leksan <63938553+jmleksan@users.noreply.github.com>
2026-07-27 02:17:11 -04:00
Naytip
3fff80ad2f
fix: resolve MCP OAuth authorize_url discovery and fail clearly (#26647) (#26654) 2026-07-27 02:15:28 -04:00
Classic298
d3cfcd801e
fix: preserve system prompt across tool calls when memories are enabled (#26857)
* fix: preserve system prompt across tool calls when memories are enabled

The native tool-call loop runs generate_chat_completion with
bypass_system_prompt=True, so the provider layer does not re-apply the
model's default system prompt on tool-call iterations. It relies instead
on metadata['system_prompt'], captured in process_chat_payload, to carry
the full system prompt forward and restore it after RAG injection.

That capture read the model default system prompt from
form_data['params']['system'], but apply_params_to_form_data had already
popped 'params' from form_data, so model_system_prompt was always empty.
metadata['system_prompt'] therefore only captured whatever was already
materialized in the messages. With memories enabled, that is the injected
<memory_context> system message, so tool-call requests were restored with
memory-only system content and the model's system prompt was dropped.
Without memories there was no system message to capture at all.

Capture the model default system prompt from form_data['params'] before
apply_params_to_form_data pops it, and use that value when building
metadata['system_prompt'].

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014Z4L51mvxDJCx1EDxP2vFN

* refactor: condense system prompt capture comment to a single line

Replace the four-line explanation above the model_system_prompt capture with a one-line note. The variable name and the surrounding code already convey what happens; the comment only needs to state why the capture sits before apply_params_to_form_data.

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-07-27 02:13:28 -04:00
Timothy Jaeryang Baek
f91ac068d0 refac 2026-07-27 02:12:40 -04:00
Timothy Jaeryang Baek
6c59ef313f refac 2026-07-27 02:11:10 -04:00
Classic298
6d4c02a89e
refac: owner-bind ephemeral web-search RAG collections (#26706)
The web-search-* namespace was the one collection namespace filter_accessible_collections admitted unconditionally for any non-admin user, on both read and write, unlike file-*, user-memory-* and knowledge bases which are owner-scoped. process_web_search now mints these ephemeral per-query collections as web-search-{user.id}-<hash>, and the access helper only admits web-search-{requester.id}-* names, so a web-search collection is readable and writable only by the user who created it (admins keep their bypass). The collections hold transient public web-search results and their names are non-enumerable query hashes, so there was no demonstrated cross-user access path; this removes the namespace exception so the per-user scoping the other namespaces enforce also covers web-search.

Co-authored-by: rexpository <rexpository@users.noreply.github.com>
2026-07-27 02:08:04 -04:00
Classic298
3a9b9a1a74
fix: resolve terminal system_oauth token server-side instead of trusting a client header (#26719)
The terminal proxy's system_oauth auth type read the OAuth access token from the client-supplied x-oauth-access-token request header and forwarded it verbatim as a Bearer token to the upstream terminal server, so an authenticated caller could substitute an arbitrary token for the one bound to their own session. Resolve the token server-side from the caller's OAuth session via oauth_manager.get_oauth_token(user.id, oauth_session_id), matching the openai.py proxy, so the forwarded token is always the one Open WebUI issued for the authenticated user and the client header is ignored.

Co-authored-by: brodmart <brodmart@users.noreply.github.com>
2026-07-27 02:07:34 -04:00
Classic298
6c7478c1c9
fix: surface web search embedding failures in the chat UI instead of silently returning an empty collection (#26883)
Previously, when web search retrieved pages successfully but saving them to the vector DB failed (for example an unreachable or misconfigured embedding endpoint), process_web_search swallowed the exception at debug log level and still returned status: True with the collection name. The chat then showed "Searched N sites" followed by "No sources found" at retrieval time, hiding the actual misconfiguration from the user and making the failure look like a search bug.

process_web_search now logs the failure at exception level and raises an HTTPException with an actionable message pointing at the embedding configuration in Admin Settings > Documents. chat_web_search_handler surfaces the detail of any HTTPException raised during the search in the emitted error status, so the real cause (embedding misconfiguration, search engine errors, no results) is shown in the chat UI instead of the generic "An error occurred while searching the web". Non-HTTP exceptions keep the generic message, so raw internal error strings are not exposed.

Ref #26750, #25038
2026-07-27 02:07:14 -04:00
Timothy Jaeryang Baek
3492021361 refac 2026-07-27 02:04:44 -04:00
Classic298
e17db990af
fix: parse .msg uploads via unstructured instead of extract_msg (#26704)
The .msg branch routed to langchain's OutlookMessageLoader, which requires the extract_msg package. extract_msg pins beautifulsoup4<4.14, but we pin unstructured==0.22.31 (needs beautifulsoup4>=4.14.3) and beautifulsoup4==4.14.3, so extract_msg can never be installed alongside the current dependency set. As a result the .msg path could not function on any supported install: uploads failed at runtime with an ImportError, and adding the missing package broke the build with an unsatisfiable resolver error.

Switch to UnstructuredEmailLoader, which parses .msg through unstructured's partition_msg (backed by python-oxmsg). Both are already shipped, so .msg uploads work with no new dependency and no version conflict. Attachment partitioning is disabled to preserve the previous body-only extraction behaviour.

Fixes #26690
2026-07-27 02:01:04 -04:00
Timothy Jaeryang Baek
304cbe4569 refac 2026-07-27 02:00:04 -04:00
Timothy Jaeryang Baek
c4f5ac65ee refac 2026-07-27 01:59:17 -04:00
Sebastian
8f9e9398f8
fix(docker): make open_webui/static writable by an arbitrary UID (OpenShift) (#26664)
The backend rewrites its bundled static assets under open_webui/static on
startup. Under OpenShift's restricted SCC the container runs as a random UID
(member of GID 0), which cannot write to the root-owned static dir, so boot
logs fill with '[Errno 13] Permission denied: .../static/*'.

Give GID 0 the owner's permissions on that directory (chgrp 0 + chmod g=u),
the standard Red Hat arbitrary-UID idiom. Applied unconditionally since the
app writes there on every start; complements the opt-in USE_PERMISSION_HARDENING.
2026-07-27 01:55:53 -04:00
Classic298
897d69a35c
fix: enforce feature permissions on the legacy chat-features block (image_generation, web_search) (#26703)
The legacy features block in process_chat_payload honoured client-supplied features.image_generation and features.web_search flags and dispatched to the image generation/edit provider and the web-search provider without re-checking the per-user permission that the direct /images routes and the native function-calling path enforce. A user denied features.image_generation or features.web_search could still trigger billable server-side image generation or web search via POST /api/chat/completions with params.function_calling set to legacy. Gate both branches on admin-or-has_permission before invoking chat_image_generation_handler / chat_web_search_handler, matching the existing code_interpreter gate, so a forged flag from an unpermitted user is ignored. Normal completions and permitted users are unaffected.
2026-07-27 01:54:00 -04:00
Classic298
f65f893ff1
perf: stop refetching the model row and user groups in the completion access check (#27378)
The chat completion entry point fetched the model row and then check_model_access immediately fetched the exact same row again. Inside the check, the direct grant lookup and every hop of the base-model chain each refetched the caller's group memberships, because neither call passed user_group_ids even though both AccessGrants.has_access and has_base_model_access already accept it.

check_model_access now takes an optional prefetched model_info (used only when its id matches the requested model, so stale callers cannot bypass the lookup) and resolves the caller's group ids once, sharing them across the direct check and the whole base-model chain. The group fetch is skipped entirely for the owner-with-no-base-chain case, which previously needed no groups either.

DB round trips for one completion-entry access check (non-owner model with one base-model hop):

| queries | before | after |
| --- | --- | --- |
| model row SELECTs | 3 | 2 |
| group membership SELECTs | 2 | 1 |

For deeper base-model chains the before column grows by one group SELECT per hop; the after column stays at one.

Functionally verified with stubbed model, group and grant accessors: owner fast path issues no group or grant queries; a non-owner with a base chain resolves groups once and passes the same set to every hop; a prefetched matching model_info skips the duplicate row fetch while a mismatched one is refetched; denial and unknown-model cases still raise; the arena path is unchanged.
2026-07-27 01:52:04 -04:00
Classic298
3fe829acc2
fix: strip model params for read-only callers in the model list endpoint (#27004)
The per-id model endpoint (GET /api/v1/models/model) strips params, the system
prompt and other curated model config, for callers who only have read access.
The list endpoint (GET /api/v1/models/list) did not: it returned each
read-accessible model's full params, so a read-shared model exposed its
params.system to non-owner read-grant holders.

Mirror the per-id behaviour: compute write_access per item and drop params
before serialising when the caller lacks write access (not the owner, not an
admin under BYPASS_ADMIN_ACCESS_CONTROL and holding no write grant). The
model-card list UI does not render params, so this does not change
functionality.

Co-authored-by: bogdancherniy11-sudo <229690748+bogdancherniy11-sudo@users.noreply.github.com>
2026-07-27 01:51:29 -04:00
Classic298
c05de13b4f
fix: do not expose tool source code to read-only users (#27005)
* fix: do not expose tool source code to read-only users

The tool read endpoints build their responses from a content-bearing model via
model_dump() under ConfigDict(extra='allow'). ToolResponse deliberately omits
content (the Python source) and specs, but extra='allow' re-admits both, and the
get_tools defer_content flag was a no-op, so GET /tools/, GET /tools/list and GET
/tools/id/{id} returned a tool's full source to any caller with mere read access,
including any authenticated user for a publicly read-shared tool. Tool source
commonly embeds hard-coded credentials and internal URLs.

Strip content and specs for callers without write access across the three read
endpoints. Tool execution loads source server-side, so tool use is unaffected,
and writers still receive content where they did before. The duplicated
write-access check is extracted into a small helper.

Co-authored-by: bogdancherniy11-sudo <229690748+bogdancherniy11-sudo@users.noreply.github.com>

* fix: limit the tool source strip to the per-id endpoint

Upstream dev has since fixed the defer_content no-op in Tools.get_tools, so the list endpoints (GET /tools/ and GET /tools/list) no longer fetch tool source at all and the stripping added there is redundant. Stripping specs also broke the chat Available Tools modal, which lists a tool's functions from specs for every user who can use the tool.

Reduce the change to the one remaining leak: GET /tools/id/{id} builds its response from a full model_dump() and ConfigDict(extra='allow') re-admits content, so drop content there for callers without write access. Specs stay visible to read users as before and the helper functions are no longer needed.

---------

Co-authored-by: bogdancherniy11-sudo <229690748+bogdancherniy11-sudo@users.noreply.github.com>
2026-07-27 01:51:01 -04:00
catty42
9562f1a67d
fix: honor grep -c and -l flags for piped input in kb_exec (#26721)
Co-authored-by: yuki4266 <258261435+yuki4266@users.noreply.github.com>
Co-authored-by: Tim Baek <tim@openwebui.com>
2026-07-27 01:49:02 -04:00
Classic298
d29685275b
perf: drop the full-payload deepcopy in the OpenAI to Ollama conversion (#27371)
convert_payload_openai_to_ollama deep-copied the entire request payload on every completion routed to an Ollama model, and again on every tool-call iteration. The cost of that copy scales with the number of messages and nested content parts in the history, so long chats pay the most, purely as CPU work before the request even leaves the server.

The function only ever mutates two things: it deletes keys on the top-level dict and on the nested options dict. convert_messages_openai_to_ollama already builds fresh message dicts. Shallow-copying exactly those two levels therefore preserves behavior while removing the whole-tree copy.

Benchmark (per conversion call):

| payload | before | after | speedup |
| --- | --- | --- | --- |
| 200-message text chat (~180 KB) | 0.22 ms | 0.057 ms | 4x |
| 20-message chat + 1 MB base64 image | 0.41 ms | 0.38 ms | 1.1x |

The image row barely moves because deepcopy shares immutable strings; the win comes from container-heavy histories, which are exactly the payloads that grow over a conversation's lifetime.

The output is byte-identical to the previous implementation (verified against it, including dict key order, root parameter hoisting, max_tokens remapping, stop handling and response_format precedence), and the caller's payload is left unmodified exactly as before.
2026-07-27 01:47:23 -04:00
Classic298
915ef7d079
fix: restrict folder deletion to the owner or an admin (#27003)
* fix: restrict folder deletion to the owner or an admin

Deleting a folder cascades into the folder owner's chats, messages and the
entire subfolder subtree; the cascade is bound to the folder's owner, not the
caller. The delete handler only enforced owner/admin for root folders.
Subfolder deletion required merely write access, and a write grant on a shared
root folder is inherited by every descendant subfolder. A write-collaborator
could therefore permanently delete the owner's chats by deleting a subfolder of
a shared folder, data they do not own. With delete_contents=false the same path
force-moved the owner's chats out of the folder instead.

This also contradicted the documented sharing model: only the owner or an admin
may delete a shared folder, and write access covers adding and editing chats and
subfolders, not removing the folder.

Because any folder deletion cascades into the owner's data, restrict it to the
owner or an admin for root and subfolders alike, replacing the root/subfolder
split with a single check. Owners and admins are unaffected, and a
write-collaborator can still create, rename and add to shared folders and delete
subfolders they own.

Co-authored-by: legobattman <302282032+legobattman@users.noreply.github.com>

* style: condense the folder deletion authorization comment

Shorten the multi-line comment above the owner-or-admin check to a single line stating why deletion is restricted. The full rationale lives in the pull request description and does not need to be narrated in the code.

---------

Co-authored-by: legobattman <302282032+legobattman@users.noreply.github.com>
2026-07-27 01:46:58 -04:00
Timothy Jaeryang Baek
305880f2e2 refac 2026-07-27 01:46:10 -04:00
Timothy Jaeryang Baek
7801909d27 a11y
Co-Authored-By: Vince Castillo, PhD <154394560+professorcastillo@users.noreply.github.com>
2026-07-27 01:43:58 -04:00
Classic298
bc600d3f08
fix: escape KaTeX render-error fallback to prevent XSS via {@html} (#26718)
KatexRenderer rendered the raw math source through {@html} whenever renderToString threw. throwOnError only suppresses KaTeX ParseError, so a RangeError (maximum call stack size exceeded, reachable with deeply-nested brace input) escaped into the catch and re-exposed the unescaped source. Because the math tokenizer captures everything between the delimiters verbatim, that source can carry an HTML/JS payload which then executed in the viewer's browser on the application origin, a stored, cross-user XSS reachable through normal chat/channel/shared-chat rendering. Escape the fallback so the source is shown as text and is never injected as HTML. Valid math is unaffected, it still renders through the success path.

Co-authored-by: maxntv <maxntv@users.noreply.github.com>
2026-07-27 01:36:41 -04:00
Timothy Jaeryang Baek
067cf31f40 refac 2026-07-27 01:32:41 -04:00
Timothy Jaeryang Baek
8a90bf6256 chore: format 2026-07-27 01:22:08 -04:00
G30
cce3b68265
fix: enforce a single open user profile preview across ProfilePreview instances (#27578) 2026-07-27 01:21:57 -04:00
Timothy Jaeryang Baek
def26ce266 refac 2026-07-27 01:21:32 -04:00
Timothy Jaeryang Baek
75e54bf46b refac 2026-07-27 01:21:00 -04:00
Timothy Jaeryang Baek
89caa7c849 refac 2026-07-27 01:19:34 -04:00
Vince Castillo, PhD
6379d37863
fix: expose ConfirmDialog with dialog role and label its input (WCAG 4.1.2, 3.3.2) (#26769)
ConfirmDialog trapped focus and closed on Escape but its container was a plain
  div, so screen readers did not announce it as a modal dialog. Its text input
  also had only a placeholder, giving no persistent accessible name. Add
  role=dialog / aria-modal / aria-label / tabindex to the dialog surface and an
  aria-label to the textarea.

  Relates to #2790

Co-authored-by: Tim Baek <tim@openwebui.com>
2026-07-27 01:15:12 -04:00
Timothy Jaeryang Baek
e8f2c123e6 refac 2026-07-27 01:13:09 -04:00
Classic298
7e96c53a20
feat: multiselect valve input type with static or dynamic options (#26884)
Adds a multiselect input type for Valves and UserValves so plugin authors can let users pick multiple values from static or runtime-resolved options instead of maintaining comma-separated text fields with hardcoded allowed-value lists in the description.

ENABLED_ITEMS: list[str] = Field(
    default=["foo"],
    json_schema_extra={"input": {"type": "multiselect", "options": "get_item_options"}},
)

@classmethod
def get_item_options(cls):
    return [{"value": "foo", "label": "Foo"}, {"value": "bar", "label": "Bar"}]

Options accept the same shapes as the existing select input: either a static list (strings or {value, label} dicts) or a classmethod name resolved at request time (including __user__ context for UserValves). No backend changes are needed because resolve_valves_schema_options already resolves options independently of the input type.

The new MultiSelect component follows the existing Select portal dropdown pattern and renders checkbox rows that stay open while toggling, with the selected labels shown in the trigger. Values bind as a real string array end to end: the array-to-comma-string conversions in the chat controls valves panel and the valves modal are skipped for multiselect fields, so the stored valve is a native list[str] validated by Pydantic.

Requested in #26848.
2026-07-27 01:11:38 -04:00
Timothy Jaeryang Baek
051a1f6c41 refac 2026-07-27 01:10:58 -04:00
Timothy Jaeryang Baek
6732852ce6 refac 2026-07-27 01:05:52 -04:00
Timothy Jaeryang Baek
de681aa543 refac 2026-07-27 01:03:10 -04:00
Classic298
b40b6fd698
fix: reject backslash in the terminal proxy path sanitizer (#27198)
_sanitize_proxy_path decodes the path and then relies on posixpath.normpath plus a leading '..' check. posixpath splits on '/' only, so a backslash run is treated as part of a single path component: 'foo/..\..\etc' normalizes to itself, does not start with '..' and is forwarded unchanged, reaching the upstream as '/foo/..%5C..%5Cetc'. An upstream that treats the backslash as a separator would resolve those '..' sequences.

Reject any path containing a backslash after decoding, matching the existing fail-closed behaviour for paths that are still encoded past the decode cap. A backslash is not meaningful in the upstream API paths this route proxies, so legitimate requests are unaffected.

Co-authored-by: babakizo420 <babakizo420@users.noreply.github.com>
2026-07-27 01:01:10 -04:00
Classic298
e30ed01b05
perf: stream pure passthrough proxy responses by network chunk instead of by line (#27384)
stream_wrapper without a content handler iterates aiohttp's response.content, which reads line by line: every line costs a buffer scan, a slice, a bytes concat, a generator resume and its own ASGI response message. A typical SSE event is two lines (the data line and the blank separator), so every upstream token event became two yields and two transport writes even on routes where the body is never inspected.

stream_wrapper now takes passthrough=True, which iterates response.content.iter_any(): the exact same bytes, one yield per network read, no line scanning. It is applied only to routes no internal consumer parses line-by-line: the ollama pull/push/create/generate proxies and its v1 completions, chat completions, messages and responses endpoints, plus the openai embeddings, responses and catch-all proxies. The two internally consumed chat routes keep line iteration, which the streaming middleware and the Ollama-to-OpenAI converter require; the ollama send_request signature documents that constraint.

Benchmark (local aiohttp SSE server, 500 events, consumed through stream_wrapper):

| metric | before (readline) | after (iter_any) |
| --- | --- | --- |
| stream consumption time | 1.46 ms | 0.62 ms |
| generator yields + response writes per stream | 1000 | 1 |

The single yield is a loopback artifact (the whole body arrives in one buffered read); over a real network it becomes one yield per TCP read instead of two per SSE event.

Functionally verified: line mode and passthrough mode produce byte-identical output for the same stream, and passthrough always yields fewer, larger chunks.
2026-07-27 00:58:24 -04:00
Classic298
5dcca59aee
fix: route OAuth profile-picture fetch through the SSRF-safe session (#26699)
_process_picture_url validated the picture URL with validate_url() but then fetched it with a plain aiohttp session that resolves the hostname again at connect time, leaving a DNS-rebinding TOCTOU window (the same gap already closed for the RAG loader, the content probe, the image fetches and webhook delivery). Routing the fetch through get_ssrf_safe_session() pins the connect-time resolution via _SSRFSafeResolver and rejects non-global addresses, so a rebinding host can no longer redirect the fetch to loopback, RFC1918 or cloud-metadata endpoints. It also stops the forwarded OAuth access_token from leaking to a rebound internal target.
2026-07-27 00:56:24 -04:00
Timothy Jaeryang Baek
dd86b984bd refac 2026-07-27 00:55:16 -04:00
Timothy Jaeryang Baek
1717b493d8 refac
Co-Authored-By: Classic298 <27028174+Classic298@users.noreply.github.com>
2026-07-27 00:54:28 -04:00
Timothy Jaeryang Baek
5c505c1119 refac 2026-07-27 00:48:30 -04:00
G30
085d11eef2
chore: drop redundant background repaints so surfaces inherit their parent (#27576)
* chore: drop redundant background repaints so surfaces inherit their parent

Four spots repaint the exact color their parent surface already provides
(bg-white / dark:bg-gray-900 rows inside same-colored pages and modals,
and the selectClass dark repaint inside the connection modals — the
sibling input const is already fully transparent). Visually identical in
stock light and dark; removing them lets instance theming show through
instead of leaving opaque boxes:

- .tiptap tr (app.css) — table rows in notes/editors
- Edit User Group Users tab body rows (common Modal surface)
- AddToolServerModal + AddTerminalServerModal selectClass dark repaint

The matching repaints inside the ModelUsage/UserUsage components are
not part of this change — those files were dead code and were removed
entirely in #27574.

* chore: catch remaining redundant surface repaints missed in the first pass

Same rule as the previous commit — every one of these repaints the exact
color its parent surface already provides, so removal is stock-identical
in light and dark while letting instance theming show through:

- Analytics Dashboard's inline Model Usage / User Activity row markup
  (the Analytics tab renders these tables from Dashboard.svelte itself;
  the unreferenced ModelUsage/UserUsage component files were removed
  in #27574)
- Evaluations Feedbacks + Leaderboard body rows (settings modal surface)
- admin UserList body rows (app page surface)
- chat markdown tables (MarkdownTokens): thead and body rows — unlike
  the tiptap header (gray-850 contrast, untouched), this thead painted
  the page's own color
- CitationsModal source rows (common Modal surface)
- AddConnectionModal selectClass dark repaint — third copy of the same
  const already fixed in AddToolServerModal / AddTerminalServerModal
2026-07-27 00:44:51 -04:00
Classic298
41573d52f1
fix: require an authenticated user on the Ollama version route (#27199)
get_ollama_versions was the only Ollama route besides the static health check without an authentication dependency, so an anonymous caller could read the configured backend's version string and, by walking url_idx until the lookup raised, count the configured backends.

Nothing depends on the route being public. The frontend wrapper takes a token and sends it on every call, and its three call sites (admin model management, the model selector and the About panel) all pass an authenticated token, so the client already treats this as an authenticated route. Add the same get_verified_user dependency the sibling routes carry.

Co-authored-by: Grg0rry <Grg0rry@users.noreply.github.com>
2026-07-27 00:44:31 -04:00
G30
8295f2dacc
chore: remove dead admin Analytics ModelUsage and UserUsage components (#27574)
Nothing in the tree imports either component; the admin Analytics tab
renders its own inline copies of both tables directly from
Dashboard.svelte. Both files landed with the dashboard in a4ad34841
(feat: analytics frontend dashboard) but were never wired into it.

The remaining name matches elsewhere (the getUserUsage API and
UserUsage* types in src/lib/apis/users/index.ts, consumed by
chat/Settings/Usage.svelte, plus the backend usage endpoints) belong to
the unrelated per-user usage feature and are untouched.
2026-07-27 00:35:08 -04:00
Timothy Jaeryang Baek
55e0801dab refac 2026-07-27 00:34:25 -04:00
EntropyYue
f21d7947f9
fix: Set default Redis socket timeout to None (#27104) 2026-07-27 00:30:00 -04:00
Timothy Jaeryang Baek
57e60423b9 refac 2026-07-27 00:27:38 -04:00
Timothy Jaeryang Baek
20647bd2d5 chore: format 2026-07-27 00:12:47 -04:00
Timothy Jaeryang Baek
e53ff57fb5 refac 2026-07-27 00:12:16 -04:00
Timothy Jaeryang Baek
c727643e05 refac 2026-07-27 00:11:59 -04:00
Timothy Jaeryang Baek
4a7d4ebada refac 2026-07-27 00:10:36 -04:00
Timothy Jaeryang Baek
8ddf119570 refac 2026-07-26 23:55:37 -04:00
Timothy Jaeryang Baek
e5a08d5220 refac 2026-07-26 23:54:16 -04:00
Timothy Jaeryang Baek
ba7c95f7ef refac 2026-07-26 23:50:09 -04:00
G30
cb64068893
feat: add default file upload mode user setting (#20900)
* feat: add default upload mode setting

Add user setting to configure the default upload mode for files, allowing users to choose between "Using Entire Document" (full context) and "Using Focused Retrieval" (RAG processing) as the default behavior.

* i18n: sync locale catalogs for the new upload mode strings

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* chore: re-trigger CI (previous run hit the pre-existing Node heap OOM, see #27254)

* fix: apply the default upload mode at upload time so the payload carries it

The previous approach only pre-set the modal toggle's visual state on
mount; item.context is written solely by the Switch's on:change, so the
sent files kept context: undefined and the backend never saw 'full'. It
also showed a misleading ON state for legacy context-less files, since
FileItemModal mounts with every FileItem chip render.

Stamp context on the fileItem in uploadFileHandler instead (before
...itemData, so callers passing an explicit context still win) and
revert the FileItemModal hunk — the modal already renders from
item.context alone.

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-26 22:50:03 -05:00
Timothy Jaeryang Baek
6f93ecd4fd refac 2026-07-26 23:49:03 -04:00
Classic298
2196b4e1ff
perf: stop resolving DNS on the thread pool (add aiodns) (#27440)
aiohttp resolves every hostname with ThreadedResolver unless the aiodns package is importable, and ThreadedResolver runs socket.getaddrinfo on asyncio's default ThreadPoolExecutor. That executor is capped at min(32, cpu_count + 4) threads and is shared with every other piece of blocking work posted to it, so DNS is currently a bounded blocking resource sitting in front of every model call, every web search fetch, every RAG page load and every tool call. In plain terms: once that pool is busy, requests wait on name lookups that should never have occupied a thread at all.

This is a dependency-only change. aiohttp sets `DefaultResolver = AsyncResolver` as soon as aiodns is importable (aiohttp/resolver.py), so resolution moves onto the event loop via c-ares with zero application code touched. That is deliberate rather than lazy: there are 50 `aiohttp.ClientSession(...)` construction sites in the backend, most building a fresh default connector per call, and the alternative of passing `resolver=aiohttp.AsyncResolver()` explicitly would mean touching all of them and re-touching every future one. The shared pool in `utils/session_pool.py` does set `ttl_dns_cache`, but that only helps the shared pool. Every per-request session, including `SafeWebBaseLoader._fetch()` which builds a new session per URL, starts with a cold DNS cache and resolves from scratch.

It also unbreaks a code path that is dead today. `backend/open_webui/retrieval/loaders/mistral.py:480` constructs `aiohttp.AsyncResolver()` unconditionally, and `AsyncResolver.__init__` raises `RuntimeError("Resolver requires aiodns library")` when aiodns is absent, so the Mistral OCR content extraction engine fails on a stock install. This supplies the dependency that line already assumes. Once it is present that kwarg is redundant, since it now names the default, and dropping it is a reasonable follow-up. Reproduced by blocking the aiodns import:

```
aiodns importable: False
DefaultResolver: ThreadedResolver
AsyncResolver(): RuntimeError: Resolver requires aiodns library
```

## Benchmarks

Both sides run the real aiohttp resolver classes. The DNS wire time is replaced by an identical fixed 50ms delay on both sides, so the only variable measured is where that delay is spent. 24 cores, so the default executor holds 28 threads. `exec_max` is the worst latency an unrelated `run_in_executor` job suffered while the lookups were in flight.

Concurrent lookups, wall time:

| concurrent lookups | ThreadedResolver | AsyncResolver | speedup | exec_max before | exec_max after |
|---|---|---|---|---|---|
| 16 | 54.3ms | 41.0ms | 1.3x | 2.1ms | 1.9ms |
| 32 | 101.9ms | 50.6ms | 2.0x | 36.6ms | 1.8ms |
| 64 | 152.5ms | 43.2ms | 3.5x | 88.4ms | 2.0ms |
| 128 | 254.9ms | 44.5ms | 5.7x | 190.3ms | 2.2ms |
| 256 | 508.2ms | 50.7ms | 10.0x | 443.8ms | 2.4ms |
| 512 | 965.2ms | 47.8ms | 20.2x | 900.2ms | 2.6ms |

ThreadedResolver scales linearly with concurrency because it can only run 28 lookups at a time. AsyncResolver stays flat at roughly the cost of one lookup.

The reverse direction is worse and is not hypothetical. Open WebUI already posts long blocking jobs to that same executor (`retrieval/vector/dbs/pinecone.py:323` batch upserts, `retrieval/loaders/youtube.py:156` transcript loads). With 28 such jobs holding the pool, a single DNS lookup waits for them to finish:

| | one DNS lookup |
|---|---|
| ThreadedResolver | 1989.7ms |
| AsyncResolver | 58.9ms |

A Pinecone bulk upsert currently stalls name resolution for every other user on the instance. After this change it cannot.

At low concurrency on a real network the two are equivalent, as expected: 8 concurrent lookups against disjoint cold hostname sets landed within noise of each other in both directions.

## Behaviour verification

Checked against Open WebUI's own code, not in isolation:

- c-ares reads the system hosts file. Verified against a machine whose hosts file maps `adobe.io` to `0.0.0.0`, an address real DNS never returns for that name: c-ares returned `0.0.0.0`. `host.docker.internal`, compose `extra_hosts` and Kubernetes `hostAliases` keep working.
- `_SSRFSafeResolver` subclasses `aiohttp.resolver.DefaultResolver`, so this change swaps its base class from ThreadedResolver to AsyncResolver at runtime. It still resolves public hosts, still returns entries with the `host`/`port` keys the SSRF check reads, and still raises on a private address: resolving `localhost` raised `ValueError: The URL you provided is invalid.`
- A real fetch through `get_ssrf_safe_session()` returned 200.
- NXDOMAIN still surfaces as `aiohttp.ClientError` (`ClientConnectorDNSError`), not a c-ares specific exception, so existing error handling is unaffected.

Known limit: c-ares reads `/etc/resolv.conf` and the hosts file but not the rest of `nsswitch.conf`. Names served only by an NSS module, such as `.local` via avahi/mDNS, NIS/LDAP backends or Windows NBNS, will resolve differently or not at all. On a multi-homed test machine the local hostname returned two addresses through the system resolver and one through c-ares. Deployments pointing Open WebUI at an mDNS or NetBIOS hostname are the group affected. Resolver failures also arrive as plain `OSError` rather than `socket.gaierror`, which no code in this repo catches today.
2026-07-26 23:29:52 -04:00
Classic298
f32b19c1f6
feat: add {{USER_GROUPS}} and {{USER_GROUP_IDS}} placeholders for custom forwarded headers (#27236)
Custom per-connection headers can now forward the user's groups to
upstream backends via two new template placeholders:

- {{USER_GROUPS}}: comma-separated group names
- {{USER_GROUP_IDS}}: comma-separated group ids

The group lookup is async, so get_custom_headers becomes an async
wrapper around the sync template substitution (parse_custom_headers)
and fetches groups lazily — only when a header value actually
references a groups placeholder. The external document loader path
runs in a worker thread without an event loop, so Loader.aload
prefetches the groups before offloading and passes them through to
ExternalDocumentLoader.


Claude-Session: https://claude.ai/code/session_01EbBEfTyu8fFJmC13rnQthT

Co-authored-by: Claude <noreply@anthropic.com>
2026-07-26 23:21:26 -04:00
Timothy Jaeryang Baek
3cd72ee6a8 refac 2026-07-26 23:19:20 -04:00
Timothy Jaeryang Baek
b7489bbc6c refac 2026-07-26 23:16:58 -04:00
Timothy Jaeryang Baek
ed663f16ec refac 2026-07-26 23:09:22 -04:00
Timothy Jaeryang Baek
95d590b360 refac 2026-07-26 23:03:32 -04:00
Timothy Jaeryang Baek
8b206de48e refac 2026-07-26 22:59:23 -04:00
Timothy Jaeryang Baek
bf35f64a7f refac 2026-07-26 22:45:11 -04:00
Timothy Jaeryang Baek
fc4906c9e9 refac 2026-07-26 22:32:17 -04:00
Timothy Jaeryang Baek
aadab2f480 refac 2026-07-26 22:32:06 -04:00
Timothy Jaeryang Baek
846ba80a9d refac 2026-07-26 22:32:03 -04:00
Timothy Jaeryang Baek
d94d36ad72 refac 2026-07-26 22:16:31 -04:00
Timothy Jaeryang Baek
d14fddf254 refac 2026-07-26 21:55:13 -04:00
Timothy Jaeryang Baek
0cbf337679 refac 2026-07-26 21:54:06 -04:00
Timothy Jaeryang Baek
85c47fb467 refac 2026-07-26 21:51:35 -04:00
Timothy Jaeryang Baek
1de36d600f refac 2026-07-26 21:35:56 -04:00
Timothy Jaeryang Baek
71c4da8c06 refac 2026-07-26 21:12:14 -04:00
Timothy Jaeryang Baek
f867825bf3 refac 2026-07-26 21:08:49 -04:00
Timothy Jaeryang Baek
b45c020f68 refac 2026-07-26 21:08:44 -04:00
Timothy Jaeryang Baek
d2936c880c refac 2026-07-26 21:07:27 -04:00
Timothy Jaeryang Baek
d484a2a99e refac 2026-07-26 21:07:20 -04:00
Timothy Jaeryang Baek
bab71ed08b refac 2026-07-26 21:06:18 -04:00
Timothy Jaeryang Baek
db5c092299 refac 2026-07-26 21:02:31 -04:00
Timothy Jaeryang Baek
f798d05586 refac
Some checks are pending
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
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: 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
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 / 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 (-ollama, ollama) (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 / Format & Build (push) Waiting to run
Frontend Build / Unit Tests (push) Waiting to run
2026-07-26 19:34:41 -04:00
Timothy Jaeryang Baek
94a60b0457 refac 2026-07-26 19:10:41 -04:00
Classic298
54f06d8c53
perf: build chat responses without deep-copying the blob through model_dump (#27388)
Chat search built each result row with ChatTitleIdResponse(**chat.model_dump(), ...), which recursively copies the entire chat blob per row only for the constructor to ignore everything except id, title and timestamps: a 60-row search page deep-copied up to 60 full conversations. The folder listing, archived and export endpoints and every single-chat response did the same dump-and-revalidate dance via ChatResponse(**chat.model_dump()).

Search rows are now built from the five fields the response actually has (the snippet helper receives the blob by reference as before), and all 18 ChatResponse constructions use ChatResponse.model_validate(chat, from_attributes=True), which reads the fields off the already-validated ChatModel without copying the blob.

Benchmark (~500 KB chat blob):

| metric | before | after |
| --- | --- | --- |
| search result row | 0.05 ms | 0.003 ms |
| ChatResponse construction | 0.05 ms | 0.003 ms |
| per search page (60 rows) | 3 ms | 0.2 ms |

Beyond CPU, each converted row also stops materializing a second full copy of the conversation in memory while the page is being built.

Functionally verified: both construction styles produce identical model_dump() output for ChatResponse (including defaulted fields absent on ChatModel) and for search rows including the snippet.
2026-07-26 18:57:52 -04:00
Classic298
4f93c3e36c
fix: authorize before cancelling tasks in the chat delete endpoint (#27006)
DELETE /api/v1/chats/{id} called stop_item_tasks(id) before checking the
caller's chat.delete permission or ownership of the target chat. An
authenticated user who knew another user's chat id could therefore cancel that
chat's in-flight generation (streaming response, title or tag generation) even
though the deletion was then rejected. The chat id is discoverable through
legitimate read-only access to a shared chat or folder.

Reorder the handler to authorize first (admin, or owner holding chat.delete) and
only then cancel tasks and delete, matching the dedicated task-stop endpoint.
Legitimate deletions are unchanged; an unauthorized caller now returns 404 or 401
before any cancellation. The duplicated tag-cleanup and event-publish blocks are
merged.

Co-authored-by: GabrielGomesAL <193945687+GabrielGomesAL@users.noreply.github.com>
2026-07-26 18:57:35 -04:00
Classic298
1ac8ef7853
fix: gate the remaining text contrast failures behind High Contrast Mode (#27558)
Completes the contrast set after #27555, #27554 and the gray-500 branch, which between them cover text-gray-400, text-gray-500, dark:text-gray-600 and placeholders. This is everything still under 4.5:1 after those.

The grey scale in src/tailwind.css is achromatic oklch(L 0 0), so relative luminance is exactly L³. What is left:

- text-gray-300 dark:text-gray-700, the lightest muted pair, at 1.58:1 in light and 2.14:1 in dark. Used for inactive tab labels across the admin, workspace and playground layouts, breadcrumb separators and empty state hints. It resolves to gray-600 in light and gray-400 in dark.
- text-gray-400/70 on the embedded chat history dropdown icon, 2.07:1 against the 3:1 that WCAG 1.4.11 requires of icons.
- Hover states that land lighter than the new resting colour. Once the resting state is gray-600, an element hovering to gray-500 gets less readable on interaction rather than more, so hover and group-hover targets of gray-500 resolve to gray-800. Sidebar/Section.svelte and the citation modal links are the sites this affects.
- The autocompletion ghost text in src/app.css, hardcoded #a0a0a0, 2.65:1 in light. The dark canvas already passes.
- The shimmer used for loading text, a #b4b4b4 gradient clipped to the glyphs at 2.10:1 in light. There is no solid colour to raise, so with the setting on it renders as flat gray-700 text instead.

Everything above is gated on the existing High Contrast Mode setting and changes nothing when it is off. No markup is touched, so this is src/app.css only.

Deliberately left alone: disabled: variants, since WCAG 1.4.3 exempts inactive components; the FileNav breadcrumb ancestors, which are non-clickable; decorative folder icons; and text-gray-100, dark:text-gray-800 and dark:text-gray-900, which are inverse text on filled buttons and already high contrast against their own backgrounds. The ad-hoc dark:text-gray-800 pairs in ChannelModal.svelte and automations/+layout.svelte stay as they are; dark:text-gray-800 doubles as the inverse text on the white buttons in Message.svelte, ResponseMessage.svelte and UserMessage.svelte, so it cannot be remapped in CSS without breaking those.

Not fixed here, and a genuine follow-up: .hljs-comment in src/app.css is #616161, roughly 3:1 on the dark code background. It sits outside the Tailwind grey scale and needs a highlight.js theme override rather than a utility remap.

Verified in a browser against Tailwind's emitted rules and layer order: with the setting on, the lightest pair resolves to gray-600 in light and gray-400 in dark, the hover and group-hover targets to gray-800, ghost text to gray-600 and the shimmer to solid gray-700, while inverse button text and every dark hover variant stay where they are; with the setting off nothing changes in either theme.
2026-07-26 18:57:00 -04:00
Classic298
5b035ea52b
fix: let Select announce its selected value and open state (WCAG 2.5.3, 4.1.2) (#27492)
On latest `dev`, the `Select` trigger sets `aria-label={placeholder}`. In the accessible name computation `aria-label` is evaluated before the element's contents, so on a button that renders visible text it **replaces** that text instead of adding to it.

The trigger's content is `selectedLabel`, which resolves to the selected item's label and only falls back to `placeholder` when nothing is selected. So a control visually reading "Week" is exposed to assistive technology as "Select view", permanently, no matter what is selected. The dropdown items expose no `aria-selected` either, the current one is marked with a check icon only, so there is no path by which a screen reader user can find out what the control is set to.

Breaks WCAG 2.5.3 Label in Name (Level A), because the accessible name does not contain the visible label, so voice control cannot target the control by what it says on screen. Also 4.1.2 Name, Role, Value (Level A), because the value is never exposed.

Fix: drop the overriding `aria-label` so the name is computed from the visible text, and expose `aria-expanded` so the open state is conveyed. All 7 call sites plus the `DropdownOptions` wrapper override `slot="trigger"` and every one of them renders `selectedLabel` (or `placeholder`) as text, so no trigger is left unnamed. The two call sites that pass no `placeholder` were already emitting an empty `aria-label`, which is skipped by the name computation, so they are unaffected.

`aria-haspopup` is deliberately not added: the popup is a plain `DropdownMenu` of buttons with no `listbox` role, so claiming one would misdescribe it.

`placeholder` is still used, it drives the `selectedLabel` fallback and `TagSelector` renders it directly.

Severity: Serious. Affects every custom select in Admin Settings, Workspace and the calendar and automations pages.

### 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-07-26 18:56:49 -04:00
Classic298
4650f64c1e
fix: make admin user table sortable by keyboard (WCAG 2.1.1, 4.1.2) (#27501)
On latest `dev`, the five sortable column headers in the admin Users table are click handling `<th>` elements:

```svelte
<th scope="col" class="px-2.5 py-1.5 font-normal cursor-pointer select-none" on:click={() => setSortKey('name')}>
```

A `<th>` is not interactive. There is no `<button>`, no `tabindex`, no `role` and no key handler, so **sorting the user list is impossible without a mouse**. The sort direction is also conveyed only by an 8×8 pixel chevron, with no programmatic state, so assistive technology cannot report which column is sorted or in which direction.

Breaks WCAG 2.1.1 Keyboard (Level A) and 4.1.2 Name, Role, Value (Level A).

Fix: move the click handler onto a real `<button>` inside the header, which brings native focus, Enter and Space activation and the correct role, and add `aria-sort` to the `<th>`, which already carries `scope="col"` and therefore the implicit `columnheader` role. Only the active column reports a direction, since `orderBy` is a single value; the non sortable actions column deliberately gets no `aria-sort` at all rather than `none`, so it is not advertised as sortable.

The cell padding moves from the `<th>` onto the button so the whole header stays clickable. Left on the `<th>`, the padding ring would have become a dead zone, shrinking the hit target and flipping the cursor at an invisible boundary inside the header.

`cursor-pointer` is dropped from the `<th>` because `src/tailwind.css` already applies it to every `button`.

The repeated `aria-sort` ternary is extracted to a small `sortState` helper rather than pasted five times.

The same mouse only `<th on:click>` pattern still exists in the Analytics, Evaluations and Groups tables and is not touched here.

Severity: Serious. A core admin function is unreachable without a pointing device.

### 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-07-26 18:56:38 -04:00
Classic298
c055203f29
fix: refresh OAuth session before the id_token expires (#27520)
`_normalize_token_expiry()` derived the session expiry from the access token alone, and that value is what `oauth_session.expires_at` stores and what both `get_oauth_token()` implementations check to decide whether to refresh five minutes ahead. Providers that issue a shorter-lived id_token than access token (Microsoft Entra ID: roughly 60 minutes against 75) therefore left a window where the session still looked valid while the id_token had already expired, so pipes and tools reading `__oauth_token__["id_token"]` forwarded a dead JWT and downstream services rejected it with 401.

The stored expiry is now capped at the id_token's `exp` claim whenever that JWT expires first, which moves the refresh ahead of the earliest expiring token in the set. This is applied in the single function every session write already passes through, so it covers both the SSO manager and the MCP client manager on their callback and refresh paths alike. Sessions without an id_token, with an opaque one, or with no `exp` claim are unaffected.

Fixes #27066
2026-07-26 18:56:18 -04:00
Classic298
99da2324e3
fix: preserve chunk order when assembling multi-chunk transcriptions (#27417)
When an audio file is split into multiple chunks for transcription, transcribe() collected the per-chunk results with asyncio.as_completed(), which yields results in completion order rather than submission order. Whenever a later chunk finished transcribing before an earlier one, the assembled transcript was scrambled, for example the second half of a recording appearing before the first, and the stored file content plus everything downstream (file preview, full-context retrieval) read out of chronological order. This change awaits the chunk tasks with asyncio.gather() instead, which runs them just as concurrently but returns the results in the order the tasks were created, i.e. chunk_paths order. The existing error handling and chunk cleanup are unchanged: an HTTPException from a chunk is re-raised as is and any other error is wrapped in a 500. Fixes #27143
2026-07-26 18:55:52 -04:00
Classic298
30be10f968
feat: prevent duplicate auth form submissions while one is pending (#27416)
* feat: prevent duplicate auth form submissions while one is pending

When a sign in, sign up or LDAP request is slow, the auth form can be submitted again and every extra click or Enter press starts another concurrent authentication request. The form never tracked a pending state, so submitHandler dispatched a new API call on every submit event. This adds a submitting flag that makes submitHandler ignore re-entrant submits, disables both submit buttons with a dimmed style while a request is in flight and resets the flag in a finally block so the form recovers after a failed attempt. Guarding submitHandler covers button clicks and Enter key submits for sign in, sign up and LDAP alike since all of them flow through the single form submit handler.
Fixes #27264

* feat: show a spinner while an auth request is pending

Disabling the submit button stops a second submission but gives no positive sign that the first one is still running, so on a slow identity provider the form looks unresponsive rather than busy. Both submit buttons now render the existing Spinner next to their label while submitting is set, following the same in-button pattern used by the workspace editors.
2026-07-26 18:55:41 -04:00
Classic298
381149ea5e
fix: persist filter outlet() changes to structured message output (#27414)
When a filter's outlet() modified the structured assistant output in place, the change was shown immediately but lost after reload. outlet_filter_handler built its outlet payload with a shallow reference to the message's output list from messages_map, so the filter mutated the stored baseline itself and the subsequent output comparison compared the object against itself, never detecting a change and never persisting it. The same aliasing corrupted originalContent for messages whose text lives only in output. Deepcopy the output when building the outlet payload so messages_map stays a pristine pre-filter baseline and the existing change detection persists outlet-modified output through the existing upsert path. Fixes #27017.
2026-07-26 18:55:21 -04:00
Classic298
69f8be4cf9
fix: show download preparation toast and prevent duplicate zip jobs (#27421)
* fix: show download preparation toast and prevent duplicate zip jobs

Downloading a file or folder from the file navigator gave no feedback while the server prepared the response, which can take 30 seconds or more for large folders that are zipped server-side. Users assumed the click did nothing and pressed Download again, starting additional zip jobs on the server. Both downloadFile and bulkDownload, the two functions every download control funnels through, now show a persistent "Preparing download..." loading toast while the request is in flight and dismiss it once the download starts or fails. A shared downloading flag makes repeated clicks no-ops until the current download finishes, so a single click starts exactly one server-side job. Fixes #27055

* fix: report terminal download failures instead of dismissing the toast

A failed download dismissed the preparation toast without saying anything, which reads as the download silently disappearing. Both download paths now report the failure. The two helpers also declare a nullable return but could still reject once the response body started streaming, so an interrupted transfer escaped as an unhandled rejection and left the same silent dismissal. They now return null in that case, which also stops an interrupted preview from leaving its spinner running.
2026-07-26 18:55:09 -04:00
Timothy Jaeryang Baek
bef63a2ae9 refac 2026-07-26 18:54:17 -04:00
Timothy Jaeryang Baek
df94268e89 refac 2026-07-26 18:54:07 -04:00
crustopher-lgtm
5efe0951d5
feat: add OpenSERP self-hosted web search backend (#27437)
Add self-hosted OpenSERP as a web search engine option. OpenSERP
provides browser-rendered search across Google, Bing, Yandex, Baidu,
DuckDuckGo, and Ecosia with no API keys required.

- New module: retrieval/web/openserp.py (async, uses aiohttp session pool)
- Config: OPENSERP_BASE_URL env var (defaults to http://localhost:7070)
- Routing: search_web() dispatch for 'openserp' engine
- Follows existing patterns (searxng, brave)

Co-authored-by: crustopher-lgtm <crustopher-lgtm@users.noreply.github.com>
2026-07-26 18:52:08 -04:00
Timothy Jaeryang Baek
42ea8a5a2f refac 2026-07-26 18:50:22 -04:00
G30
bda49ccdb6
fix(ui): close sidebar on mobile when opening Calendar from user menu (#26979)
Every other navigation entry in the user menu (Settings, Admin Panel,
Archived Chats, Workspace, Notes, Automations, Playground, Sign Out)
collapses the sidebar on mobile after navigating, but the Calendar entry
was missing this handling, leaving the sidebar open over the Calendar
page on mobile. Add the same mobile guard used by the sibling entries.
2026-07-26 18:47:12 -04:00
Timothy Jaeryang Baek
b81627b2c9 refac 2026-07-26 18:46:39 -04:00
G30
11d72c1ce2
fix(ui): replace history entry on redirect so browser back navigation works (#27478) 2026-07-26 18:44:23 -04:00
G30
79695a1d14
fix: persist modelIdx so duplicate side-by-side models don't collapse on reload (#26980)
When the same model is selected multiple times in a side-by-side chat,
each response is created with a distinct modelIdx (0,1,2,3) that
identifies its column. The backend now owns message persistence, but it
built the assistant placeholders without modelIdx, so the field was
never saved. On reload MultiResponseMessages groups responses by
modelIdx and falls back to grouping by model id when modelIdx is
missing; with duplicate models that fallback lumps every response into
each column, so all columns render the first response (and show a bogus
'1/N' pager).

Send modelIdx with each message_ids entry from the frontend and persist
it on the assistant placeholders in both the new-chat and existing-chat
paths. The message_ids list is now forwarded for every send (not just
multi-model ones) so single-column regenerations in a duplicate-model
chat also keep their column identity across reloads.
2026-07-26 18:44:03 -04:00
G30
86bf927d08
fix(ui): stack automation modal footer on mobile (#27027)
The automation create/edit modal footer laid the schedule/model dropdowns
and the Cancel/Create actions in a single justify-between row. On narrow
modals the fixed-width actions left too little room for the dropdowns,
which wrapped to two stacked lines with Cancel squeezed in the middle.

Stack the footer vertically on mobile (dropdowns row, then a right-aligned
actions row) and restore the horizontal layout at sm and up.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-26 18:43:45 -04:00
G30
db92ef292f
fix: unarchive chats moved into folders and refresh sidebar folders after menu moves (#27485) 2026-07-26 18:43:34 -04:00
G30
71f8b6d5b4
feat: add a master OAuth / OIDC enable toggle in Authentication settings (#26988)
The OAuth / OIDC section in Admin Settings > Authentication had no
enable/disable switch, unlike the LDAP section above it. Add one that
persists via the existing Save flow and actually gates OAuth sign-in,
mirroring how the LDAP toggle works.

- config: new ENABLE_OAUTH persistent config ('oauth.enable'), defaulting
  to True so existing deployments with a provider configured keep working.
- oauth: expose ENABLE_OAUTH via the OAuth runtime config and reject the
  login and callback handlers with 404 when it is disabled.
- /api/config: report no OAuth providers when disabled so the login page
  hides the OAuth buttons (and cannot auto-redirect), without clearing the
  admin's provider configuration.
- auths: expose ENABLE_OAUTH through the admin OAuth config get/update
  endpoints (OAuthConfigForm + OAUTH_CONFIG_KEYS).
- Authentication.svelte: bind the OAuth / OIDC header Switch to the
  persisted oauthConfig.ENABLE_OAUTH and collapse the section when off,
  matching the LDAP header (size, weight, alignment).
2026-07-26 18:43:21 -04:00
Classic298
18ca19044c
fix: gate muted text contrast fix behind High Contrast Mode (#27554)
Follow-up to #27495, reopened as a high contrast mode change. Builds on the `high-contrast` class landed in #27555.

Muted UI text is written as text-gray-400 dark:text-gray-600. The grey scale in src/tailwind.css is achromatic oklch(L 0 0), so relative luminance is exactly L³: text-gray-400 is 2.07:1 on white and dark:text-gray-600 is 3.12:1 on #171717. The pair is effectively inverted, and both halves fail the 4.5:1 required by WCAG 1.4.3, with the light value also failing the 3:1 required of icons under 1.4.11. This is the text used for settings and admin section headings, field descriptions, sidebar labels, timestamps and counters, all at 10px to 12px, so the large-text exemption does not apply.

Rather than rewriting the class literal at 251 sites, the remap is two CSS rules that only apply when the existing High Contrast Mode setting is on, so the default theme is untouched:

- text-gray-400 resolves to gray-600 (5.75:1) in light mode
- dark:text-gray-600 resolves to gray-400 (8.65:1) in dark mode

dark:text-gray-500 already passes at 6.46:1 and is left alone.

The rules live in `@layer utilities` and use `:where()` to stay at low specificity: they outrank the base utility but lose to `hover:` and `dark:hover:` variants, so hover feedback keeps working. Verified in a browser against Tailwind's emitted rules and layer order: with the setting on, resting text resolves to gray-600 in light and gray-400 in dark, hover still resolves to its own value, and with the setting off nothing changes in either theme.

One component change is required alongside it. In admin/Settings/Audio.svelte the help text puts links inside the muted block via `[&_a]:text-gray-600`; once the surrounding prose resolves to gray-600 the link becomes the same colour as the text it sits in, and it has no resting underline, which would be a new WCAG 1.4.1 failure. The link moves to gray-900. This is the only place in the codebase where a link colour is nested inside muted text.

Letting the variants win has one edge: a few elements hover to a grey lighter than their new resting colour, so hovering would have lowered contrast instead of raising it. A light-mode hover landing on gray-500 now resolves to gray-800, which keeps the hover darker than the gray-600 resting state. Sidebar/Section.svelte is the site this branch would otherwise break.

Not covered here: text-gray-500 dark:text-gray-400 (2.77:1 in light), which is a separate branch.
2026-07-26 18:37:49 -04:00
Timothy Jaeryang Baek
453b9fb029 refac 2026-07-26 18:36:49 -04:00
Classic298
50afbc5319
fix: allow setting model order via MODEL_ORDER_LIST env var (#27420)
With ENABLE_PERSISTENT_CONFIG=False the admin's model order is reset on every restart because ui.model_order_list falls back to its DEFAULT_CONFIG default, and unlike every other Models setting (DEFAULT_MODELS, DEFAULT_PINNED_MODELS, DEFAULT_MODEL_METADATA and DEFAULT_MODEL_PARAMS) that default was hardcoded to an empty list with no environment variable to source it from. This adds a MODEL_ORDER_LIST environment variable parsed as a JSON array using the same guarded pattern as the neighbouring DEFAULT_MODEL_METADATA and DEFAULT_MODEL_PARAMS defaults, falling back to an empty list on parse errors. Behaviour when the variable is unset is unchanged.

Fixes #27206
2026-07-26 18:34:18 -04:00
Classic298
0116c6e1b9
perf: stop running chardet over entire uploaded files (#27445)
`_detect_text_encoding()` hands the complete file to `chardet.detect()`. chardet is pure Python and costs roughly 1.3 seconds per megabyte, so uploading a large non-UTF-8 text file stalls for seconds inside encoding detection alone. A 4 MiB Shift-JIS file spends 6.4 seconds there. The UTF-8 fast path above it means only non-UTF-8 files reach this, which in practice are exactly the CJK documents the surrounding code was written to handle, so the slow case and the case that matters are the same case.

Detection does not need the whole file. It needs the bytes that are actually not UTF-8, and `UnicodeDecodeError.start` from the fast-path decode already says where those begin, so this samples a 256 KiB window around that offset.

Two things make that safe rather than merely fast.

Centring the window on the first non-UTF-8 byte instead of the file head is what keeps the common case correct. A plain head sample makes chardet report ascii for a file that is ASCII for its first few hundred KiB and only turns CJK later, and the method then falls through to latin-1 instead of the right codec.

The window still cannot help when a stray byte, a pasted Windows-1252 artifact for example, sits hundreds of KiB ahead of the real payload: the sample is then almost pure ASCII and carries no signal. So when the sample holds almost no non-ASCII bytes and is a strict subset of the file, detection falls back to the whole buffer. That case pays the old cost, which is the right trade, because it is precisely the case where sampling would otherwise be wrong. Without this guard a Cyrillic document with a stray leading byte was detected as ISO-8859-1 rather than windows-1251, which is silent mojibake.

Measured, with the encoding returned identical in every case:

| file | before | after |
|---|---|---|
| shift_jis 4 MiB | 6402ms | 755ms |
| gb18030 4 MiB | 3199ms | 449ms |
| big5 4 MiB | 2926ms | 413ms |
| euc-jp 4 MiB | 2456ms | 413ms |
| euc-kr 4 MiB | 2382ms | 468ms |
| latin-1 4 MiB | 1902ms | 394ms |
| gb18030 1 MiB | 807ms | 376ms |
| ascii head then gb18030 tail | 533ms | 294ms |
| stray byte then cp1251 payload | 496ms | 1051ms |
| any UTF-8 file | 8ms | 0ms |

29 cases, all returning an identical encoding before and after: six encodings at 100 KiB, 1 MiB and 4 MiB, three layouts where the non-UTF-8 bytes only begin beyond the window, four where a stray byte is separated from the payload, plus plain UTF-8, UTF-8 CJK and an empty file. The stray-byte rows are slower than before because they scan twice, once over the window and once over the whole buffer. They are the pathological shape, and correctness wins there.

The residual time is now the decode-and-validate loop below, which walks the file once per candidate codec, and `_has_cjk_characters`, which is a per-character Python loop over the decoded text. Both are the same "full scan for a detection decision" pattern and could take a bounded prefix too. That is left alone here.
2026-07-26 18:34:02 -04:00
Classic298
bc948f8f22
perf: parse scraped web pages off the event loop (#27446)
`alazy_load()` builds every BeautifulSoup tree inline in an async function, so a web search that pulls in ten pages stops the entire worker for the whole time it spends parsing. Nothing else on that worker runs during it: not other users' token streams, not health checks, not socket.io traffic. Parsing is CPU work and it belongs in a thread.

Measured over 37 real pages, 13.5 MiB total, with a 5ms ticker sampling event-loop lag:

| | wall | worst loop stall | ticker fired |
|---|---|---|---|
| inline, html.parser (today) | 1793.8ms | 1788.8ms | 1 time |
| offloaded, html.parser | 1872.9ms | 82.9ms | 88 times |
| inline, lxml | 1346.7ms | 1341.8ms | 1 time |
| offloaded, lxml | 1445.4ms | 37.0ms | 118 times |

Today the loop is not merely slow during a batch, it is gone: a 5ms timer fired exactly once across 1.8 seconds. After the change it fires normally and the worst single stall drops by a factor of 20 to 36. The cost is 4 to 7 percent more wall time for the batch itself, from the thread handoffs, which is the right trade for a server handling more than one user.

Three details behind the shape of the change:

`get_text()` is only 2 percent of the cost (34ms against 1706ms of parsing over the corpus), so the whole per-page unit moves into the thread rather than the parse alone. Splitting them measured worse on both axes.

The offload is per page, not per batch. Handing the whole batch to one thread measured worse than either (2081ms wall, 235ms worst stall), so the loop is yielded to between pages.

The metadata block in `alazy_load()` was a duplicate of the module-level `extract_metadata()`, field for field, and `lazy_load()` was already using the shared helper. The new helper calls it too, which is why the diff removes more lines than it adds. The `ascrape_all()` override goes with it: it was a verbatim copy of the inherited implementation and `alazy_load()` was its only caller, so anything still calling it now gets the identical parent method, which resolves `self._unpack_fetch_results` to the override this class keeps.

Verified by feeding the real loader a 37 page corpus and comparing every resulting Document against the implementation this replaces:

```
PASS  one Document per url (37)
PASS  every Document identical to the pre-change implementation (0 differ)
PASS  parsing ran off the main thread
PASS  event loop kept running during parsing (90 ticks)
```

Both `page_content` and `metadata` are byte-identical on all 37 pages. This is independent of the parser in use and composes with switching the default parser to lxml: that change makes the stalls shorter, this one takes them off the loop.
2026-07-26 18:33:27 -04:00
Classic298
b9cfba62d7
chore: regenerate uv.lock (#27557)
Regenerated with the current uv so `uv lock --check` passes again. The committed lockfile was written by an older uv and every run since has reported it as needing an update, which makes it impossible to tell real drift apart from format drift.

No resolved dependency changes: all 354 packages keep their versions, and no existing artifact URL or hash changes. The diff is almost entirely `upload-time` annotations added per artifact. The remaining changes are the `revision = 3` format marker, a `provides-extras` entry on the project stanza, additional GraalPy wheel URLs for already-locked versions of jiter, pybase64, pydantic-core and ujson, and the removal of the hardcoded `version = "0.10.2"` from the project's own stanza, which was stale metadata since pyproject.toml declares `dynamic = ["version"]`.

Nothing that gets installed changes: the Dockerfile installs from backend/requirements.txt and no workflow runs `uv lock` or `uv sync`.
2026-07-26 18:31:12 -04:00
Timothy Jaeryang Baek
4856afcef8 refac 2026-07-26 18:30:58 -04:00
Timothy Jaeryang Baek
dcc7fb1e8e refac 2026-07-26 18:26:15 -04:00
Classic298
301bf519ab
fix: resolve circular OpenAPI schema refs in tool server specs (#27413)
OpenAPI specs with circular schema references, such as Mealie's where Recipe and RecipeCategory reference each other through properties and array items, crashed convert_openapi_to_tool_payload with a RecursionError, so the tool server produced no specs and the integration never appeared in the model or tool selection. resolve_schema already had a visited-set guard against circular references, but the recursive calls for properties and items dropped the set, so cycles running through those edges were never detected. This threads the visited set through those calls and passes a per-path copy when following a $ref, so only true ancestor cycles are pruned to an empty schema while sibling references to the same schema still resolve fully. Fixes #27239.
2026-07-26 18:20:10 -04:00
G30
771540f3de
feat: sortable title and updated at columns on the knowledge workspace page (#27457) 2026-07-26 18:19:31 -04:00
Classic298
fb1f1a3c92
perf: parse scraped web pages with lxml, not html.parser (#27439)
Every page pulled in by web search and web RAG is parsed with BeautifulSoup's `html.parser`, a pure-Python parser. It is the slowest option bs4 offers, and it is being handed 300 KiB to 1.5 MiB documents, several per query. `SafeWebBaseLoader` inherits `default_parser = "html.parser"` from langchain's `WebBaseLoader` and never overrides it, so this is an upstream default carried by accident, not a decision anyone made for Open WebUI.

`default_parser` is the single chokepoint for both the sync `_scrape()` path and the async `ascrape_all()` path, so one `setdefault` covers everything and an explicit caller override still wins.

lxml is already in the tree as a transitive hard dependency of ddgs, python-pptx and unstructured, so nothing new enters the image and `uv.lock` already resolves it at 6.1.1. The pin makes it explicit and closes a latent failure: bs4's `"xml"` feature, already used for `.xml` URLs in `_unpack_fetch_results()`, requires lxml and would raise `FeatureNotFound` the day that transitive dependency moves.

## Benchmarks

37 real pages, 13.8 MiB of HTML, median of 5 runs each. The timed operation is `BeautifulSoup(html, parser)` plus `get_text()` plus `extract_metadata()`, which is exactly what the loader does per page. bs4 4.14.3, lxml 6.1.1, CPython 3.12.

| | html.parser | lxml | |
|---|---|---|---|
| 37 pages, 13.8 MiB total | 1611.0ms | 1151.3ms | 1.4x faster, 460ms saved |

Largest pages:

| page | size | html.parser | lxml | speedup |
|---|---|---|---|---|
| pypi.org/project/aiohttp/ | 1259 KiB | 243.75ms | 180.51ms | 1.4x |
| gnu.org/software/bash/manual/bash.html | 1017 KiB | 257.97ms | 178.99ms | 1.4x |
| rfc-editor.org/rfc/rfc9110.html | 1157 KiB | 205.94ms | 154.87ms | 1.3x |
| docs.aiohttp.org/en/stable/client_reference.html | 403 KiB | 108.62ms | 84.93ms | 1.3x |
| ollama.com/library | 779 KiB | 117.55ms | 73.64ms | 1.6x |
| theregister.com | 1052 KiB | 88.32ms | 60.12ms | 1.5x |
| kubernetes.io/docs/concepts/services-networking/service/ | 563 KiB | 72.18ms | 43.43ms | 1.7x |
| docs.python.org/3/library/socket.html | 301 KiB | 71.88ms | 49.04ms | 1.5x |

Ranges from 1.1x to 1.7x, and the win grows with page size. A ten result web search sheds roughly 125ms of parsing. Because the async path builds its soups inline in `_unpack_fetch_results()`, that is 125ms the event loop spends parsing HTML instead of serving other users' streams. Pages under about 10 KiB are marginally slower under lxml due to fixed setup cost, which is worth nothing either way.

## Output verification

The risk in changing parser is silently different extracted text, so that was measured rather than assumed. Across all 37 real pages:

- **Zero characters of text were lost.** Every diff opcode against html.parser output was an insertion. Not one page dropped content under lxml.
- 659 characters were added, all on one page (docs.docker.com), where an inline Alpine.js `@click` handler containing a regex confuses libxml2's attribute handling and leaks a 73-character JS fragment into the text nine times. That is 659 characters of script noise in 27,206 characters of extracted text, with no content affected.
- Metadata (`title`, `description`, `language`) was identical on 35 of 37 pages. The two exceptions are 141-byte Wikipedia bot-block stubs with no `<html>` element, where lxml's fragment auto-wrapping adds `language: "No language found."`. Both parsers extract the same text from them.

Large documents were checked separately because libxml2 carries internal size caps. A 12 MiB single text node, 12 MiB spread across 400k nodes, a 3 MiB attribute value and 50k sibling elements with a trailing marker all produced byte-identical text under both parsers, with no truncation.

Malformed markup was checked too. lxml and html.parser diverge on unterminated comments, bare CDATA and duplicated `<html>` elements, all cases where both parsers are guessing and neither is correct. None of those shapes appeared in the 37 page corpus.

`backend/open_webui/env.py:184` also uses `html.parser`, on the local CHANGELOG at import time. That is trivial input on a startup path and is deliberately left alone.
2026-07-26 18:19:09 -04:00
Paulo Rodrigues
9c352e37b8
i18n: align the pt-PT locale with European Portuguese conventions (#27408)
The European Portuguese (pt-PT) catalogue mixes Brazilian and European forms
for the same concepts. In every case below the file ALREADY uses the European
term elsewhere, so these read as inconsistencies within the locale.

Corrects 74 string values. No keys are added, removed, renamed or reordered,
and no empty strings are filled (982 before, 982 after).

Genuinely Brazilian forms (Priberam tags these [Brasil] explicitly):
  arquivo -> ficheiro   (file sense only; Priberam: "Equivalente no portugues
                         de Portugal: ficheiro")
  usuario -> utilizador (Priberam sense 3: "[Brasil] ... = UTILIZADOR")
  acessar -> aceder     (Priberam: entire entry tagged [Brasil])
  equipe  -> equipa     (Priberam: "Grafia em Portugal: equipa")
  midia   -> multimedia (Priberam: every sense [Brasil])
  "em um" -> "num"
  "esta digitando" -> "esta a digitar": European Portuguese prefers
    estar a + infinitive over estar + gerund (Microsoft pt-PT Style Guide p.34)

European UI convention, where the file is inconsistent with itself. These
words are NOT Brazilian; the change is for consistency, not correctness:
  senha -> palavra-passe (0 vs 12 in this file)   salvar -> guardar (3 vs 19)
  compartilhar -> partilhar (6 vs 29)             excluir -> eliminar (4 vs 19)
  conexao -> ligacao (3 vs 27)                    gerenciamento -> gestao
  desenvolvedor -> programador
  Settings -> "Definicoes" (Settings label sense only, 15 vs 3)
  the explicit pronoun "voce" -> European formal verb forms

Outright defects:
  "Confirme sua a nova palavra-passe" -> "Confirme a sua nova palavra-passe"
  "e sua especificacao" -> "e a sua especificacao" (missing article)
  "Compartilhamento Publico" -> "Partilha Publica" (gender agreement)
  "Archive" -> "Arquivar", not "Arquivo". All four call sites are button
    labels/tooltips invoking archiveChatHandler(), so this is an action and
    needs the verb, matching its siblings "Archive All" -> "Arquivar Tudo".

Deliberately NOT changed:
  - 12 strings whose English key says "Config"/"configuration". "Configuracao"
    is correct European Portuguese; only the Settings label becomes
    "Definicoes".
  - "You" -> "Voce" (the chat label), the string referencing that label, and
    the default system prompt.
  - "confiavel" is left alone: Priberam gives it no regional label and
    professionally translated pt-PT (GNOME) ships it.
2026-07-26 18:18:39 -04:00
Classic298
18d004cabe
chore: drop python-jose, nothing imports it (#27444)
The migration to joserfc completed the job but left the old dependency pinned. `python-jose` now has zero imports anywhere in the backend: the only `jose` references left are `joserfc` in `utils/oauth.py`, and a repo-wide search for `from jose`, `import jose` or `python_jose` returns nothing outside the three pin files.

Removing it also removes `ecdsa` and `rsa` from the image, which were pulled in only by python-jose. `uv lock` confirms that: it drops exactly those three packages and nothing else, because google-auth 2.55 depends on cryptography and pyasn1-modules rather than rsa. That is worth having beyond the size saving, since `ecdsa` ships a documented Minerva-style timing side-channel in its P-256 signing path that upstream has declined to fix, so keeping it in the image means shipping a flagged crypto library that nothing calls.

Verified by blocking the `jose` module at import time and importing the backend anyway:

```
PASS  import open_webui.utils.auth
PASS  import open_webui.utils.oauth
PASS  import open_webui.main
jose in sys.modules: False
PASS  create_token/decode_token round trip
```

One user-visible consequence worth stating: Tools and Functions run in the same interpreter, so a third-party plugin that imports `jose` directly stops working after this. Nothing in Open WebUI itself does, and PyJWT remains a dependency, but a plugin relying on a library the application never declared for that purpose is the only thing this can break.

`uv.lock` was edited surgically rather than regenerated, to avoid the unrelated whole-file churn a newer uv version introduces. The result was diffed against real `uv lock` output and matches it exactly apart from that version's cosmetic fields.
2026-07-26 18:18:17 -04:00
G30
c9c97835bf
fix(ui): keep dropdowns positioned and stable as their content resizes (#27460) 2026-07-26 18:17:53 -04:00
G30
527b0d3b7f
fix(ui): stop the chat input attach menus from fetching twice on open (#27461) 2026-07-26 18:14:06 -04:00
Classic298
e140d8f3cc
fix: scope timer cancellation to the timer's owner (#27472)
The events:chat socket handler called the ownership-checked update for last_read_at, discarded the boolean it returns, and then cancelled the chat's pending timers regardless of the answer. cancel_timers_for_chat selected on the internal marker, the type, the parent chat id and the status, and never on the owner, so it matched rows belonging to any user. An authenticated user who knew another user's chat id could mark that chat read over their own socket session and silently cancel the owner's pending timers, and the owner got no notification: the scheduled action simply never fired.

The missing owner predicate also cut the other way in ordinary use. Because the query matched every timer sharing a parent chat id, one user reading a chat cancelled the timers of anyone else holding one on the same chat, so this was collateral damage as much as an attack.

cancel_timers_for_chat now requires a user_id and filters on it, which is the durable fix, and the socket handler returns early unless the ownership-checked update reports that the caller owns the chat. The parameter is required rather than defaulted so a later caller cannot reintroduce the unscoped query by omission. Both existing call sites already know the acting user. Timer rows are created with the same owner as the parent chat and the execution path already refuses to run one whose owner does not match, so scoping the cancellation the same way cannot strand a timer that would otherwise have fired.

One behaviour change worth noting: an administrator posting into another user's chat no longer cancels that user's chat.user_message timers, because the acting user is the administrator. The timer fires instead of being cancelled, which is the safe direction.
2026-07-26 18:12:44 -04:00
Classic298
a15e44a5ff
refac: use MilvusClient instead of deprecated ORM-style PyMilvus APIs (#27521)
* refac: use MilvusClient instead of deprecated ORM-style PyMilvus APIs

PyMilvus 2.6 emits a PyMilvusDeprecationWarning for every ORM-style call (`connections.connect`, `utility.*`, `Collection` and its methods) and will remove those APIs in PyMilvus 3.1. Both Milvus backends still used them, so a running instance floods its logs with deprecation warnings during indexing and retrieval, and would break outright once PyMilvus 3.1 lands.

Both vector clients now go through `MilvusClient`:
- `milvus_multitenancy.py`: collection creation, index creation, has_collection, insert, search, query iteration, delete and reset.
- `milvus.py`: the remaining ORM calls in `query()` (`connections.connect`, `Collection(...).load()`, `Collection.query_iterator`), plus the now-unused `FieldSchema` import.

Behaviour is unchanged: same schema, same index parameters and the same two-step scalar-index fallback, same filter expressions, same result shapes. Verified against embedded Milvus (milvus-lite, pymilvus 2.6.14) with a functional harness over both clients: insert, get, query by string/int/bool metadata filters, vector search, tenant isolation, oversized-text truncation, delete by id and by filter, delete_collection and reset all return identical results before and after, while the deprecation warnings drop from 57 to 0 for the multi-tenancy client and from 16 to 0 for the standard one.

One Milvus Lite nuance worth recording: `MilvusClient` sends index build parameters (`M`, `efConstruction`, `nlist`) as flat keys rather than as a nested `params` blob. A Milvus server accepts both forms, Milvus Lite only reads the nested one, so those tuning values are ignored on Lite. `MilvusClient` offers no way to send the nested form, and `milvus.py` already built its index parameters this way, so both backends are now consistent.

Fixes #26978

* refac: correct the Milvus scalar-index comment

The comment claimed that embedded Milvus Lite requires an explicit scalar index type. It does not: Milvus Lite rejects `create_index` on a VARCHAR field outright ("create_index only supports vector fields"), for every index type and with or without a metric type, so neither the parameterless call nor the explicit INVERTED fallback can succeed there. Filtered queries on `resource_id` still work on Lite, just unindexed.

Only the accurate half is kept, which is the reason the parameterless call is deliberate rather than an omission.
2026-07-26 18:12:08 -04:00
Classic298
f2ff310b2a
fix: gate gray-500 muted text contrast behind High Contrast Mode (#27556)
Follow-up to #27497, reopened as a high contrast mode change, and the third and last of the contrast set after #27554 and #27555.

The grey scale in src/tailwind.css is achromatic oklch(L 0 0), so relative luminance is exactly L³ and text-gray-500 is 2.77:1 on white, against the 4.5:1 required by WCAG 1.4.3 and the 3:1 required of icons by 1.4.11. Around 290 sites use text-gray-500 dark:text-gray-400 for secondary labels, descriptions, counters and icons. Dark mode already passes at 6.46:1 and is left alone.

Rather than rewriting the class literal at every site, the remap is two CSS rules that only apply when the existing High Contrast Mode setting is on, so the default theme is untouched. Light mode resolves to gray-600 (5.75:1).

The split is not cosmetic. The `text-gray-500` utility is overridden inside `@layer utilities` with `:where()` so the rule sits below `hover:text-gray-*` and `dark:hover:text-gray-*` in specificity and hover feedback keeps working. The `.app-muted`, `.app-icon-muted` and `.tiptap table` classes in src/app.css are declared unlayered, which means no layered rule can reach them, so their override is unlayered too. They are `@apply text-gray-500 dark:text-gray-400` and fail identically, so leaving them out would have left the slash command menu and tiptap tables below 4.5:1 with the setting on.

Verified in a browser against Tailwind's emitted rules and layer order: with the setting on, the utility, .app-muted and .tiptap table all resolve to gray-600 in light while a focusable element carrying hover:text-gray-700 still resolves to gray-700 on interaction; dark mode and the setting-off case are unchanged in both themes.

One site is deliberately left out: EmbeddedChatHistoryDropdown.svelte uses text-gray-500/70, a separate class token that the selector does not match.
2026-07-26 18:11:47 -04:00
Timothy Jaeryang Baek
0e0d08382a refac 2026-07-26 18:11:12 -04:00
Timothy Jaeryang Baek
1f0dc90abe refac 2026-07-26 18:06:03 -04:00
Classic298
e3cce68ef2
fix: gate placeholder contrast fix behind High Contrast Mode (#27555)
Follow-up to #27496, reopened as a high contrast mode change.

Placeholder text is the lowest contrast text in the product. The grey scale in src/tailwind.css is achromatic oklch(L 0 0), so relative luminance is exactly L³, and placeholder:text-gray-300 is 1.58:1 on white against the 4.5:1 required by WCAG 1.4.3. The large text exemption does not apply, the largest of these is text-lg. Placeholders are frequently the only format hint a field gives, for example admin/Settings/General.svelte uses e.g.) "http://localhost:3000".

Rather than deleting the ~200 per-component placeholder utilities and rewriting the base rule, the remap now happens in two CSS rules that only apply when the existing High Contrast Mode setting is on, so the default theme is untouched:

- placeholders resolve to gray-600 (5.75:1) in light mode
- placeholders resolve to gray-500 (6.46:1) in dark mode

The rules sit in `@layer utilities` and are anchored on `input`/`textarea`, which puts them above both the base rule in src/tailwind.css and every per-component `placeholder:text-*` utility, so no call site has to change. Placeholders stay distinguishable from real input values, which are text-gray-700 (8.46:1) in light and dark:text-gray-300 (11.39:1) in dark.

The chat composer placeholder is a tiptap ::before, so neither the base rule nor any utility reaches it. It is hardcoded #676767, which is 5.66:1 in light but only 3.17:1 on the dark canvas, so only the dark side is remapped, to gray-500. That rule stays outside the layer because the rule it overrides is unlayered too.

The root layout toggles a `high-contrast` class on documentElement from `$settings.highContrastMode`, alongside the existing theme classes, so every route is covered and the class is removed again when the setting is turned off.

Verified in a browser against Tailwind's emitted rules and layer order, on inputs both with and without per-component placeholder utilities: with the setting on, placeholders resolve to gray-600 in light and gray-500 in dark, the composer placeholder resolves to gray-500 in dark even with the prefers-color-scheme override treated as unconditional, and with the setting off nothing changes in either theme. Also checked against the oled-dark theme (gray-500 on #000 is 7.57:1) and the dark:bg-white/[0.03] input surface (6.01:1).

Note: the `high-contrast` class toggle is the same hunk as in the muted text contrast branch. Whichever lands first, the other rebases cleanly by dropping it.
2026-07-26 18:02:57 -04:00
Timothy Jaeryang Baek
7bfc4bb2c2 refac 2026-07-26 18:02:33 -04:00
Timothy Jaeryang Baek
0671b7aa2b refac 2026-07-26 17:58:01 -04:00
G30
202f47ece8
fix(ui): stop sidebar chat rows flickering while the pointer moves across them (#27474) 2026-07-26 17:57:44 -04:00
Kylapaallikko
8c9c64250d
Update fi-FI translation.json (#27469)
Added missing translations.
2026-07-26 17:57:00 -04:00
Classic298
076a84e3f0
fix: enforce automation limits in the builtin automation tools (#27523)
The `create_automation` and `update_automation` builtin tools wrote straight to `Automations.insert` / `Automations.update_by_id`, skipping the limit checks that `/api/v1/automations/create` and `/api/v1/automations/{id}/update` run through `check_automation_limits`. A non-admin user could therefore ask the model to create automations indefinitely, ignoring `AUTOMATION_MAX_COUNT`, and could schedule them below `AUTOMATION_MIN_INTERVAL`, on both create and update.

Both tools now call the same `check_automation_limits` helper the routers use, so the limits and the admin bypass cannot drift between the chat path and the HTTP path. A rejection is returned to the model as a plain error message instead of raising. `update_automation` also gained the missing user lookup guard, since the helper needs the user's role.

The `automations.enable` toggle and the `features.automations` user permission were already enforced when the tool set is assembled, so they are unaffected.

Fixes #27121
2026-07-26 17:55:56 -04:00
Classic298
3ce734c6c6
fix: bump uvicorn to 0.51.0 to move off the legacy websocket implementation (#27553)
Uvicorn's `--ws auto` selected its `websockets_impl` protocol on 0.41.0, which is built on `websockets.legacy`. That module raises `AssertionError` in `_drain_helper` during keepalive pings and kills the websocket connection. Each crash runs the Socket.IO `disconnect` handler and drops the session from `SESSION_POOL`, so every subsequent server-to-browser call fails. The most visible symptom is the Pyodide code execution tool, which reaches the browser through `sio.call('events', ...)` and returns `{"stderr": "Client session disconnected."}` on every run.

Uvicorn 0.50.0 changed `--ws auto` to select the sans-io implementation whenever websockets is installed, and deprecated the legacy one. Bumping the pin therefore fixes this on every launch path at once, without adding a `--ws` flag to the startup scripts. Doing nothing is not stable either: websockets is unpinned apart from uvicorn's own `>=13.0` floor, and `websockets.legacy` is removed outright in websockets 17, which turns the current AssertionError into an ImportError on a fresh install.

Bumping to 0.51.0 rather than the minimum 0.50.0 also picks up the sans-io keepalive pings added in 0.44.0, so raw websocket endpoints keep the idle-timeout behaviour they have today behind a reverse proxy. Uvicorn 0.51.0 drops colorama from its `standard` extra and raises the httptools floor to 0.8.0, which the lockfile already satisfies.

Verified on the bumped pin: the backend boots, `/health` returns 200, `--ws auto` resolves to `WebSocketsSansIOProtocol`, a Socket.IO client completes a websocket handshake against the running app, and a bidirectional `sio.call` round trip succeeds. The unit test suite reports an identical 2273 passed / 7 failed on 0.41.0 and 0.51.0, with the 7 failures unrelated to uvicorn.

Fixes #27550
2026-07-26 17:55:44 -04:00
Classic298
1e0ab84717
fix: unshadow the time module so the web loader rate limiter can sleep (#27528)
`from datetime import datetime, time, timedelta` shadows the `time` module, so `RateLimitMixin._sync_wait_for_rate_limit` calls `datetime.time.sleep` and raises `AttributeError: type object 'datetime.time' has no attribute 'sleep'` whenever it actually has to wait.

Every synchronous loader path that paces requests hits this. `SafeFireCrawlLoader.lazy_load` calls the limiter directly, and Tavily, Microsoft Web IQ and Playwright reach it through `_safe_process_url_sync`. The exception is raised inside their per-URL `try`, so with `continue_on_failure=True` (the default) the URL is logged as a per-URL failure and dropped instead of being scraped. This is live by default: `WEB_LOADER_CONCURRENT_REQUESTS` is passed as `requests_per_second` and defaults to 10, so any URL whose predecessor finished within 100ms takes the sleep branch and is lost. Tavily and Microsoft Web IQ report it as "SSL verification failed", which points at the wrong cause.

`_wait_for_rate_limit` uses `asyncio.sleep` and is unaffected, but `SafeMicrosoftWebIQLoader.alazy_load` runs `lazy_load` in a threadpool, so its async entry point is affected too.

`datetime.time` is not used anywhere in the file, so importing the `time` module instead is enough.

The per-URL `continue` half of #26079 landed in 6f8221df5, which also added the `_sync_wait_for_rate_limit()` call to the Firecrawl loop. This makes that call work rather than throw.

Fixes #26079
2026-07-26 17:55:28 -04:00
Timothy Jaeryang Baek
421834b2de refac 2026-07-26 17:51:49 -04:00
Classic298
c882222f68
fix: verify chat ownership on /api/chat/completed and /api/chat/actions (#27486)
Both routes read `chat_id` from the request body and passed it into `get_event_emitter` without checking the caller owns that chat. The emitter persists through `upsert_message_to_chat_by_id_and_message_id`, which resolves by primary key and takes no owner argument, so an invoked filter or action wrote into whichever chat the caller named. `/api/chat/completions` already performs this check; these two routes did not.

Adds `verify_chat_ownership`, called at the top of both handlers. It runs before the existing try block because the `except Exception` there catches HTTPException and would rewrite the 404 into a 400. Admins are exempt, matching the completions path, so deliberate cross-user operations keep working.

`local:` chat ids are allowed through: they are per-socket, the emitter suppresses database writes for them, and the socket emit targets the caller's own room. `channel:` chat ids are rejected instead. They reach the channel emitter, whose write only checks that the message belongs to the channel and never that the caller may write it, and the membership and write-access gate for channels exists solely on `/api/chat/completions`. No caller sends a `channel:` id to these two routes: the only frontend callers are in the regular chat UI, and the backend channel path dispatches through the completions handler.

Co-authored-by: manus-use <213290975+manus-use@users.noreply.github.com>
2026-07-26 17:39:11 -04:00
G30
878cebac07
fix(ui): persist ollama connection deletions immediately (#27483) 2026-07-26 17:38:49 -04:00
Classic298
a0ee66c145
fix: name the group permission switches (WCAG 4.1.2) (#27513)
`admin/Users/Groups/Permissions.svelte` contains **64** `<Switch>` instances and not one of them passes `ariaLabel`, `ariaLabelledbyId` or `id`. bits-ui renders the switch as a `<button role="switch">` whose subtree is a text free thumb, so all 64 have **no accessible name**. The visible label is a sibling `<div>` with no association to the control.

This is the worst remaining case in the admin area: 64 toggles in one dialog, many with near identical adjacent labels (Import Models / Export Models / Import Prompts / Export Prompts / Import Tools / Export Tools). A screen reader user hears 64 consecutive "switch, on" and "switch, off" with no way to tell which permission is which.

Breaks WCAG 4.1.2 Name, Role, Value (Level A).

Fix: pass the row's own label to each switch. The `ariaLabel` expression is the **same `$i18n.t()` key** as the visible text two lines above it, so the accessible name equals the visible label in every locale, which also satisfies 2.5.3 Label in Name and keeps voice control working.

`ariaLabel` rather than `ariaLabelledbyId`, which is what `chat/Settings/Interface.svelte` uses for the same row shape. The difference is that `Interface.svelte` is a singleton, whereas this component is rendered from `EditGroupModal`, which is instantiated in three places including once per group in `GroupItem.svelte`. Only one can be visible today, but nothing enforces that, and 64 hardcoded ids would fail silently the day two coexist, since `aria-labelledby` resolves to the first matching id. `aria-label` has no such failure mode and needs half the edits.

All 64 mappings were checked individually rather than assumed. The nearest preceding label is the correct one in every case, including the three rows wrapped in a `<Tooltip>` (whose `content` attribute precedes the label in source order) and the ~60 `{#if}` / `{:else if}` explanatory strings (which always follow their switch). All 64 resulting labels are distinct.

The nested sub toggles are unambiguous on their own because upstream already labelled them fully ("Import Models" rather than "Import"), so no extra scoping is needed.

Two known follow ups, deliberately not bundled:

- The warning tooltips on Tools Access, Skills Access and Automations ("Warning: Enabling this will allow users to upload arbitrary code on the server.") are attached to a non focusable wrapper `<div>`, so keyboard and screen reader users never receive them. That needs a change in `common/Tooltip.svelte` or an `ariaDescribedbyId` on `Switch`, not a naming change.
- This file is a ~14 line block repeated 64 times where only the label and permission key vary, and it wants a shared `PermissionRow` component. Extracting it here would bundle a large structural refactor into an accessibility fix and make the diff unreviewable against the claim, so it is left alone.

The diff is +164/−65 rather than 64 changed lines, because 33 of the switches exceed the 100 column print width and Prettier reflows them to the multi line form. The file is Prettier clean and compiles with no new warnings.

Severity: Serious.

### 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-07-26 17:38:35 -04:00
Classic298
ba4c92c4f0
fix: make sidebar section headers keyboard operable (WCAG 2.1.1, 4.1.2) (#27489)
On latest `dev`, each sidebar section header in `Sidebar/Section.svelte` is a real `<button>` carrying `aria-expanded` and `aria-controls`, but it has **no activation handler**. The toggle comes only from `on:pointerup` on the wrapper inside `common/Collapsible.svelte`.

Keyboard activation dispatches a synthetic `click`, never `pointerup`, and that wrapper's own `on:click` handler calls `stopPropagation()`. So pressing Enter or Space on the header does nothing at all, while `aria-expanded` tells assistive technology this is a working disclosure control.

This affects every section in the sidebar: Models, Notes, Channels, Folders and Chats. Section state is persisted to `localStorage`, so a user whose section was collapsed on a previous visit has no keyboard way to open it again, and the content stays unreachable.

Breaks WCAG 2.1.1 Keyboard (Level A), and 4.1.2 Name, Role, Value (Level A), because the exposed expanded state belongs to a control that cannot be operated.

Fix: handle activation on the header button itself, where focus actually lands, and stop the now duplicate pointer path so a mouse click does not toggle twice. The existing inline `onChange` body is extracted to `setOpen` so the `change` dispatch and the `localStorage` write stay in one place and fire exactly once per toggle in both input modes. The adjacent "+" (`onAdd`) button already stops both `pointerup` and `click`, so it still does not toggle the section.

`Collapsible`'s wrapper cannot simply become a `<button>` instead, because its slot receives buttons from this component and others, so the fix belongs here.

`common/Folder.svelte` and `Sidebar/RecursiveFolder.svelte` have the same latent defect and are not touched by this PR.

Severity: Critical. Sidebar navigation cannot be expanded without a mouse.

### 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-07-26 17:38:06 -04:00
Classic298
15f724b0f2
fix: give SensitiveInput a unique default id (WCAG 1.3.1, 4.1.2) (#27488)
On latest `dev`, `SensitiveInput` defaults to `export let id = 'password-input'`. The id is used both for the input itself and as the `for` target of the screen reader label rendered just above it.

There are 80 `<SensitiveInput>` usages in `src/` and only 4 pass an explicit id, so the remaining 76 all render `id="password-input"` together with `<label for="password-input">`. These collide on the same page in completely ordinary configurations: `admin/Settings/Audio.svelte` renders 4 at once with `STT_ENGINE === 'openai'` and 4 more with `TTS_ENGINE === 'openai'`, `admin/Settings/Documents.svelte` has 11, and `admin/Settings/WebSearch.svelte` has 33.

`for` resolves to the first matching element, so every label after the first points at the wrong input. In practice a screen reader user tabbing to the OpenAI TTS API key field hears the label belonging to the STT key field from a different section, and every one of those fields announces the same name. Browser password managers and any `getElementById` lookup collapse onto the first element the same way.

Breaks WCAG 1.3.1 Info and Relationships (Level A), because the programmatic label/field relationship is wrong, and 4.1.2 Name, Role, Value (Level A), because the fields do not expose their correct accessible name.

Fix: default the id to a per instance unique value. A Svelte prop default is evaluated per component instance, so each `SensitiveInput` gets its own stable id, and the 4 call sites that pass an explicit id are unaffected. `uuid` is already a direct dependency and `import { v4 as uuidv4 } from 'uuid'` is the existing pattern in the codebase, including `common/Collapsible.svelte`, which already generates a DOM id this way.

Note for self hosted setups: a `#password-input` selector in `static/custom.css` would stop matching. That selector already matched up to 8 elements at once on the Audio settings page, so it was never a reliable hook.

Severity: Serious. Every API key field in Admin Settings is mislabelled for assistive technology.

### 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-07-26 17:37:45 -04:00
Classic298
d3802f7660
fix: make reasoning and details disclosures keyboard operable (WCAG 2.1.1, 4.1.2) (#27490)
On latest `dev`, the `title !== null` branch of `Collapsible` renders its header as a bare `<div>` whose only handler is `on:pointerup`, with the two Svelte a11y warnings suppressed above it.

`pointerup` is never dispatched by keyboard activation, and the `<div>` has no `role`, no `tabindex` and no `aria-expanded`. The header is therefore not focusable, not activatable and not announced as a control. This is the header for "Thinking..." / "Thought for N seconds", "Analyzing..." / "Analyzed", and every `<details>` block rendered from model output, via `Messages/Markdown/MarkdownTokens.svelte`, `Messages/StructuredOutputRenderer.svelte` and `chat/Controls/Controls.svelte`.

In practice a keyboard or screen reader user cannot expand any model reasoning trace, tool call detail or code interpreter block, and a screen reader reads the header as static text with no hint that anything is collapsed behind it.

Breaks WCAG 2.1.1 Keyboard (Level A), since the disclosure has no keyboard operation at all, and 4.1.2 Name, Role, Value (Level A), since it exposes neither a button role nor its expanded state.

Fix: render that header as a real `<button type="button">` with `aria-expanded` and the native `disabled` attribute, and toggle on `click`, which fires for both pointer and keyboard activation. This branch contains no `<slot />` and no interactive descendants, so a button is valid here. `block text-start` keeps the previous box and alignment behaviour, since a `<button>` otherwise defaults to `inline-block` and centred text. `disabled:cursor-default` replaces the old `{disabled ? '' : 'cursor-pointer'}` ternary, which became a no-op once this was a button, because `src/tailwind.css` applies `cursor-pointer` to every `button`. Verified in a browser that display, text alignment and rendered height match the previous `<div>`, and that a disabled header no longer shows a pointer cursor.

Switching from `pointerup` to `click` also means the header no longer toggles on right click, or when a drag starts outside it and ends inside.

The `{:else}` branch is deliberately left alone. Its `<slot />` receives buttons from `Sidebar/Section.svelte`, `common/Folder.svelte` and `Sidebar/RecursiveFolder.svelte`, so it cannot legally become a `<button>` and needs a different fix.

Severity: Critical. Model reasoning output is entirely unreachable without a mouse.

### 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-07-26 17:37:34 -04:00
Classic298
65473b6ffa
fix: stop Enter on Cancel from confirming ConfirmDialog (WCAG 3.2.2) (#27491)
On latest `dev`, `ConfirmDialog` registers `handleKeyDown` on `window` and treats **every** Enter keypress as Confirm, calling `event.preventDefault()` first so the focused control never gets its native activation.

The dialog also activates a focus trap with no `initialFocus`, so focus-trap falls back to the first tabbable node, which is the **Cancel** button. So the dialog opens with Cancel focused, and pressing Enter runs Confirm.

This is the confirm surface for Delete chat, Delete folder, Delete model, Delete knowledge base and ~40 other call sites. A keyboard user who tabs to Cancel and presses Enter deletes the thing they were trying to keep. Screen reader users are hit hardest, since they cannot see which button they are on and the control that means "back out safely" performs the irreversible action instead.

Two related paths have the same cause: Enter in the `input=true` textarea submits instead of inserting a newline, and a markdown link inside `message` (reachable via `eventConfirmationMessage` from tool `__event_call__` payloads, and via `web_search_confirmation_content`) becomes the first tabbable node, so Enter on that link confirms instead of following it.

Breaks WCAG 3.2.2 On Input (Level A): changing the focused control changes what the Enter key does, and activating a control performs a different action than the one it is labelled with. Also 2.1.1 Keyboard (Level A), since Cancel has no working keyboard activation.

Fix: let the focused control act on Enter itself, and only fall back to Confirm otherwise. Uses the same `target instanceof Element && target.closest(...)` guard already used in `Functions.svelte`, `Knowledge.svelte`, `Models.svelte`, `Prompts.svelte`, `Skills.svelte` and `Tools.svelte`. `select` is deliberately not in the list, because a native `select` does not act on Enter and excluding it would silently break confirm for the `inputType === 'select'` variant. Two stray `console.log` calls in the same function are removed.

Behaviour after this change: Enter on Cancel cancels, Enter on Confirm confirms, Enter in the textarea inserts a newline, Enter on a link follows it, and Enter anywhere else still confirms as before.

Severity: Critical. Silent, unrecoverable data loss triggered by the most ordinary keyboard interaction there is.

### 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-07-26 17:37:21 -04:00
Classic298
2725ae6d6c
fix: expose Checkbox as a checkbox with a name and state (WCAG 4.1.2) (#27494)
On latest `dev`, `common/Checkbox.svelte` renders a `<button type="button">` containing only `aria-hidden="true"` SVGs. It has no `role`, no `aria-checked` and no accessible name, and the component has no `$$restProps` spread, so a caller cannot supply a name either.

Assistive technology announces every one of these as an unnamed "button". A screen reader user cannot tell that the control is a checkbox, cannot tell whether it is on or off, and cannot tell what it toggles. The visible label is always an unassociated sibling element, for example `Capabilities.svelte` puts it in a preceding `<div>` with no `id`, and `Groups/Users.svelte` puts it in a different table cell from the checkbox.

Breaks WCAG 4.1.2 Name, Role, Value (Level A) on all three counts at once.

Fix: expose `role="checkbox"` and `aria-checked` on the control, add an `ariaLabel` prop, and pass the label text that is already in scope at each call site. `aria-checked` mirrors the component's existing icon logic exactly, so the indeterminate dash reports `mixed` rather than `false`. The `ariaLabel={ariaLabel || undefined}` shape matches the sibling `common/Switch.svelte`. Every label expression is the same one that renders the visible text next to the checkbox, so the accessible name always matches what is on screen.

Three call sites are deliberately left out of this PR, because they nest `Checkbox` inside another `<button>`, which is invalid HTML and independently broken:

- `workspace/Knowledge/KnowledgeBase.svelte` — the Checkbox's `on:change` sets `includeContent = true` and then the same click bubbles to the outer button, which flips it back with `includeContent = !includeContent`. Clicking the checkbox square is a no-op today, only the text label works. Giving it a confident name would advertise a control that does nothing.
- `workspace/common/MemberSelector.svelte` (two instances) — the inner Checkbox has no `on:change` at all and only works because its click bubbles to the row button. Naming it would create two focusable controls per row with the same name.

Both need the nesting resolved first, so that the row button carries the checkbox semantics. That is a behavioural fix and belongs in its own PR.

Severity: Serious. Affects model capabilities, default features, builtin tools, tool/filter/skill/action selectors and group membership.

### 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-07-26 17:37:06 -04:00
Classic298
a7a2c7605b
fix: give the rich text editor an accessible name (WCAG 4.1.2, 3.3.2) (#27503)
On latest `dev`, `RichTextInput` passes only `attributes: { id }` to tiptap, so the rendered contenteditable has an implicit `textbox` role and **no accessible name at all**.

The only label is the tiptap placeholder, which renders as CSS generated content in `src/app.css` via `content: attr(data-placeholder)`. Generated content never becomes an element's accessible name, so assistive technology announces the field as "edit text, blank".

This is the chat composer, the channel and thread composers, and the note editor, so it is the most used control in the product.

Breaks WCAG 4.1.2 Name, Role, Value (Level A), and 3.3.2 Labels or Instructions (Level A), since the only instruction is invisible to assistive technology.

Fix: expose the placeholder as `aria-label` on the editor element.

`attributes` is passed as a **function** rather than an object literal. The object form is evaluated once when the `Editor` is constructed and never rebuilt, but `placeholder` is deliberately runtime mutable: `channel/MessageInput.svelte` and `channel/Thread.svelte` swap it between "You do not have permission to send messages in this thread." and "Reply to thread..." once `channel` resolves, and it also changes when the interface language changes. With the object form the field would have been permanently named with whatever string happened to be set at mount, which for a channel the user *can* write to is the no-permission message. That would be worse than no name at all. ProseMirror supports the function form and re-evaluates it on every state update, and the component's existing `setPlaceholder` already dispatches an empty transaction, so the label now tracks the visible placeholder. It binds to `_placeholder`, the same value that feeds the visible text, so the two cannot diverge.

`aria-multiline` is deliberately not set. It is only valid on an explicit `textbox`/`searchbox` role, and adding `role="textbox"` would flatten the editor's inner structure so headings, lists and links inside rich text stop being exposed.

Severity: Critical. The application's primary input announces as an unnamed edit field.

### 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-07-26 17:36:36 -04:00
Classic298
7effaa05d1
fix: name the switches in Admin Settings rows (WCAG 4.1.2, 1.3.1) (#27510)
`common/Switch.svelte` already accepts `id`, `ariaLabel` and `ariaLabelledbyId`, but **not one of the 148 `<Switch>` instances under `src/lib/components/admin/` passes any of them**.

`admin/Settings/AdminSettingRow.svelte` renders the row label as a plain `<div>` and the control in a **sibling** slot, so there is nothing tying them together. bits-ui renders the switch as a `<button role="switch">` whose subtree is a text free thumb, so it has no accessible name from any source.

A screen reader user working through Admin Settings hears a long run of "switch, on" and "switch, off" with no indication of what any of them controls.

Breaks WCAG 4.1.2 Name, Role, Value (Level A) and 1.3.1 Info and Relationships (Level A).

Fix: `AdminSettingRow` mints a per instance id, puts it on the label element, and hands it to the default slot, so each row's switch can point at the label that is already rendered next to it. This is the pattern `chat/Settings/Interface.svelte` already uses by hand in 45 places, hoisted into the shared row component so call sites stop hand authoring ids.

`aria-labelledby` rather than a wrapping `<label>`: per HTML-AAM a `<button>` takes its name from `aria-labelledby`, then `aria-label`, then its own subtree, never from an associated `<label>`. `chat/Settings/Subagents.svelte` already wraps two switches in a `<label>` and they are still unnamed, which is the same trap. Using the existing label element also guarantees the accessible name is byte identical to the visible text, which keeps voice control working.

The `description` paragraph deliberately sits outside the referenced element, so verbose help text is not pulled into the name.

Scope: this covers the **72** switches that live inside an `AdminSettingRow`, which is every switch that flows through the shared row component. There are no rows containing more than one switch, so nothing is silently skipped.

The remaining 76 admin switches are not in this component and are not touched. 64 of them are in `admin/Users/Groups/Permissions.svelte`, which hand rolls its own row markup, and the other 12 are per entity toggles in lists and dropdowns where the label is a dynamic row name. `Permissions.svelte` is the worst remaining case, 64 toggles with near identical adjacent labels, and it needs either its own labelling pass or a conversion to `AdminSettingRow` that changes its visual styling. Either way that is not an accessibility only diff and belongs in its own PR.

All 12 touched files compile with the Svelte compiler with no new warnings and are Prettier clean.

Severity: Serious. Admin Settings is unusable with a screen reader.

### 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-07-26 17:35:34 -04:00
Classic298
94b1b7e6b6
fix: close Playwright pages and browser on failure in SafePlaywrightURLLoader (#27526)
`SafePlaywrightURLLoader` opened a new Playwright page for every URL and never closed it, and it only closed the browser after the URL loop finished normally. Pages therefore piled up for the whole batch, and any early exit (a raised error with `continue_on_failure=False`, or the caller abandoning/cancelling the generator mid-search) skipped `browser.close()` entirely.

With `PLAYWRIGHT_WS_URL` pointing at a remote Playwright server this leaks sessions on that server: navigation and route timeouts on slow or bot-protected pages leave pages and browser connections open until the server is restarted, which degrades every later web search.

Both `lazy_load()` and `alazy_load()` now scope the page to the per-URL loop body and the browser to the whole loop using their context managers, so each page is closed as soon as its URL is done and the browser is closed on success, on failure, and on cancellation. Closing a page also disposes the context implicitly created by `new_page()`. Exception handling is unchanged: a close error raised while `continue_on_failure` is set is still caught, logged, and the loop continues.

Fixes #25880
2026-07-26 17:35:13 -04:00
Classic298
225e238856
fix: only route PDFs and images to the PaddleOCR-VL loader (#27529)
When `RAG_DOCUMENT_LOADER_ENGINE` is set to `paddleocr_vl`, the dispatch branch in `Loader._get_loader` checked only the engine name and a non-empty token, so every uploaded file was handed to the PaddleOCR-VL loader regardless of its type. Text based uploads such as `.md`, `.txt` and `.csv` were base64 encoded and posted to the `/layout-parsing` endpoint tagged as PDFs, and the API rejected them with `422 Unprocessable Entity` ("PDFium: Data format error"), so those files never indexed at all.

The loader already knows which extensions it can handle: it tags images with `fileType: 1` and treats everything else as a PDF. That list is now a module level constant, and the dispatch branch gates on `['pdf'] + images`, the same way `mistral_ocr`, `datalab_marker`, `document_intelligence` and `mineru` already limit themselves. Deriving the gate from the loader's own list keeps the two in sync, so a file can never be admitted by the gate and then mislabelled as a PDF on the wire. Everything outside that set falls through to the default loader chain, so `.md` and `.txt` load as text, `.csv` through `CSVLoader`, `.docx` through `Docx2txtLoader`, and so on.

The branch also never checked `PADDLEOCR_VL_BASE_URL`. With the URL cleared, `PaddleOCRVLLoader` raised `ValueError` from its constructor and the upload failed outright instead of falling back. Both settings are now required for the branch to be taken, matching how the other engines guard their own configuration.

Fixes #24988
Fixes #26759
2026-07-26 17:34:59 -04:00
G30
4ac22b89fd
fix: don't wipe sidebar folder chat list when a refresh overlaps an in-flight fetch (#27535) 2026-07-26 17:28:15 -04:00
Classic298
f517cc7172
fix: apply the verified-user role gate to WebSocket authentication (#27537)
The Socket.IO handshake and the terminal WebSocket route each reimplement JWT authentication instead of going through the HTTP dependency chain. Both verified that the token decoded, that it had not been revoked, and that the user row existed, but neither applied the role check that `get_verified_user` enforces on every HTTP route, so any role outside `user` and `admin` was accepted.

That splits authorization across two planes. Deactivating an account by setting its role to `pending` takes effect immediately over HTTP, which returns 401, while the same JWT still opens a WebSocket. Changing a role disconnects the account's live sockets but does not revoke its token, so the client simply reconnects and gets a fresh session. Until the token expires, four weeks by default, a deactivated account keeps its channel rooms and can still read and write any note it holds an access grant on through the collaborative document handlers.

Resolve the user once, in `get_verified_user_by_token`, and route both WebSocket entry points through it. The role set moves into `VERIFIED_USER_ROLES` so the HTTP and WebSocket gates cannot drift apart, which is the underlying cause rather than either call site on its own. This also replaces five copies of the decode, revocation check and user lookup sequence.

`user-join` now resolves the user instead of reusing the identity cached in `SESSION_POOL`, which costs one extra query per handshake. Gating on the cached role would make the authorization decision depend on every future role-mutation path remembering to tear down the session pool, and that is precisely the invariant that failed here.
2026-07-26 17:27:54 -04:00
G30
29499cb4ba
perf: dedupe folder refetches and chat-list sweeps on sidebar folder selection (#27540) 2026-07-26 17:27:33 -04:00
G30
4d576c1aa2
chore: remove no-op stopPropagation statement from folder title click handler (#27542) 2026-07-26 17:27:12 -04:00
G30
65209b0235
fix: enforce a single open chat hover preview across sidebar chat items (#27549) 2026-07-26 17:26:49 -04:00
G30
ef197de0d7
fix: open settings deep links on client-side navigation and open Add Terminal settings directly (#27552) 2026-07-26 17:26:34 -04:00
Classic298
d1aa812d80
i18n: complete de-DE translations (#27448)
Some checks failed
Frontend Build / Format & Build (push) Has been cancelled
Frontend Build / Unit Tests (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
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
Python CI / Ruff Format (3.11) (push) Has been cancelled
Python CI / Ruff Format (3.12) (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_CUDA=true free_disk:true name:cuda suffix:-cuda]) (push) Has been cancelled
Create and publish Docker images with specific build args / merge (map[name:cuda suffix:-cuda]) (push) Has been cancelled
Create and publish Docker images with specific build args / merge (map[name:cuda126 suffix:-cuda126]) (push) Has been cancelled
Create and publish Docker images with specific build args / merge (map[name:main suffix:]) (push) Has been cancelled
Create and publish Docker images with specific build args / merge (map[name:ollama suffix:-ollama]) (push) Has been cancelled
Create and publish Docker images with specific build args / merge (map[name:slim suffix:-slim]) (push) Has been cancelled
Create and publish Docker images with specific build args / notify-helm-charts (push) Has been cancelled
Create and publish Docker images with specific build args / copy-to-dockerhub (, main) (push) Has been cancelled
Create and publish Docker images with specific build args / copy-to-dockerhub (-cuda, cuda) (push) Has been cancelled
Create and publish Docker images with specific build args / copy-to-dockerhub (-cuda126, cuda126) (push) Has been cancelled
Create and publish Docker images with specific build args / copy-to-dockerhub (-ollama, ollama) (push) Has been cancelled
Create and publish Docker images with specific build args / copy-to-dockerhub (-slim, slim) (push) Has been cancelled
* i18n: complete de-DE translations

Fill in all 544 untranslated (empty) strings in the German locale and add
the two keys that were missing entirely ("Response Auto-Scroll" and
"Follow assistant responses as they are generated.").

Wording follows the conventions already used in the file: formal "Sie"
address for user-facing sentences, infinitive phrasing for labels and
buttons, third-person descriptive phrasing for setting descriptions, and
the established terminology (Kontextverdichtung, Erinnerungen,
Wissensspeicher, Werkzeuge, Chunk, Embedding, Skills, Pipelines).
Ambiguous strings were resolved against their usage in the Svelte
components, e.g. "at"/"Through" (schedule and heatmap tooltips),
"Runs"/"runs" (automation runs vs. tool invocations), "Current"
(active chat) and "Selected" (model filter).

* i18n: fix de-DE wording and two pre-existing plural bugs

Review pass over the German locale:

- "Claim" and "DN" are masculine: "Claim, der ..." instead of "Claim,
  das ...", "Passwort für den Bind-DN", "Base DN, der ...".
- "hinzufügen" governs the dative, matching the existing string
  "... fügen Sie sie zuerst dem Arbeitsbereich "Wissen" hinzu."
- "Beschränkt oder schließt Domains ... aus" was a zeugma; the separable
  prefix only belongs to "schließt".
- Sub-agent settings render as label + input + unit suffix on one line,
  so the label and suffix no longer repeat each other.
- The built-in tool descriptions are infinitive, so the notification one
  is too.
- Align wording with terms already used in the file: Assistentennachrichten,
  Benutzernachrichten, Vervollständigungen, Tool-Server, Wissensspeicher,
  lexikalisch. Normalize the few German typographic quotes to the ASCII
  quotes used everywhere else.
- The username setting claimed the chat shows "Sie", but "You" is
  translated as "Du".

Also fixes bugs that predate these translations: "Starting in {{count}}
minutes" had the raw "minutes_one"/"minutes_other" suffix in its value,
and the singular and plural of "Ran {{COUNT}} analysis/analyses" were
swapped.
2026-07-24 17:59:54 -05:00
Timothy Jaeryang Baek
3110050aba refac 2026-07-24 12:49:17 -04:00
joaoback
9d293935a9
i18n: add pt-BR translations for newly added UI items and consistency pass (#27430)
New **pt-BR** translations for items introduced in the latest releases, plus a consistency/quality pass across existing strings (grammar, tone, capitalization, pluralization). Placeholders and hotkeys preserved. No logic changes.
2026-07-24 12:37:39 -04:00
Classic298
e32c6743ba
docs: align security policy framing with project ownership (#27431)
The security policy described Open WebUI as "a small volunteer team" and "a volunteer- and community-driven project", and explained response times as a shortage of capacity. Read by enterprise evaluators, security researchers and third parties trying to impose disclosure timelines, that wording makes the project look informal, under-resourced and externally steerable, which is the opposite of the position the policy is meant to hold.

Open WebUI is led and maintained by a small core team with clear ownership of the security process. This updates the wording to say that, and reframes response times as prioritisation across the project rather than a capacity shortfall. No rule, scope, commitment or timeline changes: the reporting channel, the disclosure schedule, the credit rules and the expected timeframe all stay exactly as they were.

Also removes the implicit first-come-first-served promise in the follow-up paragraph, which contradicted the severity-based prioritisation stated two paragraphs later, and bumps the last-updated date.
2026-07-24 12:37:23 -04:00
G30
06d2189b26
fix(chat): keep sidebar chat selection in sync with the active chat (#26977)
When navigating from a chat to a non-chat route (e.g. the admin panel),
the previously-viewed chat stayed selected in the sidebar and
deleting/archiving it wrongly redirected back to the new-chat page.
Cloning a chat also left the source chat highlighted alongside the new
clone, so two chats appeared selected at once.

Two independent sources kept the stale selection:

- The chatId store was never cleared when the Chat component unmounted,
  so $chatId still pointed at the last-viewed chat (this drove the
  delete/archive redirect). Clear chatId/chatTitle in Chat's onDestroy.
- The sidebar's optimistic selectedChatId highlight, set on click, was
  only cleared on window blur (hence it appeared to fix itself after a
  tab switch) and never followed programmatic navigation. Bind it to the
  chatId store so it tracks the active chat for leave, delete and clone.
2026-07-24 01:45:37 -05:00
Timothy Jaeryang Baek
300302d432 refac 2026-07-24 02:36:10 -04:00
Classic298
fe4b319428
fix: deny chained access to unregistered base models for non-admins (#26905)
A workspace model shared publicly could be used by any user even when its
base model was private. Unregistered base models (no row in the model
table) are admin-only for direct use — get_filtered_models hides them from
non-admins and check_model_access rejects them — but has_base_model_access
treated a missing row as "no ACL" and allowed the chained request through.

has_base_model_access now takes the caller's role and only allows an
unregistered base model hop for admins, so a shared preset can no longer
reach a base model the caller could not use directly. Registered base
models keep their existing grant-based enforcement.


Claude-Session: https://claude.ai/code/session_018toPfJW1hMXAhokGaL43Ep

Co-authored-by: Claude <noreply@anthropic.com>
2026-07-24 01:32:32 -05:00
Classic298
f7e7f32102
fix: honor Admin UI web loader settings in get_web_loader (#26749)
Since the config refactor, get_web_loader dispatched on the WEB_LOADER_ENGINE module constant, which is read from the environment once at import time. The engine selected in the Admin UI is stored under web.loader.engine in the config table but was never consulted, so UI-configured loader engines (external, playwright, firecrawl, tavily, microsoft_web_iq) were silently ignored and the built-in SafeWebBaseLoader always fetched pages directly. The same applied to the per-engine settings such as the external web loader URL and API key. This breaks egress-restricted deployments that rely on an external web loader: pages are fetched directly from the container and fail with errors like "Network is unreachable" even though an external loader is configured.

Pass the DB-backed loader settings into get_web_loader from both call sites, web search in process_web_search and web fetch via get_loader, and resolve every engine setting from them, keeping the module-level env constants as the fallback for keys that were never saved. Also initialise WebLoaderClass so an unknown engine raises the intended ValueError instead of an UnboundLocalError.

Fixes #26747
2026-07-24 01:30:47 -05:00
Classic298
18719fef9c
fix: malformed WEB_FETCH_FILTER_LIST entry blocking all web fetches (#26910)
Docker compose list-form environment syntax passes quotes through verbatim, so WEB_FETCH_FILTER_LIST="" reaches the backend as two literal quote characters rather than an empty string. Config parsing turned that into the filter entry '""', which has no "!" prefix and therefore landed in the allow list. A non-empty allow list requires every host to match one of its entries, and a quotes-only pattern can never match a hostname, so every fetch_url and web loader request was rejected with "URL blocked by filter list" and surfaced to the user as "The URL you provided is invalid".

get_allow_block_lists now strips surrounding quote characters from each entry and drops entries that are empty after normalisation. Quoted but otherwise valid entries such as "example.com" or !"example.com" now behave as their unquoted forms, and garbage entries no longer convert the default blocklist into a match-nothing allowlist that blocks everything.

Fixes #26908
2026-07-24 01:27:24 -05:00
Timothy Jaeryang Baek
b9d72741bb refac 2026-07-24 02:19:57 -04:00
Classic298
f89b501985
fix: access-check note entries in get_accessible_folder_files (#26739)
get_accessible_folder_files is the server-side filter that reduces a folder's attached-knowledge list (and, once #26723 lands, a direct model's) to the entries the caller may read, before that list is handed to the builtin knowledge tools as `__model_knowledge__`. It validated `file` and `collection` entries but passed `note` entries through unchecked (they fell into the `else` keep-as-is branch), even though notes are a first-class attached-knowledge type that flows through this list.

No current caller is exploitable, because every note consumer (`query_knowledge_files`, `view_note`, and the legacy retrieval path) independently re-checks note access before returning content. But relying on each consumer to remember that check is exactly the fragility this helper exists to remove, and the same `_has_read_access_to_file` membership short-circuit that makes an unvalidated `file` entry dangerous would turn any future note path that trusts list membership into an IDOR. Validate notes here so the filter enforces its own contract instead of leaning on downstream re-checks.

A note entry is now kept only when the caller owns it or holds a read grant. Notes are private by default and carry no self-grant, so ownership is checked explicitly alongside the grant lookup. Admins still bypass all checks and genuinely unknown types are still kept as-is.

Related: #26723
2026-07-24 01:18:43 -05:00
Timothy Jaeryang Baek
b4d13793a3 refac 2026-07-24 02:14:56 -04:00
Timothy Jaeryang Baek
d7513e4ce8 refac 2026-07-24 02:05:31 -04:00
Classic298
585b704597
fix: clear token cookie on 401 auth redirect to stop login flash loop (#26751)
Since v0.10.0 a global fetch interceptor redirects to /auth and clears
localStorage.token whenever an authenticated backend request returns 401.
The OAuth callback cookie ("token", set with httponly=False so the
frontend can read it) is left behind. The auth page's oauthCallbackHandler
then immediately signs the user back in from that cookie and navigates to
"/", where the next 401 triggers the redirect again. The result is an
endless /auth and / ping-pong that renders as uncontrollable screen
flashing, and as a PWA stuck on a flashing splash screen when SvelteKit's
update check turns each navigation into a full page reload. Affected
users could only recover by clearing cookies, which matches the reports.

Clear the token cookie together with localStorage when redirecting, so
/auth stays on the login form and the user can sign in again normally.

Fixes #26731
2026-07-24 00:59:43 -05:00
Classic298
e398ba3506
fix: don't seed non-persistent config keys (oauth.* with flag off) (#26928)
seed_defaults inserted a row for every key in DEFAULT_CONFIG regardless
of whether the DB is authoritative for it. With ENABLE_OAUTH_PERSISTENT_CONFIG
off, the oauth.* keys were seeded from the then-current (often empty) env
values. Enabling the flag later made those stale rows override live env vars
(e.g. ENABLE_OAUTH_SIGNUP=true stopped taking effect) and further env changes
were never picked up.

Skip keys where persistent_enabled_for() is false, matching the masking the
read paths (get/get_many/get_namespace/get_all) already apply.


Claude-Session: https://claude.ai/code/session_01Vr2RCYUTXCtgtV4WMUCK86

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 00:58:30 -05:00
Timothy Jaeryang Baek
28bdcb063b refac 2026-07-24 01:54:36 -04:00
Timothy Jaeryang Baek
793a43d9c4 refac 2026-07-24 01:48:56 -04:00
Timothy Jaeryang Baek
bd5d7b2e87 refac 2026-07-24 01:47:11 -04:00
Timothy Jaeryang Baek
212eec408c refac 2026-07-24 01:44:30 -04:00
Classic298
b6acd3cc45
fix: web speech STT repeating previous transcriptions and inserting text on cancel (#26793)
The VoiceRecording component stays mounted (hidden) between recordings, and the web speech engine accumulated every session's transcript into the never-reset transcription variable. Each new recording therefore confirmed all previous utterances again, so the inserted text repeated once per session and previously deleted text reappeared in the input.

Additionally, cancelling a recording (X button, Escape or a recognition error) called stopRecording(), which stops the SpeechRecognition instance and fires its onend handler, which unconditionally confirms the transcription. Cancelled recordings therefore still inserted the accumulated transcript.

Reset the transcription at the start of each web speech session and detach the onend handler on cancel so cancelled recordings no longer confirm.

Fixes #26784
2026-07-24 00:40:02 -05:00
Timothy Jaeryang Baek
ce831f7b85 refac 2026-07-24 01:25:52 -04:00
Timothy Jaeryang Baek
7b12fd677f refac 2026-07-24 01:23:39 -04:00
Timothy Jaeryang Baek
1f5b0d816f refac 2026-07-24 01:19:28 -04:00
Timothy Jaeryang Baek
33cf3fbb7f refac 2026-07-24 01:13:04 -04:00
Timothy Jaeryang Baek
ff11ff5a3e refac 2026-07-24 01:11:23 -04:00
Timothy Jaeryang Baek
cea991260f refac 2026-07-24 01:09:50 -04:00
G30
e212e3c7f4
fix(ui): resolve workspace model ID to base model on delete from model selector (#26819) 2026-07-24 00:03:31 -05:00
Classic298
a35b37adcd
fix: keep chats shared with an admin readable when ENABLE_ADMIN_CHAT_ACCESS is off (#27127)
get_chat_by_id sent admins down a branch that returned the chat only when
ENABLE_ADMIN_CHAT_ACCESS was on, or the chat was internal, and never fell
through to the access-grant and shared-folder checks. With the setting off,
an admin was therefore denied a chat that had been deliberately shared with
them, either directly or through a shared folder, while any non-admin holding
the same grant could open it. The admin role removed access the user had been
given rather than only closing the admin-only path.

Try the admin path first, then let everyone fall through to the grant and
folder checks. ENABLE_ADMIN_CHAT_ACCESS=false still closes the admin-only
route to other users' chats, and internal chats stay reachable.
2026-07-24 00:02:12 -05:00
Classic298
9b635d8f3d
fix: calendar attendee RSVP correctness — server-derived status and hide declined invites (#27007)
* fix: let only the attendee set their own calendar RSVP status

set_attendees took each attendee's status from the caller-supplied value, so an
event organiser could set another user's RSVP (for example to 'accepted') on
create or update. RSVP is meant to be self-service: the /events/{id}/rsvp
endpoint already scopes status changes to the calling user.

Derive attendee status server-side instead of from the request. An existing
attendee keeps the status they set via RSVP and a newly added attendee starts
'pending'; any caller-supplied status is ignored. Event edits no longer reset
attendees' existing responses.

Co-authored-by: legobattman <302282032+legobattman@users.noreply.github.com>

* fix: hide declined calendar invites from the attendee view

`get_events_by_range` surfaced every event where the user is an attendee regardless of their RSVP status, so declining an invite left it in the calendar with no way to remove it. Exclude `declined` attendee rows from the attendee branch, so a decline now removes the event from the user's own view while pending, accepted and tentative invitations still surface.

Co-Authored-By: legobattman <302282032+legobattman@users.noreply.github.com>

---------

Co-authored-by: legobattman <302282032+legobattman@users.noreply.github.com>
2026-07-24 00:01:16 -05:00
Classic298
48f78ca58d
fix: prevent startup crash when function/tool has null user_id (#26850)
The Function and Tool database columns declare user_id as a nullable
String column, but their Pydantic read-models required a non-null
string. A record with user_id NULL therefore raised a
pydantic ValidationError inside get_functions()/get_tools(), which run
during install_tool_and_function_dependencies() at app startup —
crashing the whole application and blocking all chat completions.

Make user_id Optional in the read/response models so such records
validate gracefully (user is already rendered as None downstream when
the id has no matching user) instead of taking down startup.


Claude-Session: https://claude.ai/code/session_01Y4RRUNq7ZUFkRWbWPkDw3m

Co-authored-by: Claude <noreply@anthropic.com>
2026-07-24 00:00:37 -05:00
Timothy Jaeryang Baek
799748b886 refac 2026-07-24 01:00:00 -04:00
G30
1dc4fd3e9d
fix(ui): hide redundant edit pencil on mobile in admin Models list (#27178)
On narrow viewports the admin Models list rows previously collapsed into
unusable vertical stacks; the Models page redesign on dev has since
absorbed the truncation fixes this branch carried (min-w-0 chain, real
truncate on the name, shrink-0 action group, inline access label).

The one remaining gap: the per-row edit pencil duplicates the row tap
(both open the model editor) while costing scarce horizontal space on
mobile. Hide it below the sm breakpoint; it remains on sm+ screens.
2026-07-23 23:58:07 -05:00
Viktor1
6ace7e5b3a
i18n: add Slovenian language support (#26875)
* i18n: add Slovenian language entry

* i18n: add Slovenian translation
2026-07-23 23:57:22 -05:00
Timothy Jaeryang Baek
5f6a9d16b2 refac 2026-07-24 00:56:43 -04:00
Timothy Jaeryang Baek
d637c2128c refac 2026-07-24 00:56:29 -04:00
Timothy Jaeryang Baek
239cb74007 refac 2026-07-24 00:56:25 -04:00
Timothy Jaeryang Baek
b35e2d265a refac 2026-07-24 00:54:03 -04:00
Timothy Jaeryang Baek
8cbb7f765c refac 2026-07-24 00:47:12 -04:00
Timothy Jaeryang Baek
de939a6562 refac 2026-07-24 00:44:06 -04:00
Timothy Jaeryang Baek
1e88367cc8 refac 2026-07-24 00:43:31 -04:00
Timothy Jaeryang Baek
4e869011cd refac 2026-07-24 00:42:11 -04:00
Timothy Jaeryang Baek
bef8ae4b2f refac 2026-07-24 00:40:42 -04:00
Timothy Jaeryang Baek
381ababeba refac 2026-07-24 00:40:21 -04:00
Classic298
ec18ce2ca0
fix: persist access_grants.allow_groups in default permissions (#27124)
AccessGrantsPermissions only declared allow_users, so allow_groups was
missing from the model backing the default user permissions endpoints.
Pydantic ignores undeclared fields, so POST /users/default/permissions
dropped allow_groups before model_dump(), it never reached the persisted
user.permissions config, and fill_missing_permissions restored it to the
default on the next read. Turning "Allow Sharing With Groups" off in
Admin Settings silently reverted to on, while the same toggle worked when
set per group, since group permissions are stored as a plain dict.

GET /users/default/permissions and /users/default/permissions/defaults
dropped it from their responses for the same reason.

Declare allow_groups on the model so it round-trips, matching the
access_grants block in DEFAULT_USER_PERMISSIONS. It defaults to True, so
existing payloads that omit it are unaffected.
2026-07-23 23:53:59 -04:00
Jan Behet
92b4361e7b
fix: correct typos in German translation (#26778)
This PR fixes two small errors in the German translation file:

- "kaann" -> "kann"
- "AAlle" -> "Alle"

No functional changes, only UI strings.

Co-authored-by: Tim Baek <tim@openwebui.com>
2026-07-23 23:52:35 -04:00
Classic298
c609ec4115
fix: require message authorship for standard-channel message edit and delete (#27197)
The channel message update and delete handlers enforced authorship only on group and dm channels. On standard channels the else branch accepted any caller holding write access on the channel, so a member who could post could also edit or delete messages authored by other members. Because the update form binds content, data and meta, and the model layer never touches message.user_id, an edited message kept the original author's attribution, so another member's message could be rewritten under their name.

Write access on a channel is the capability to post, not a moderation capability, and the frontend gates the edit and delete controls on authorship (message.user_id === user.id, or admin) for every channel type. The group and dm branch already encodes this with an explicit authorship check. Apply the same rule to the standard branch: the caller must hold write access on the channel and be the message author, unless they are an admin. Pinning is unchanged, since it is exposed to every member by design.
2026-07-23 23:40:03 -04:00
Classic298
c895490aa8
fix: blank chat messages on Safari caused by content-visibility virtualization (#26805)
Since v0.10.0 chat messages are virtualized with content-visibility: auto to skip rendering of off-screen messages. Safari's implementation of content-visibility has known paint bugs (WebKit bugs 277573, 281570 and 283846) that can leave the contents of a message unpainted even when it is on screen. On iOS this makes assistant responses render as empty, both in Safari and as a PWA, while the same chats render fine in Chromium and Firefox. This matches the regression window reported in #26712, which appeared when upgrading from 0.9.6 to 0.10.2.

Detect Safari (including all iOS browsers, which use WebKit) with the same user agent check already used in MessageInput and ShareChatModal, and skip the virtualization class there. Safari falls back to rendering all messages like before v0.10.0, while other engines keep the optimization. Verified with a spoofed Safari user agent that messages render without the virtualization class and with content-visibility resolving to visible, while Chromium keeps content-visibility: auto.

Fixes #26712
2026-07-23 23:38:18 -04:00
Classic298
b940cd529b
fix: matplotlib SyntaxError in sandboxed Pyodide code execution (#26800)
The sandboxed Pyodide host (used when ENABLE_PYODIDE_FILE_PERSISTENCE is
disabled, the default) embeds its script in a String.raw template. The
matplotlib show() override was written with '\\t' escapes as if in a normal
string context, but String.raw preserves them verbatim, so the iframe's
script parser turns them into literal backslash-t characters in the
generated Python source. Pyodide then fails to compile any code that
triggers the matplotlib patch with "SyntaxError: unexpected character
after line continuation character", which is why matplotlib only worked
with the file persistence worker path enabled.

Use single '\t' escapes instead: String.raw keeps them as-is in the
script text and the sandbox's JS parser produces real tab indentation,
matching the working implementation in pyodide.worker.ts.

Fixes #26660
2026-07-23 23:34:26 -04:00
Classic298
0f8d12201c
fix: empty assistant message content in action function body (#26798)
Assistant responses are now stored as structured output items on message.output, with message.content left empty. The action payload built in chatActionHandler still sent message.content only, so action functions received assistant messages with an empty content property. Derive the content from the structured output via getOutputText, falling back to message.content, matching how the rest of Chat.svelte resolves assistant text.

Fixes #26672
2026-07-23 23:34:14 -04:00
Classic298
7ef0530b24
fix: handle urllib3-future 4-element socket options in SSRF-safe web loader (#26796)
_ssrf_safe_new_conn unpacks each entry of self.socket_options straight into socket.setsockopt(), which accepts exactly 3 positional arguments. urllib3-future, a drop-in fork that shadows the urllib3 package whenever it is installed (for example as a dependency of niquests pulled in through a tool or function's requirements), declares its default socket options with a per-protocol 4th element: [(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1, "tcp")]. Its own _set_socket_options() strips that element before calling setsockopt(), but our override does not, so with urllib3-future present every synchronous web fetch (fetch_url, web search loading) fails on connect with "TypeError: setsockopt() takes exactly 3 arguments (4 given)" and returns empty content.

Mirror urllib3-future's handling in the override: for 4-element options whose last element is a protocol string, apply "tcp" options truncated to the first 3 elements and skip "udp" options (all sockets created here are SOCK_STREAM). Plain 3-element options, and any other shapes stock urllib3 would accept, are passed through unchanged, so behavior with stock urllib3 (which only ever uses 3-element tuples) is identical.

Verified locally: with urllib3-future installed the loader previously raised the TypeError on every URL and now fetches successfully; with stock urllib3 2.3.0 and 2.7.0 fetches behave the same before and after.

Note: #26015 reported this same crash but attributed it to stock urllib3 2.x, which only uses 3-element tuples; the 4-element form comes from urllib3-future shadowing urllib3.

Fixes #26791
2026-07-23 23:33:55 -04:00
Classic298
504e724fde
fix: detect bare pipe alternation as regex in grep_knowledge_files (#26795)
is_regex_pattern only recognized the BRE-escaped form \| and not a bare |,
so a pattern like "Jornak|Silverlake|Orissa" was treated as one literal
string (including the pipe characters) and silently returned no matches.
This contradicted the tool docstring, which explicitly advertises
"error|warn" as an auto-detected regex example, and misled models into
concluding the searched terms were absent from the file.

Checking for a bare | also covers the escaped form, since \| contains |,
and normalize_regex already converts escaped pipes before compilation.
Literal patterns without regex metacharacters are unaffected.

Fixes #26781
2026-07-23 23:33:35 -04:00
Timothy Jaeryang Baek
315a6b5995 refac 2026-07-23 23:16:28 -04:00
G30
866e8582d1
ci: auto-label bug reports created outside the issue form (#27256)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-23 23:07:25 -04:00
Timothy Jaeryang Baek
c3c70d4a7c refac 2026-07-23 22:56:02 -04:00
Timothy Jaeryang Baek
2ef6c76f51 refac 2026-07-23 22:52:23 -04:00
Timothy Jaeryang Baek
43e7eefa95 refac 2026-07-23 22:45:54 -04:00
Timothy Jaeryang Baek
40320c1136 refac 2026-07-23 22:44:04 -04:00
Timothy Jaeryang Baek
75a8a0046b refac 2026-07-23 22:36:03 -04:00
Timothy Jaeryang Baek
8d2fee5d45 refac 2026-07-23 22:35:54 -04:00
Jiwon Lee
9d020edf0f
fix: make native date/time picker icons visible in dark mode (#27275) 2026-07-23 22:23:46 -04:00
Timothy Jaeryang Baek
021c4c7a2e refac 2026-07-23 22:20:50 -04:00
G30
3132f11e55
feat: add setting to toggle keyboard shortcuts on/off (#27300) 2026-07-23 22:14:56 -04:00
G30
4f823774ab
fix: migrate deprecated authlib.jose import to joserfc in oauth utils (#27310) 2026-07-23 21:54:59 -04:00
G30
4fca375ad4
chore: remove unused ArchivedChatsModal component (#27308) 2026-07-23 21:54:45 -04:00
Classic298
acf586c006
fix: resolve the web loader parser per URL instead of locking in the first one (#27367)
SafeWebBaseLoader._unpack_fetch_results assigned the resolved parser to the parser parameter itself, so the None check only ran for the first URL. In a mixed batch every later document was parsed with whatever the first URL happened to select: an .xml feed first meant all following HTML pages went through the xml parser (broken text extraction), and an HTML page first meant .xml URLs were parsed as HTML. Web search regularly fetches mixed batches, so this silently degraded extraction quality depending on result order.

The parser is now resolved per URL; an explicitly passed parser still applies to the whole batch as before. Verified with mixed xml/html batches in both orders and with an explicit parser override.
2026-07-23 21:35:27 -04:00
Classic298
656a848043
perf: halve Redis round trips on model resolution and socket pools (#27225)
When WEBSOCKET_MANAGER=redis, app.state.MODELS and the socket session/
usage pools are Redis-backed dicts, so every membership test and
getitem is a network round trip:

- generate_chat_completion checked `model_id not in models` (HEXISTS)
  and then read `models[model_id]` (HGET) on every chat completion.
  A single .get() now serves both, with the same not-found error.
- The direct-connection branch spread the pool with `{**MODELS, ...}`,
  which iterates keys() then fetches each value — HKEYS plus one HGET
  per model. dict(MODELS.items()) issues a single HGETALL instead.
- get_user_ids_from_room called SESSION_POOL.get(sid) twice per
  session (once to filter, once for the value); the usage handler
  checked membership then fetched the same key. Both now do one
  lookup.

In non-Redis mode these are plain dicts and behavior is identical.


Claude-Session: https://claude.ai/code/session_01MHg5zs1VBjvRWQ54qHpfYD

Co-authored-by: Claude <noreply@anthropic.com>
2026-07-23 21:32:01 -04:00
Timothy Jaeryang Baek
429f2df50c refac 2026-07-23 21:29:33 -04:00
Classic298
484fb61743
perf: make streamed content accumulation genuinely linear (#27359)
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_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_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/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/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 / 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 (-ollama, ollama) (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 / Format & Build (push) Waiting to run
Frontend Build / Unit Tests (push) Waiting to run
The streaming handler's content accumulator is a closure cell (declared nonlocal in stream_body_handler), and CPython's in-place string append optimization only applies to plain local variables (STORE_FAST), never to cell variables (STORE_DEREF). The content += value form introduced in #27231 therefore still allocates and copies the full accumulated string on every delta, exactly like the f-string it replaced; whether that copy is cheap or expensive is up to the allocator, and measurements swing accordingly (6 to 83 ms of pure copying for a 400 KB response on Python 3.12, against 0.3 ms for this patch).

Accumulate the deltas in a list instead and join once at the single read site (publish_chat_finished_event at stream end). List append is amortized O(1) with no dependence on reference counts, bytecode specialization or allocator behaviour, so accumulation is O(n) by construction. The non-str fallback keeps the previous f-string coercion semantics.

Verified end to end against a mock SSE upstream: a streamed chat with a think-tag block plus 40 content deltas produces output items, message text, reasoning text and usage identical to current dev, with no errors in the server log.
2026-07-23 18:17:41 -05:00
Timothy Jaeryang Baek
9a49b271aa refac 2026-07-23 19:17:19 -04:00
Timothy Jaeryang Baek
f2dd88285a refac 2026-07-23 19:15:32 -04:00
Timothy Jaeryang Baek
858e9236df refac 2026-07-23 19:12:47 -04:00
Classic298
d0f759ce40
fix: capture uncompressed response bodies in audit logs (#27369)
CompressMiddleware was registered before AuditLoggingMiddleware. Starlette prepends on add_middleware, so the audit layer ended up outside compression and, at the REQUEST_RESPONSE level, recorded the zstd/brotli/gzip bytes of every response, decoded with errors='replace'. Any client that sent Accept-Encoding (i.e. every browser) therefore produced audit entries whose response_object was unreadable mojibake.

Registering the audit middleware before the compression middleware places it inside compression, so it observes the response body exactly as the route produced it while the client still receives the compressed stream.

Verified with a stacked ASGI harness: in the old order the captured body is not parseable; in the new order the captured body round-trips as the original JSON and the client response stays compressed.
2026-07-23 18:09:40 -05:00
Classic298
3d45947053
fix: report sub-second timings in the X-Process-Time header (#27368)
The header value was truncated with int(), so every request faster than one second reported X-Process-Time: 0 and the header carried no information for exactly the requests it is meant to describe. Emit fractional seconds with microsecond precision instead, matching the pre-ASGI-refactor behavior where the raw float was sent.
2026-07-23 18:09:23 -05:00
Classic298
e0918ddb40
perf: stop the audit middleware from re-running the whole auth pipeline (#27373)
With audit logging enabled, every audited request authenticated twice. The route dependency resolved the user once, and then _log_audit_entry called get_current_user again in the request's finally block: a second JWT decode, two more Redis revocation lookups, a second user row fetch with pydantic validation and, crucially, a second fire-and-forget last-active write transaction per request.

get_current_user now stashes the resolved user on the scope-backed request state (the same mechanism the auth middleware already uses for request.state.token), and the audit middleware reuses it, falling back to the old resolution only when no user was stashed (e.g. routes without an auth dependency). While in the file, the audit path patterns are compiled once in the constructor instead of per request, and the always-log endpoint set is a class attribute instead of a per-call literal; both are fixed for the process lifetime.

Benchmark:

| metric | before | after |
| --- | --- | --- |
| audit auth resolution, CPU floor (JWT decode + user validate only) | 16.7 us | 0.24 us |
| extra work per audited request | 2 Redis GETs + 1 user SELECT + 1 last-active write | none |

The before column understates the saving: it excludes the Redis and DB round trips listed in the second row, which dominate in real deployments.

Functionally verified with a stacked ASGI harness: when the route resolves a user the audit entry carries that user and the auth pipeline is not invoked again; without a stashed user the fallback path still resolves and logs correctly; the skip matrix (exclusions, whitelist mode, always-log auth endpoints, unauthenticated and non-audited methods) is unchanged.
2026-07-23 18:09:04 -05:00
Classic298
699d512e2f
perf: drop redundant session.refresh calls after commit across the model layer (#27381)
Both session factories run with expire_on_commit=False, so ORM objects keep their attribute values after commit. Every session.refresh issued right after a commit therefore re-SELECTed a row whose values the session already held, including full chat JSON blobs and user settings, purely to overwrite identical data. Fifty such calls existed across the model layer, covering nearly every write path in the app (chat inserts, title updates, pin/archive toggles, user role and settings updates, tool, prompt, function, model, file, tag, feedback, memory, automation and grant writes).

All fifty are removed. The only refreshes with an actual job were the two update-then-reload paths in tools and skills, where a Core UPDATE statement bypasses the identity map; those now use session.get(..., populate_existing=True), which guarantees a fresh row in one SELECT whether or not the row was already present in the session (the previous code issued get plus refresh, two SELECTs, on the default configuration).

Benchmark (real SQLite DB, per write):

| write path | before | after |
| --- | --- | --- |
| chat title update, ~600 KB chat blob | 2.08 ms | 1.24 ms |
| user role update, small row | 1.21 ms | 0.68 ms |

On Postgres each removed refresh is additionally a network round trip. The chat-blob case also skips re-parsing the entire JSON document per write.

Functionally verified against a fresh database: user insert, role and settings updates, chat insert (including the server-default meta column, which is always provided client-side), title update and pin toggle, tool insert and the Core-update reload path, tag insert and the prompt insert flow that pins version_id after history creation all return correct values and persist correctly.
2026-07-23 18:08:00 -05:00
Classic298
6b655689cc perf: cut repeated per-model work out of model list assembly
get_all_models runs on every models refresh and, without the base-models cache (off by default), on every /api/models request. Several of its costs multiplied by the model count for no reason:

- The active action and filter id sets were derived from get_functions_by_type, which loads full function rows including plugin source and validates them, only for the ids and is_global flags. A generalized column-only query now returns (id, is_global) tuples; the existing filter-specific helper delegates to it.
- Action priorities were computed inside the per-model sort key, constructing a pydantic Valves object per action per model; with global actions in every model's list that was models x actions constructions per refresh. Priorities are now memoized per action.
- Global action and filter item dicts were rebuilt per model from the same modules. The item lists are now built once per function and shallow-copied per model, keeping per-model dicts independent exactly as before (nested values were already shared).
- Deactivated base-model overrides were dropped with models.remove, a linear scan and shift per removal; removals are now collected and filtered out in one identity-based pass, preserving list.remove's exact object semantics.
- RedisDict.set fingerprinted the payload by serializing the already-serialized mapping a second time plus a sha256; a direct dict comparison against the last written mapping has the same skip semantics without re-serializing anything.
- /api/models did tag normalization and profile-image stripping for every model before access filtering discarded the invisible ones, and always evaluated a json.dumps debug f-string; the work now runs only on visible models and the debug line is gated on the log level. The duplicate-id dedup keeps its position before filtering so the effective-model semantics are unchanged.

Benchmark:

| metric | before | after |
| --- | --- | --- |
| model-cache fingerprint, 200 models | 45 us | 1.4 us |
| action priority Valves builds, 200 models x 4 global actions | 0.37 ms (800 builds) | 0.002 ms (4 builds) |
| function-table payload for id sets | full rows incl. source | (id, is_global) tuples |

Functionally verified: the column-only id query matches the full-row query for actions and filters including inactive exclusion, and the fingerprint skip logic writes on first set, skips identical payloads, updates plus deletes stale keys on change and clears on empty, against a scripted fake Redis.
2026-07-23 19:05:32 -04:00
Classic298
310ae91302
perf: make tools defer_content real and batch tool and knowledge access filters (#27387)
Tools.get_tools(defer_content=True) contained the literal dead statement "stmt = stmt": the deferral was a no-op, so every tools listing loaded the full Python source of every tool (five caller sites pass defer_content=True expecting the optimization: the tools list endpoints and the user and group permission overviews). The listing now selects every column except content, and ToolModel.content becomes optional to represent deferred rows; router projections are content-less response models, so nothing downstream reads the source on these paths.

On top of that, get_tools_by_user_id issued one grant query per non-owned tool and Knowledges.get_knowledge_bases_by_user_id did the same per knowledge base (the latter also sits inside per-file access checks). Both now resolve grants for all non-owned rows in a single get_accessible_resource_ids call, the same batch helper the model listing already uses.

Benchmark (real SQLite DB):

| metric | before | after |
| --- | --- | --- |
| tools listing, 33 tools x ~200 KB source | 5.13 ms | 3.18 ms |
| grant queries per accessible-tools call, N non-owned tools | N | 1 |
| grant queries per accessible-KBs call, N non-owned KBs | N | 1 |

The listing row scales with source size; on Postgres the deferral additionally avoids shipping every tool's source over the wire per listing, and each removed grant query was a real round trip.

Functionally verified: deferred listings match full listings field for field with content None, grants included and router projections working; access filtering returns exactly owned plus granted tools and knowledge bases and nothing for strangers; full (non-deferred) reads still carry the source.
2026-07-23 17:51:07 -05:00
Classic298
5b518cbe43
perf: list knowledge base file metadata without loading extracted file contents (#27386)
Knowledges.get_file_metadatas_by_id fetched full File rows, whose data column carries the entire extracted text of each document, validated each into a FileModel and then threw everything except id, hash, meta and the timestamps away. The function backs every knowledge base detail view and runs again after every file add or remove (eight call sites in the knowledge router), so rendering a filename list for a 50-file knowledge base parsed tens of megabytes of JSON per request.

The listing now selects exactly the five columns the response needs, joined through KnowledgeFile, mirroring the column-only helper that already existed in the files model for id-based lookups.

Benchmark (real SQLite DB, 50 files with ~200 KB extracted text each):

| metric | before | after |
| --- | --- | --- |
| KB file metadata listing | 14.2 ms | 1.20 ms |

The gap widens linearly with file size and count since extracted contents no longer get read, parsed or validated at all.

Functionally verified: output matches the old implementation field for field on all 50 files and an unknown knowledge base still returns an empty list.
2026-07-23 17:50:55 -05:00
Classic298
d67bc4ffcd
perf: batch the file access check queries (#27383)
has_access_to_file runs for every non-owner file GET, per RAG file check and per shared-chat or model-attached file. Its final step called Models.get_models_by_user_id, which issued one grant query per non-owned workspace model, so a single file check on an instance with M workspace models cost M grant queries plus a group query, with the deny path always paying full price. Its collection_name step listed every knowledge base the user can access (itself one grant query per knowledge base) just to scan the list for one id. And get_accessible_folder_files repeated the whole pipeline per folder entry, refetching the caller's group memberships every time.

Three changes, all using parameters and helpers that already exist:
- Models.get_models_by_user_id resolves grants for all non-owned models in one get_accessible_resource_ids call and accepts prefetched user_group_ids.
- The collection_name check fetches the one referenced knowledge base and performs a single owner-or-grant check with the already-resolved group ids, preserving the write-requires-owner guard exactly (including its short-circuit before any grant query).
- get_accessible_folder_files resolves group ids once and threads them through every per-entry check.

Benchmark:

| metric | before | after |
| --- | --- | --- |
| filter loop CPU, 300 workspace models (queries stubbed) | 47 us | 19 us |
| grant queries per file-access check, M workspace models | M | 1 |
| group membership queries per folder listing, F files | F | 1 |

The stubbed CPU row understates the win: each removed query in the other two rows was a real database round trip.

Functionally verified with stubbed accessors: owned plus granted models are returned with owned ids excluded from the batch query; model-attached file access resolves through the batched path; the collection_name path does one KB fetch and one grant check with no full listing; a missing KB falls through; write access via a KB still requires the KB owner to own the file and short-circuits before the grant query; folder listings fetch groups exactly once.
2026-07-23 17:50:08 -05:00
Classic298
d5f099a5d4
perf: cut per-request database session overhead (#27385)
Two independent sources of fixed per-request cost:

The async SQLite engine was created with pool_pre_ping=True. A pre-ping guards against server connections dropped by timeouts or restarts, which cannot happen to a local SQLite file; each ping still costs a hop into the aiosqlite worker thread plus a SELECT 1 on every connection checkout, and with session sharing off a single request checks out a connection for every model-layer call it makes. The Postgres engines keep their pre-ping, where it is actually protective.

CommitSessionMiddleware unconditionally ran ScopedSession.commit() plus remove() after every HTTP request. The scoped registry instantiates a session on first access, so on the vast majority of requests (which never touch the sync session, per the middleware's own docstring) this built a Session, opened and committed an empty transaction and tore everything down for nothing. The middleware now checks ScopedSession.registry.has() first: requests that used the sync session are committed and removed exactly as before, on success and on the rollback path alike, and idle requests skip the machinery entirely.

Benchmark (real SQLite database):

| metric | before | after |
| --- | --- | --- |
| user row fetch incl. session + connection checkout | 681 us | 514 us |
| idle-request sync session work (create + empty commit + teardown) | 12.3 us | 0.26 us |

The first row saves per model-layer call, not per request: a request making five DB calls saves the checkout ping five times.

Functionally verified: normal reads and writes work with pre-ping off; an idle request through the middleware leaves no sync session behind; a request that uses the sync session still gets committed and removed.
2026-07-23 17:49:48 -05:00
Classic298
dd514ee20b
fix: persist upstream streaming error lines by awaiting the message upsert (#27365)
The branch that normalizes plain JSON error lines from streaming upstreams (lines without the SSE data: prefix) called Chats.upsert_message_to_chat_by_id_and_message_id without await. The coroutine was never executed, so the error was never written to the chat and Python emitted a "coroutine was never awaited" RuntimeWarning instead. The frontend still received the error event, but after a reload the message showed no trace of the failure.

The parallel error-persist branch further down the same handler already awaits the call; this aligns the two.
2026-07-23 17:49:18 -05:00
Timothy Jaeryang Baek
e769f9ff4f refac 2026-07-23 17:53:13 -04:00
Timothy Jaeryang Baek
ec56022bc1 refac 2026-07-23 16:35:48 -04:00
Timothy Jaeryang Baek
892dc03151 refac 2026-07-23 16:35:01 -04:00
G30
e62e4eb9fe
fix(ui): contain archived chats and analytics content within the settings modal (#27306) 2026-07-23 13:34:35 -05:00
Timothy Jaeryang Baek
132a29fd1c refac 2026-07-23 14:29:30 -04:00
Timothy Jaeryang Baek
c8f2e09fdc refac 2026-07-23 14:29:27 -04:00
Timothy Jaeryang Baek
25faa19941 refac 2026-07-23 14:27:55 -04:00
Classic298
e64acf1c0a
perf: batch and deduplicate per-request DB reads in the chat middleware (#27223)
Several spots in the chat pipeline issued sequential single-key config
SELECTs, or fetched the same key twice back-to-back, on every request:

- chat_completion_tools_handler: task model default/external and the
  tools prompt template were four sequential Config round trips (the
  template was fetched twice). One batched Config.get_many now serves
  all of them.
- chat_completion_files_handler: the six RAG settings (top_k,
  top_k_reranker, relevance_threshold, hybrid_bm25_weight,
  enable_hybrid_search, full_context) were six sequential round trips
  inside the retrieval call. Batched into one get_many.
- Voice and code-interpreter prompt templates were each fetched twice
  within one conditional; fetch once and reuse. The code-interpreter
  engine was likewise fetched twice per execution.
- Skill resolution fetched the accessible-skills list, kept only the
  ids, then re-fetched each mentioned skill by id (N+1). Reuse the
  rows from the access query.

Value semantics are identical: get_many applies the same defaults as
the individual gets, and the pre-existing truthiness/empty-string
checks on templates are preserved exactly.


Claude-Session: https://claude.ai/code/session_01MHg5zs1VBjvRWQ54qHpfYD

Co-authored-by: Claude <noreply@anthropic.com>
2026-07-23 13:21:29 -05:00
Juan Calderon-Perez
ca2d7c9deb
feat: add LDAP group synchronization support (#27263)
* feat: expose LDAP group sync settings in admin config

LDAP group synchronization was already wired into the login flow but its
settings (group management, auto-creation, and the group attribute) could
only be set via environment variables. OAuth, by contrast, exposes its
group-mapping settings through the admin config API and UI.

Bring LDAP to parity:
- Add enable_group_management, enable_group_creation and
  attribute_for_groups to LdapServerConfig and LDAP_SERVER_CONFIG_KEYS so
  the /admin/config/ldap/server endpoint reads and persists them.
- Add a "Group Mapping / Auto-Create Groups / Group Attribute" section to
  the LDAP admin settings UI, mirroring the OAuth group-mapping controls.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TtCvvQ7dcadoufbRpCKcpe

* fix: harden LDAP group sync config and login flow

Address review findings on the LDAP group-sync settings:

- ldap_auth: move the auto-create-groups call inside the try/except that
  wraps group sync, so a group-creation error is logged instead of
  bubbling to the broad handler and failing the whole login.
- update_ldap_server: reject saving with group management enabled but an
  empty group attribute, which would otherwise make sync silently no-op
  (mirrors the existing required-field validation).
- Authentication.svelte: merge the LDAP server config response into the
  client defaults instead of replacing the object, so any key an older
  backend omits keeps its default value.

Note: the empty-directory-groups behavior was reviewed and already
matches OAuth (both skip removal when no groups are returned), so it was
left unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TtCvvQ7dcadoufbRpCKcpe

* fix: default blank LDAP group attribute to memberOf before save

The Group Attribute field advertises "Default to memberOf", but the
backend now rejects an empty group attribute when group management is
enabled. Fall back to the memberOf default client-side when the field is
left blank, so the advertised default holds and the save isn't rejected.
The backend validation remains as defense-in-depth for direct API calls.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TtCvvQ7dcadoufbRpCKcpe

* fix: initialize LDAP port default as null instead of empty string

The backend LdapServerConfig types port as `int | None`, but the frontend
initialized it to an empty string. If a save carried that default (e.g.
when the backend response omits port under version skew), Pydantic would
reject the empty string. `null` matches the model and is also what the
type="number" input yields when the field is empty.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TtCvvQ7dcadoufbRpCKcpe

* fix: parse LDAP group DNs correctly instead of splitting on commas

Group CN extraction split the DN on raw commas and sliced off "CN=",
which mangles any group whose name contains an escaped separator (e.g.
"CN=Sales\, EMEA,OU=...") into a truncated, wrong name that then fails to
match the intended Open WebUI group. Use ldap3's parse_dn to split the DN
respecting RFC 4514 escaping, and unescape the resulting value so the CN
matches what an administrator sees.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TtCvvQ7dcadoufbRpCKcpe

* chore: address review feedback on _unescape_ldap_dn_value

Trim the docstring and rename the loop index to a more descriptive name
(i -> pos) per review feedback on the group DN unescaping helper. No
behavior change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TtCvvQ7dcadoufbRpCKcpe

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-07-23 13:20:55 -05:00
Timothy Jaeryang Baek
0f82f40b70 refac 2026-07-23 13:49:49 -04:00
Timothy Jaeryang Baek
ca11bd90a7 chore: format 2026-07-23 13:41:16 -04:00
Timothy Jaeryang Baek
93bd05271c refac 2026-07-23 13:40:30 -04:00
Timothy Jaeryang Baek
3026ac64a2 refac 2026-07-23 13:36:35 -04:00
Classic298
dc4b828852
fix: correct async import hook that disables the code-interpreter module blocklist (#27245)
The middleware code-interpreter path defines `restricted_import` as `async def` and assigns it to `builtins.__import__`, which Python's import machinery calls synchronously. Calling an async function returns a coroutine without running its body, so the blocklist check never executes and `_real_import` is never called. When `CODE_INTERPRETER_BLOCKED_MODULES` is set, blocked modules are therefore not blocked, and every subsequent import inside the interpreter binds a dangling coroutine instead of the module, breaking legitimate imports as well.

Define the hook as a regular `def`, matching the working implementation in `tools/builtin.py`. A blocked top-level import now raises `ImportError`, and all other imports pass through to the real importer.
2026-07-23 12:33:44 -05:00
andrep2222
66bf96c62d
Forward user info headers to Mistral OCR API (#27253)
Mirrors the ENABLE_FORWARD_USER_INFO_HEADERS pattern already used by
the audio/TTS and external document loader integrations, so the
Mistral OCR backend can identify the requesting user the same way.

Co-authored-by: andrep <vpham@aut.ac.nz>
2026-07-23 12:33:19 -05:00
Timothy Jaeryang Baek
49abfbdd15 refac 2026-07-23 13:18:04 -04:00
Classic298
da7097565c
perf: deduplicate repeated config fetches in Ollama request handlers (#27226)
The per-request Ollama handlers (chat, generate, embed, embeddings,
and the OpenAI-compat completions/chat-completions/messages/responses
endpoints) fetched 'ollama.api_configs' up to three times and
'ollama.base_urls' separately within a single request — the .get()
default-argument pattern made the second api_configs fetch
unconditional, and get_api_key() triggered a third. Up to four
sequential SELECTs per request collapse to one.

A new get_ollama_connection_config() helper fetches base_urls and
api_configs together in one batched Config.get_many where both are
needed; handlers that only need api_configs fetch it once into a
local. Admin operations (pull/push/copy/delete) and the TTL-cached
model-list path are deliberately left untouched.

Resolution semantics (str(idx) key first, url-key legacy fallback,
same defaults) are unchanged.


Claude-Session: https://claude.ai/code/session_01MHg5zs1VBjvRWQ54qHpfYD

Co-authored-by: Claude <noreply@anthropic.com>
2026-07-23 12:48:55 -04:00
Timothy Jaeryang Baek
85664f650c refac
Co-Authored-By: Classic298 <27028174+Classic298@users.noreply.github.com>
2026-07-23 12:48:23 -04:00
Timothy Jaeryang Baek
f9107edeeb refac 2026-07-23 12:48:14 -04:00
Classic298
8ace4f0a8a
perf: stop recomputing static work in per-response ASGI middlewares (#27229)
SecurityHeadersMiddleware called set_security_headers() on every
response — 14 os.environ.get lookups plus a regex validation per
configured header, for values that are static for the process
lifetime. Compute the header list once at construction; when no
security env vars are set, skip wrapping send entirely.

RedirectMiddleware decoded and parse_qs'd the query string of every
GET, though it only acts on /watch?v= and ?shared= URLs. Add a cheap
path/substring precheck first; a false positive just falls through to
the previous full parse, so no redirect behavior changes.

Verified byte-identical responses (status, Location, header values)
against the previous implementations across redirect, passthrough,
and no-env cases.


Claude-Session: https://claude.ai/code/session_01MHg5zs1VBjvRWQ54qHpfYD

Co-authored-by: Claude <noreply@anthropic.com>
2026-07-23 12:29:01 -04:00
Timothy Jaeryang Baek
1513ddaf58 refac 2026-07-23 12:25:17 -04:00
Classic298
62491debfa
perf: linear content accumulation in the streaming response handler (#27231)
The streaming handler rebuilt the full accumulated response with
`content = f'{content}{value}'` on every content delta — a complete
string copy per chunk, making accumulation O(n^2) over the response
length. Use in-place `content += value` for the (universal) str case,
which CPython extends in place, keeping accumulation O(n); the
f-string fallback is preserved for non-str values so coercion
behavior is unchanged.

In the ENABLE_REALTIME_CHAT_SAVE branch, full_output() — which
concatenates the entire accumulated output — was called twice per
chunk (once for the DB upsert, once for the emitted delta). Compute
it once and reuse.


Claude-Session: https://claude.ai/code/session_01MHg5zs1VBjvRWQ54qHpfYD

Co-authored-by: Claude <noreply@anthropic.com>
2026-07-23 12:24:29 -04:00
Classic298
8becf9443e
fix: only admins may switch the global Automatic1111 checkpoint from image generation (#27244)
The Automatic1111 branch of the image generation route called set_image_model whenever the request carried a model field. On this backend set_image_model is not request-scoped: it persists image_generation.model to the global configuration and posts the new sd_model_checkpoint to the shared server, because Automatic1111 holds a single checkpoint instance-wide. A non-admin holding features.image_generation could therefore change the instance-wide image model and the shared backend checkpoint for every user by sending a model on an ordinary generation request, even though the setting is otherwise managed only through the admin-only image configuration route and the frontend never sends this field.

Gate the switch on an admin caller. A non-admin now generates on the currently configured checkpoint and the model field no longer mutates global state; admins retain per-request model switching here and through the image configuration route. Image editing is unaffected, as it selects its model per request without writing global configuration.
2026-07-23 12:23:24 -04:00
Classic298
9a6d168499
fix: enforce action availability and model access on the chat action route (#27243)
The chat action route loaded a Function by its raw action_id and executed its action callable after only checking that the id and the requested model existed. The model list that the client renders actions from resolves each model's actions to the active action-type Functions that are global or assigned to that model, and the action route did not mirror that resolution, so a disabled, unassigned, or wrong-type Function, or an action on a model the caller cannot access, could be reached by calling the route directly.

Gate the route on the same rules the model resolution applies: the Function must be an active action, and for server-resolved models the caller must have model access and the action must be one the model actually surfaces (matched by function id, the prefix of each model actions entry, so single and sub-actions both resolve). Direct connections carry a client-supplied model the caller already owns, so the model-bound checks are scoped to non-direct calls; the active-action check always applies. Executing admin-authored Function code remains intended behaviour — this only keeps the route consistent with which actions each model exposes.

Co-authored-by: komyunghan <komyunghan@users.noreply.github.com>
2026-07-23 12:23:05 -04:00
Classic298
9a54bc4bbb
perf: halve function-table queries in get_all_models (#27230)
get_all_models ran four function-table queries: global actions, active
actions, global filters, active filters. Global functions are by
definition (type, is_active=True, is_global=True) — a subset of the
active set — so the global id sets can be derived from the active-rows
queries' is_global flag. Four queries become two, and each dropped
query returned full rows including every plugin's source code.

Also folds the mid-function 'models.default_metadata' read into the
Config.get_many already issued at the top of the function (one fewer
round trip; the existing `or {}` default handling is preserved).


Claude-Session: https://claude.ai/code/session_01MHg5zs1VBjvRWQ54qHpfYD

Co-authored-by: Claude <noreply@anthropic.com>
2026-07-23 12:11:51 -04:00
Classic298
32242a6788
perf: 40% LESS CPU usage: cut per-instance CPU cost of shared socket.io Redis pub/sub channel (#27282)
* perf: cut per-instance CPU cost of shared socket.io Redis pub/sub channel

Profiling a multi-instance deployment (py-spy --gil) showed ~44% of worker
CPU in the socket.io pub/sub listener. Two causes, two fixes:

- Add hiredis so redis-py parses the RESP protocol in C instead of pure
  Python (redis/_parsers/resp3.py alone accounted for ~28% of GIL samples;
  redis-py auto-selects the hiredis parser when importable).

- Subclass AsyncRedisManager to drop emits whose target room has no local
  participants before upstream _handle_emit re-encodes the full packet.
  Every instance receives every emit published on the shared channel, so
  with N instances all but the hosting one were paying full packet
  re-serialization per message just to deliver it to nobody. Broadcasts
  (room=None) are unaffected.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q9CQ9qnp3sZGYQQwztsCJT

* fix: restrict pub/sub emit early-out to string rooms

Adversarial review against python-socketio 5.16.2 found one divergence
from upstream: for a degenerate empty-sequence room (emit to room=[]) on
an instance whose namespace has no local clients, the filter's
get_participants probe raises IndexError from room[0] where upstream
returns silently at the namespace guard and still publishes to Redis.
Open WebUI only ever emits to scalar string rooms or room=None, so the
case is unreachable today; guard on isinstance(room, str) anyway so any
non-string room shape passes through to upstream behavior unchanged.

Every open-webui emit uses a string room, so the fast path still covers
all real traffic.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q9CQ9qnp3sZGYQQwztsCJT

* Update requirements.txt

* Update pyproject.toml

* Update requirements-min.txt

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-07-23 12:11:38 -04:00
Timothy Jaeryang Baek
aaf2834db7 refac 2026-07-23 12:03:26 -04:00
Timothy Jaeryang Baek
d0f7da4f45 refac 2026-07-23 04:45:27 -04:00
Timothy Jaeryang Baek
bb12b1a18b refac 2026-07-23 04:16:14 -04:00
Timothy Jaeryang Baek
cc9a44569e refac 2026-07-23 04:01:30 -04:00
Timothy Jaeryang Baek
48625e657f refac 2026-07-23 03:54:19 -04:00
Classic298
073cd65afe
Log upstream provider rejections at warn/error level (#27238)
When an upstream provider rejects a request (e.g. a 400 for a
max_tokens value above the model's ceiling), the actionable error
message was only published to event sinks, which are invisible unless
an event function or webhook is configured. Admins had to query the
provider's API directly to diagnose failures (open-webui#27237).

Add a single log line in publish_model_provider_request_failed — the
chokepoint every upstream failure path (OpenAI-compatible chat,
embeddings, responses, token counting, and Ollama) already routes
through — recording status, provider, url, model, error code, and the
upstream message truncated to 1000 chars. 4xx logs at WARNING, 5xx at
ERROR. Client-facing responses are unchanged, so no additional error
detail is exposed in the chat.


Claude-Session: https://claude.ai/code/session_018VecyiPejru1EVF5yfe2sU

Co-authored-by: Claude <noreply@anthropic.com>
2026-07-23 03:42:48 -04:00
Timothy Jaeryang Baek
48cc9d388e refac 2026-07-23 03:39:56 -04:00
Timothy Jaeryang Baek
e18e249d5d refac 2026-07-23 03:39:30 -04:00
Timothy Jaeryang Baek
af629177f4 refac 2026-07-23 03:31:32 -04:00
Classic298
3cf3f8e189
fix: add missing folders field to SharingPermissions so the permission persists (#27296)
`sharing.folders` is present in `DEFAULT_USER_PERMISSIONS` but absent from the `SharingPermissions` response/update schema, so the admin default and group permission API silently drops it on every round-trip and the setting is never saved. Add `folders: bool = False`, matching the config default (`USER_PERMISSIONS_FOLDERS_ALLOW_SHARING`), restoring parity with `DEFAULT_USER_PERMISSIONS`.

Fixes #27120.
2026-07-23 03:24:51 -04:00
Andi Chandler
adf07e8df0
**i18n**: Internationalization or localization changes (#27304)
* Change spelling to British English in translation.json

Updated translations to use British English spelling for various terms.

* Update translation.json
2026-07-23 03:24:21 -04:00
joaoback
1b28b8a144
i18n: add pt-BR translations for newly added UI items and consistency pass (#27324)
New **pt-BR** translations for items introduced in the latest releases, plus a consistency/quality pass across existing strings (grammar, tone, capitalization, pluralization). Placeholders and hotkeys preserved. No logic changes.
2026-07-23 03:23:47 -04:00
Timothy Jaeryang Baek
9f00b62b3a refac 2026-07-23 03:22:51 -04:00
Timothy Jaeryang Baek
30415c925a refac 2026-07-23 03:11:38 -04:00
Timothy Jaeryang Baek
6ff1df326c refac 2026-07-23 03:05:29 -04:00
Timothy Jaeryang Baek
060d5da473 refac 2026-07-23 03:01:01 -04:00
Timothy Jaeryang Baek
73421c5b42 refac 2026-07-23 02:55:06 -04:00
Timothy Jaeryang Baek
cf887b68ea refac 2026-07-23 02:54:56 -04:00
Timothy Jaeryang Baek
5418ac921b refac 2026-07-23 02:33:09 -04:00
Timothy Jaeryang Baek
c4efa81d08 refac
Some checks failed
Python CI / Ruff Format (3.11) (push) Has been cancelled
Python CI / Ruff Format (3.12) (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_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/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
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
Frontend Build / Format & Build (push) Has been cancelled
Frontend Build / Unit Tests (push) Has been cancelled
Create and publish Docker images with specific build args / merge (map[name:main suffix:]) (push) Has been cancelled
Create and publish Docker images with specific build args / merge (map[name:cuda suffix:-cuda]) (push) Has been cancelled
Create and publish Docker images with specific build args / merge (map[name:cuda126 suffix:-cuda126]) (push) Has been cancelled
Create and publish Docker images with specific build args / merge (map[name:ollama suffix:-ollama]) (push) Has been cancelled
Create and publish Docker images with specific build args / merge (map[name:slim suffix:-slim]) (push) Has been cancelled
Create and publish Docker images with specific build args / notify-helm-charts (push) Has been cancelled
Create and publish Docker images with specific build args / copy-to-dockerhub (, main) (push) Has been cancelled
Create and publish Docker images with specific build args / copy-to-dockerhub (-cuda, cuda) (push) Has been cancelled
Create and publish Docker images with specific build args / copy-to-dockerhub (-cuda126, cuda126) (push) Has been cancelled
Create and publish Docker images with specific build args / copy-to-dockerhub (-ollama, ollama) (push) Has been cancelled
Create and publish Docker images with specific build args / copy-to-dockerhub (-slim, slim) (push) Has been cancelled
2026-07-21 14:18:41 -04:00
Timothy Jaeryang Baek
9ca8cf528a refac 2026-07-21 14:16:51 -04:00
Timothy Jaeryang Baek
409fb39717 refac 2026-07-21 13:53:30 -04:00
Timothy Jaeryang Baek
86efecd9ad refac 2026-07-20 22:45:50 -04:00
Timothy Jaeryang Baek
8631dc83dc refac 2026-07-20 22:27:13 -04:00
Timothy Jaeryang Baek
6940297486 refac 2026-07-20 22:27:09 -04:00
Timothy Jaeryang Baek
49e57f4e7e chore: format 2026-07-20 22:11:42 -04:00
Timothy Jaeryang Baek
4a42543fc3 refac 2026-07-20 22:11:16 -04:00
Timothy Jaeryang Baek
e88d2e053c refac 2026-07-20 22:10:11 -04:00
Timothy Jaeryang Baek
704d07e9a2 refac 2026-07-20 22:04:39 -04:00
Timothy Jaeryang Baek
bc8d24c951 refac 2026-07-20 22:03:31 -04:00
Timothy Jaeryang Baek
1428a4ddce refac 2026-07-20 21:44:12 -04:00
Timothy Jaeryang Baek
0c7ddbdb4f refac 2026-07-20 19:49:34 -04:00
Timothy Jaeryang Baek
2fcb36267f refac
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: 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 / notify-helm-charts (push) Blocked by required conditions
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 / copy-to-dockerhub (, main) (push) Blocked by required conditions
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 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 / 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:slim suffix:-slim]) (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 (-ollama, ollama) (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
Frontend Build / Format & Build (push) Waiting to run
Create and publish Docker images with specific build args / merge (map[name:ollama suffix:-ollama]) (push) Blocked by required conditions
2026-07-20 01:33:53 -04:00
Timothy Jaeryang Baek
af9a315ac3 refac 2026-07-20 01:33:47 -04:00
Timothy Jaeryang Baek
6e5efc1f75 refac 2026-07-20 01:21:15 -04:00
Timothy Jaeryang Baek
4da2ff2655 refac 2026-07-20 00:25:37 -04:00
Timothy Jaeryang Baek
d3ea51fd46 refac 2026-07-20 00:23:01 -04:00
Timothy Jaeryang Baek
2dbdba1f91 refac
Some checks failed
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_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/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
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
Frontend Build / Unit Tests (push) Has been cancelled
Frontend Build / Format & Build (push) Has been cancelled
Create and publish Docker images with specific build args / notify-helm-charts (push) Has been cancelled
Create and publish Docker images with specific build args / copy-to-dockerhub (, main) (push) Has been cancelled
Create and publish Docker images with specific build args / copy-to-dockerhub (-cuda, cuda) (push) Has been cancelled
Create and publish Docker images with specific build args / merge (map[name:cuda suffix:-cuda]) (push) Has been cancelled
Create and publish Docker images with specific build args / merge (map[name:cuda126 suffix:-cuda126]) (push) Has been cancelled
Create and publish Docker images with specific build args / merge (map[name:main suffix:]) (push) Has been cancelled
Create and publish Docker images with specific build args / merge (map[name:ollama suffix:-ollama]) (push) Has been cancelled
Create and publish Docker images with specific build args / merge (map[name:slim suffix:-slim]) (push) Has been cancelled
Create and publish Docker images with specific build args / copy-to-dockerhub (-cuda126, cuda126) (push) Has been cancelled
Create and publish Docker images with specific build args / copy-to-dockerhub (-ollama, ollama) (push) Has been cancelled
Create and publish Docker images with specific build args / copy-to-dockerhub (-slim, slim) (push) Has been cancelled
2026-07-17 23:39:49 -04:00
Timothy Jaeryang Baek
1a32d92d08 refac
Some checks failed
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 (-ollama, ollama) (push) Blocked by required conditions
Create and publish Docker images with specific build args / copy-to-dockerhub (-slim, slim) (push) Blocked by required conditions
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
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
Frontend Build / Format & Build (push) Waiting to run
Frontend Build / Unit Tests (push) Waiting to run
Python CI / Ruff Format (3.11) (push) Has been cancelled
Python CI / Ruff Format (3.12) (push) Has been cancelled
2026-07-17 06:50:16 -04:00
Timothy Jaeryang Baek
b2f7ecd83a refac 2026-07-17 06:45:00 -04:00
Timothy Jaeryang Baek
59c75f569b refac 2026-07-17 06:38:10 -04:00
Timothy Jaeryang Baek
f192657dc9 refac 2026-07-17 06:35:24 -04:00
Timothy Jaeryang Baek
9281adc564 refac 2026-07-17 06:34:17 -04:00
Timothy Jaeryang Baek
fd07e3a8e3 refac 2026-07-17 06:30:04 -04:00
Timothy Jaeryang Baek
f3a3550784 refac 2026-07-17 06:29:19 -04:00
Timothy Jaeryang Baek
890bfd0d97 refac 2026-07-17 06:28:07 -04:00
Timothy Jaeryang Baek
ff49217206 refac 2026-07-17 06:22:15 -04:00
Timothy Jaeryang Baek
1bf05ebc7d refac 2026-07-17 06:06:23 -04:00
Timothy Jaeryang Baek
cda5bdb9d4 refac 2026-07-17 05:51:39 -04:00
Timothy Jaeryang Baek
ea2e3d0afc refac 2026-07-17 05:48:41 -04:00
Timothy Jaeryang Baek
437c06c479 refac 2026-07-17 05:46:09 -04:00
Timothy Jaeryang Baek
d027a32ed1 refac 2026-07-17 05:40:40 -04:00
Timothy Jaeryang Baek
21e180182a refac 2026-07-17 05:34:00 -04:00
Timothy Jaeryang Baek
2d83c7438c refac 2026-07-17 05:32:50 -04:00
Timothy Jaeryang Baek
d0bea60581 refac 2026-07-17 05:31:32 -04:00
Timothy Jaeryang Baek
421da67446 refac 2026-07-17 05:15:09 -04:00
Timothy Jaeryang Baek
6fcb38fe2e refac 2026-07-17 05:12:16 -04:00
Timothy Jaeryang Baek
5424ac5891 refac 2026-07-17 05:09:29 -04:00
Timothy Jaeryang Baek
3316ba76aa refac 2026-07-17 04:54:44 -04:00
Timothy Jaeryang Baek
e5e2cd7876 refac 2026-07-17 04:48:21 -04:00
Timothy Jaeryang Baek
56f2cb5302 refac 2026-07-17 04:11:11 -04:00
Timothy Jaeryang Baek
a213355785 refac 2026-07-17 04:09:33 -04:00
Timothy Jaeryang Baek
ab519e40d9 refac 2026-07-17 03:41:51 -04:00
Timothy Jaeryang Baek
af8cc6c91a refac 2026-07-17 03:40:28 -04:00
Timothy Jaeryang Baek
88c6b8bc5c refac 2026-07-17 03:30:44 -04:00
Timothy Jaeryang Baek
203953faa2 refac 2026-07-17 03:28:45 -04:00
Timothy Jaeryang Baek
741ce0c239 refac 2026-07-17 03:24:55 -04:00
Timothy Jaeryang Baek
a6fcc61a16 refac 2026-07-17 00:55:24 -04:00
Timothy Jaeryang Baek
ce44b90eae refac 2026-07-17 00:54:13 -04:00
Timothy Jaeryang Baek
27d7eafcd9 refac 2026-07-17 00:16:01 -04:00
Timothy Jaeryang Baek
d0ba3ada2c refac 2026-07-17 00:03:13 -04:00
Timothy Jaeryang Baek
4d10bfb72a refac 2026-07-17 00:01:34 -04:00
Timothy Jaeryang Baek
30c91e46e5 refac 2026-07-17 00:00:24 -04:00
Timothy Jaeryang Baek
346b99c383 refac 2026-07-16 23:39:16 -04:00
Timothy Jaeryang Baek
b08f1e8847 refac 2026-07-16 23:38:27 -04:00
Timothy Jaeryang Baek
53b8e6560b refac 2026-07-16 23:35:13 -04:00
Timothy Jaeryang Baek
001775d8e8 refac 2026-07-16 22:53:31 -04:00
Timothy Jaeryang Baek
743b9fd3ce refac 2026-07-16 21:57:43 -04:00
Timothy Jaeryang Baek
a2000df253 refac 2026-07-16 21:57:15 -04:00
Timothy Jaeryang Baek
668f9fe390 refac 2026-07-16 21:25:35 -04:00
Timothy Jaeryang Baek
975c9f9b50 refac 2026-07-16 21:08:23 -04:00
Timothy Jaeryang Baek
d7f33996ee refac
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: 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
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 (-ollama, ollama) (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 / Format & Build (push) Waiting to run
Frontend Build / Unit Tests (push) Waiting to run
2026-07-16 02:49:21 -04:00
Timothy Jaeryang Baek
2e857a82d7 refac 2026-07-16 02:47:51 -04:00
Timothy Jaeryang Baek
6167dc3564 refac 2026-07-16 02:43:50 -04:00
Timothy Jaeryang Baek
c3ce0c5080 refac 2026-07-16 02:42:47 -04:00
Timothy Jaeryang Baek
75894161e4 refac 2026-07-16 02:35:44 -04:00
Timothy Jaeryang Baek
8270aa59ab refac 2026-07-16 02:27:59 -04:00
Timothy Jaeryang Baek
f8d78289a6 refac 2026-07-16 02:25:20 -04:00
Timothy Jaeryang Baek
58960028a4 refac 2026-07-16 02:23:46 -04:00
Timothy Jaeryang Baek
f0eea61155 refac 2026-07-16 02:21:46 -04:00
Timothy Jaeryang Baek
3c45f9f511 refac 2026-07-16 02:18:54 -04:00
Timothy Jaeryang Baek
dc5a250068 refac 2026-07-16 02:07:47 -04:00
Timothy Jaeryang Baek
9be11883b8 refac 2026-07-16 02:05:08 -04:00
Timothy Jaeryang Baek
a410ca36af refac 2026-07-16 01:46:02 -04:00
Timothy Jaeryang Baek
4d27bfff92 refac 2026-07-16 01:42:24 -04:00
Timothy Jaeryang Baek
b80d204b81 refac 2026-07-16 01:37:21 -04:00
Timothy Jaeryang Baek
e6c2b8ad59 refac 2026-07-16 01:34:50 -04:00
Timothy Jaeryang Baek
cf235738f5 refac 2026-07-16 01:27:52 -04:00
Timothy Jaeryang Baek
200d447f62 refac 2026-07-16 01:10:13 -04:00
Timothy Jaeryang Baek
c55e373b99 refac 2026-07-16 00:58:34 -04:00
Timothy Jaeryang Baek
771a024b40 refac 2026-07-16 00:39:31 -04:00
Timothy Jaeryang Baek
92fbc77877 refac 2026-07-16 00:32:27 -04:00
Timothy Jaeryang Baek
b23ddeb280 refac 2026-07-16 00:30:44 -04:00
Timothy Jaeryang Baek
c1e228d6ad refac 2026-07-16 00:10:28 -04:00
Timothy Jaeryang Baek
1fe862b965 refac 2026-07-16 00:07:41 -04:00
Timothy Jaeryang Baek
f7716fcbaf refac 2026-07-16 00:00:38 -04:00
Timothy Jaeryang Baek
e389874fe2 refac 2026-07-15 23:55:31 -04:00
Timothy Jaeryang Baek
2b8ef9340e refac 2026-07-15 23:51:37 -04:00
Timothy Jaeryang Baek
0dc93b8ae7 refac 2026-07-15 23:48:32 -04:00
Timothy Jaeryang Baek
c4c4ab57e3 refac 2026-07-15 23:44:05 -04:00
Timothy Jaeryang Baek
d864669b59 refac 2026-07-15 23:38:48 -04:00
Timothy Jaeryang Baek
07fafe081b refac 2026-07-15 23:34:45 -04:00
Timothy Jaeryang Baek
2712103c59 refac 2026-07-15 23:32:17 -04:00
Timothy Jaeryang Baek
f67252b5e8 refac 2026-07-15 23:27:58 -04:00
Timothy Jaeryang Baek
ee000c503c refac 2026-07-15 23:21:06 -04:00
Timothy Jaeryang Baek
f7af03ff26 refac 2026-07-15 22:48:41 -04:00
Timothy Jaeryang Baek
588f129695 refac 2026-07-15 22:45:00 -04:00
Timothy Jaeryang Baek
9e2536aa57 refac 2026-07-15 22:40:30 -04:00
Timothy Jaeryang Baek
e65fdf1ca5 refac 2026-07-15 22:36:47 -04:00
Timothy Jaeryang Baek
185bca8552 refac 2026-07-15 22:34:52 -04:00
Timothy Jaeryang Baek
b16a4c4e9a refac 2026-07-15 21:45:17 -04:00
Timothy Jaeryang Baek
423cafd4e7 refac 2026-07-15 21:43:47 -04:00
Timothy Jaeryang Baek
4d11553a6b refac 2026-07-15 18:54:47 -04:00
Timothy Jaeryang Baek
b3255a3656 refac 2026-07-15 18:52:12 -04:00
Timothy Jaeryang Baek
6abee5cc3c refac 2026-07-15 18:29:48 -04:00
Timothy Jaeryang Baek
c5f546d3fb refac 2026-07-15 18:29:01 -04:00
Timothy Jaeryang Baek
72b14195b6 refac 2026-07-15 18:28:53 -04:00
Timothy Jaeryang Baek
d608c1298c refac 2026-07-15 18:26:29 -04:00
Timothy Jaeryang Baek
9e2eac05b0 refac 2026-07-15 18:24:15 -04:00
Timothy Jaeryang Baek
c3878b418a refac 2026-07-15 18:21:24 -04:00
Timothy Jaeryang Baek
4b0122a120 refac
Some checks are pending
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: 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/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 / merge (map[name:cuda suffix:-cuda]) (push) Blocked by required conditions
Python CI / Ruff Format (3.11) (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 / 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: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 (-ollama, ollama) (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 / Format & Build (push) Waiting to run
Frontend Build / Unit Tests (push) Waiting to run
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 / copy-to-dockerhub (-cuda126, cuda126) (push) Blocked by required conditions
2026-07-15 18:09:34 -04:00
Timothy Jaeryang Baek
933ab1e1cd refac 2026-07-15 18:03:36 -04:00
Timothy Jaeryang Baek
ba067258de refac 2026-07-15 17:50:26 -04:00
Timothy Jaeryang Baek
db934a3b4f refac 2026-07-15 16:51:08 -04:00
Timothy Jaeryang Baek
5b934b1f90 refac 2026-07-15 16:33:25 -04:00
Timothy Jaeryang Baek
667cba1a95 refac 2026-07-15 16:31:27 -04:00
Timothy Jaeryang Baek
9cfafc0608 refac 2026-07-15 16:27:13 -04:00
Timothy Jaeryang Baek
29782aba01 refac 2026-07-15 16:26:40 -04:00
Timothy Jaeryang Baek
609cc6ad9b refac 2026-07-15 16:26:31 -04:00
Timothy Jaeryang Baek
86f55d04ec refac 2026-07-15 16:21:47 -04:00
Timothy Jaeryang Baek
946487a3ba refac 2026-07-15 16:04:08 -04:00
Timothy Jaeryang Baek
74d976c2f7 refac 2026-07-15 15:13:51 -04:00
Timothy Jaeryang Baek
285a65ef39 refac 2026-07-15 15:08:53 -04:00
Timothy Jaeryang Baek
3fcb7b2d64 refac 2026-07-15 15:06:08 -04:00
Timothy Jaeryang Baek
396ee62226 refac 2026-07-15 05:52:21 -04:00
Timothy Jaeryang Baek
f8fbd50af3 refac 2026-07-15 05:51:06 -04:00
Timothy Jaeryang Baek
727041da78 refac 2026-07-15 05:49:48 -04:00
Timothy Jaeryang Baek
f8ea15b84a refac 2026-07-15 05:38:44 -04:00
Timothy Jaeryang Baek
3ce3c52936 refac 2026-07-15 05:17:41 -04:00
Timothy Jaeryang Baek
a3908f1281 refac 2026-07-15 04:55:38 -04:00
Timothy Jaeryang Baek
bd406851ea refac 2026-07-15 04:52:08 -04:00
Timothy Jaeryang Baek
343eb1d659 refac 2026-07-15 04:52:06 -04:00
Timothy Jaeryang Baek
1760b073c7 refac 2026-07-15 04:39:37 -04:00
Timothy Jaeryang Baek
91277726cd refac 2026-07-15 04:35:52 -04:00
Timothy Jaeryang Baek
59fc600b52 refac 2026-07-15 04:33:06 -04:00
Timothy Jaeryang Baek
d859110311 refac 2026-07-15 04:29:16 -04:00
Timothy Jaeryang Baek
9499587c33 refac 2026-07-15 04:25:51 -04:00
Timothy Jaeryang Baek
2018546a7b refac 2026-07-15 04:18:56 -04:00
Timothy Jaeryang Baek
f8350360df refac 2026-07-15 04:12:11 -04:00
Timothy Jaeryang Baek
05e3f71317 refac 2026-07-15 04:01:23 -04:00
Timothy Jaeryang Baek
9a706329c5 refac 2026-07-15 03:41:49 -04:00
Timothy Jaeryang Baek
fa889837e9 refac 2026-07-15 03:39:45 -04:00
Timothy Jaeryang Baek
fee4c280f8 refac 2026-07-15 03:22:29 -04:00
Timothy Jaeryang Baek
36cff229a9 refac 2026-07-15 03:17:32 -04:00
Timothy Jaeryang Baek
309313db68 refac 2026-07-15 03:16:06 -04:00
Timothy Jaeryang Baek
3ff20b210e refac 2026-07-15 03:13:37 -04:00
Timothy Jaeryang Baek
d300522e96 refac 2026-07-15 03:11:15 -04:00
Timothy Jaeryang Baek
3fa600123a refac 2026-07-15 03:01:50 -04:00
Timothy Jaeryang Baek
04311d559d refac 2026-07-15 02:53:39 -04:00
Timothy Jaeryang Baek
622767d724 refac 2026-07-15 02:49:46 -04:00
Timothy Jaeryang Baek
84b1ab0ed3 refac 2026-07-15 02:16:55 -04:00
Timothy Jaeryang Baek
280179828a refac 2026-07-15 02:11:10 -04:00
Timothy Jaeryang Baek
b2769f831e refac 2026-07-15 02:07:39 -04:00
Timothy Jaeryang Baek
804ec68a6b refac 2026-07-15 02:01:45 -04:00
Timothy Jaeryang Baek
fb2ea27295 refac 2026-07-15 01:59:56 -04:00
Timothy Jaeryang Baek
581f2f36f4 refac 2026-07-15 01:54:51 -04:00
Timothy Jaeryang Baek
b92f592300 refac 2026-07-15 01:51:40 -04:00
Timothy Jaeryang Baek
de0e90551f refac 2026-07-15 01:38:17 -04:00
Timothy Jaeryang Baek
1d1f60ab44 refac 2026-07-15 01:35:59 -04:00
Timothy Jaeryang Baek
ccb1ab7739 refac 2026-07-15 01:31:21 -04:00
Timothy Jaeryang Baek
c64fe45376 refac 2026-07-15 01:22:51 -04:00
Timothy Jaeryang Baek
43e792a8f4 refac 2026-07-15 01:16:06 -04:00
Timothy Jaeryang Baek
c1460570b7 refac 2026-07-15 00:40:20 -04:00
Timothy Jaeryang Baek
e05f9fa17e refac 2026-07-15 00:30:28 -04:00
Timothy Jaeryang Baek
7ebf15040b refac 2026-07-15 00:15:55 -04:00
Timothy Jaeryang Baek
63ada24706 refac 2026-07-15 00:14:01 -04:00
Timothy Jaeryang Baek
254888cf15 refac 2026-07-14 23:20:15 -04:00
Timothy Jaeryang Baek
263bbc77d8 refac 2026-07-14 23:13:50 -04:00
Timothy Jaeryang Baek
7a9928ef17 refac 2026-07-14 23:08:41 -04:00
Timothy Jaeryang Baek
ea31a3bd61 refac 2026-07-14 18:05:36 -04:00
Timothy Jaeryang Baek
50d3c927bf refac
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: free_disk:false name:main suffix:]) (push) Waiting to run
Frontend Build / Unit Tests (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
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 (-ollama, ollama) (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 / Format & Build (push) Waiting to run
2026-07-14 17:34:00 -04:00
Timothy Jaeryang Baek
e8b4c7f9e2 refac 2026-07-14 17:16:20 -04:00
Timothy Jaeryang Baek
9588c97e64 refac 2026-07-14 17:10:11 -04:00
Timothy Jaeryang Baek
eb11029eac refac 2026-07-14 17:07:39 -04:00
Timothy Jaeryang Baek
e39ff71532 refac 2026-07-14 17:05:07 -04:00
Timothy Jaeryang Baek
d42da41090 refac 2026-07-14 17:01:10 -04:00
Timothy Jaeryang Baek
54cef1df48 refac 2026-07-14 17:00:27 -04:00
Timothy Jaeryang Baek
79d3e34eea refac 2026-07-14 16:57:35 -04:00
Timothy Jaeryang Baek
6772b1cb4f refac 2026-07-14 14:41:12 -04:00
Timothy Jaeryang Baek
9f17c5960a refac 2026-07-14 14:33:10 -04:00
Timothy Jaeryang Baek
8dd862d338 refac 2026-07-14 14:17:59 -04:00
Timothy Jaeryang Baek
3be493f5b0 refac 2026-07-14 04:53:37 -04:00
Timothy Jaeryang Baek
c7f1ff9323 refac 2026-07-14 04:30:47 -04:00
Timothy Jaeryang Baek
c9103a29ff refac 2026-07-14 04:26:18 -04:00
Timothy Jaeryang Baek
7b2efdff08 refac 2026-07-14 04:20:28 -04:00
Timothy Jaeryang Baek
aedb6bef4e refac 2026-07-14 04:15:20 -04:00
Timothy Jaeryang Baek
a7fbf66269 refac 2026-07-14 04:06:41 -04:00
Timothy Jaeryang Baek
5929bf2061 refac 2026-07-14 04:02:42 -04:00
Timothy Jaeryang Baek
1b64ccbaa0 refac 2026-07-14 04:02:28 -04:00
Timothy Jaeryang Baek
b2e4bda927 refac 2026-07-14 03:53:37 -04:00
Timothy Jaeryang Baek
04b146f2ce refac 2026-07-14 03:52:59 -04:00
Timothy Jaeryang Baek
e58a4633b1 refac 2026-07-14 03:51:09 -04:00
Timothy Jaeryang Baek
92842ecf23 refac 2026-07-14 03:49:21 -04:00
Timothy Jaeryang Baek
dbdacf2678 refac 2026-07-14 03:48:34 -04:00
Timothy Jaeryang Baek
b3aead23da refac 2026-07-14 03:46:37 -04:00
Timothy Jaeryang Baek
2e8d92c7b1 refac 2026-07-14 03:43:34 -04:00
Timothy Jaeryang Baek
b2fd6ccfd5 refac 2026-07-14 03:36:44 -04:00
Timothy Jaeryang Baek
fcedbebcf4 refac 2026-07-14 03:18:46 -04:00
Timothy Jaeryang Baek
7407eeede8 refac 2026-07-14 03:16:20 -04:00
Timothy Jaeryang Baek
d475cb9174 refac 2026-07-14 03:10:12 -04:00
Timothy Jaeryang Baek
d364101761 refac 2026-07-14 02:55:01 -04:00
Timothy Jaeryang Baek
d8131d1091 refac 2026-07-14 02:37:21 -04:00
Timothy Jaeryang Baek
b9d5462ed0 refac 2026-07-14 02:11:38 -04:00
Timothy Jaeryang Baek
251ef952bf refac 2026-07-14 02:10:02 -04:00
Timothy Jaeryang Baek
809d9f29f3 refac 2026-07-14 01:47:50 -04:00
Timothy Jaeryang Baek
bcf449d5ad refac 2026-07-14 01:44:00 -04:00
Timothy Jaeryang Baek
a62ba97467 refac 2026-07-14 01:41:40 -04:00
Timothy Jaeryang Baek
b012d683d8 refac 2026-07-14 01:40:58 -04:00
Timothy Jaeryang Baek
6e14d0d627 refac 2026-07-14 01:39:40 -04:00
Timothy Jaeryang Baek
34813573fa refac 2026-07-14 01:34:41 -04:00
Timothy Jaeryang Baek
a9617ca218 refac 2026-07-14 01:15:29 -04:00
Timothy Jaeryang Baek
f1ded9409a refac 2026-07-14 01:13:40 -04:00
Timothy Jaeryang Baek
8f77533317 refac 2026-07-14 01:00:24 -04:00
Timothy Jaeryang Baek
410ebb05d4 refac 2026-07-14 00:54:01 -04:00
Timothy Jaeryang Baek
5ab0ea8b8c refac 2026-07-14 00:50:44 -04:00
Timothy Jaeryang Baek
d38c953608 refac 2026-07-14 00:49:30 -04:00
Timothy Jaeryang Baek
f1584b5a37 refac 2026-07-14 00:48:40 -04:00
Timothy Jaeryang Baek
84e4d6ef82 refac 2026-07-14 00:44:08 -04:00
Timothy Jaeryang Baek
77da3d8c81 refac 2026-07-14 00:43:40 -04:00
Timothy Jaeryang Baek
f84dabe3d9 refac 2026-07-14 00:43:06 -04:00
Timothy Jaeryang Baek
4ed19d504b refac 2026-07-14 00:42:57 -04:00
Timothy Jaeryang Baek
caa2457c17 refac 2026-07-14 00:42:47 -04:00
Timothy Jaeryang Baek
f730733bc4 refac 2026-07-14 00:30:47 -04:00
Timothy Jaeryang Baek
53ccd718a5 refac 2026-07-14 00:24:20 -04:00
Timothy Jaeryang Baek
009715cd63 refac 2026-07-14 00:22:00 -04:00
Timothy Jaeryang Baek
6a7068c3a4 refac 2026-07-14 00:21:47 -04:00
Timothy Jaeryang Baek
797293c749 refac 2026-07-14 00:19:41 -04:00
Timothy Jaeryang Baek
7088d245bb refac 2026-07-14 00:10:28 -04:00
Timothy Jaeryang Baek
0c23466a3e refac 2026-07-13 23:46:28 -04:00
Timothy Jaeryang Baek
8f07c0c8ee refac 2026-07-13 23:34:54 -04:00
Timothy Jaeryang Baek
d3fd860c13 refac 2026-07-13 23:32:07 -04:00
Timothy Jaeryang Baek
3005b7bc71 refac 2026-07-13 23:31:14 -04:00
Timothy Jaeryang Baek
23062e9fca refac 2026-07-13 23:31:01 -04:00
Timothy Jaeryang Baek
17e6496538 refac 2026-07-13 23:28:43 -04:00
Timothy Jaeryang Baek
959558fd82 refac 2026-07-13 23:27:54 -04:00
Timothy Jaeryang Baek
e1f96aa20e refac 2026-07-13 23:21:45 -04:00
Timothy Jaeryang Baek
2f37e853d1 refac 2026-07-13 23:19:58 -04:00
Timothy Jaeryang Baek
e355959e91 refac 2026-07-13 23:16:22 -04:00
Timothy Jaeryang Baek
08dacd19da refac 2026-07-13 23:01:10 -04:00
Timothy Jaeryang Baek
51ff386fd6 refac 2026-07-13 23:00:54 -04:00
Timothy Jaeryang Baek
e8b59b2ef3 refac 2026-07-13 22:50:24 -04:00
Timothy Jaeryang Baek
49d202a18e refac
Some checks are pending
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
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 (-ollama, ollama) (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 / Format & Build (push) Waiting to run
Frontend Build / Unit Tests (push) Waiting to run
2026-07-13 12:06:17 -04:00
Timothy Jaeryang Baek
09d4cccb79 refac
Some checks failed
Python CI / Ruff Format (3.11) (push) Has been cancelled
Python CI / Ruff Format (3.12) (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_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/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
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 / merge (map[name:cuda suffix:-cuda]) (push) Has been cancelled
Create and publish Docker images with specific build args / merge (map[name:cuda126 suffix:-cuda126]) (push) Has been cancelled
Create and publish Docker images with specific build args / merge (map[name:main suffix:]) (push) Has been cancelled
Create and publish Docker images with specific build args / merge (map[name:ollama suffix:-ollama]) (push) Has been cancelled
Create and publish Docker images with specific build args / merge (map[name:slim suffix:-slim]) (push) Has been cancelled
Create and publish Docker images with specific build args / notify-helm-charts (push) Has been cancelled
Create and publish Docker images with specific build args / copy-to-dockerhub (, main) (push) Has been cancelled
Create and publish Docker images with specific build args / copy-to-dockerhub (-cuda, cuda) (push) Has been cancelled
Create and publish Docker images with specific build args / copy-to-dockerhub (-cuda126, cuda126) (push) Has been cancelled
Create and publish Docker images with specific build args / copy-to-dockerhub (-ollama, ollama) (push) Has been cancelled
Create and publish Docker images with specific build args / copy-to-dockerhub (-slim, slim) (push) Has been cancelled
2026-07-10 18:32:21 -05:00
Timothy Jaeryang Baek
9a772f42c8 refac 2026-07-10 16:41:35 -05:00
A-PS1999
8c71897bc0
Corrected a few Japanese translations, added several more which were previously blank (#26881)
Some checks failed
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/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/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 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 / 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 (-ollama, ollama) (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 / Format & Build (push) Has been cancelled
Frontend Build / Unit Tests (push) Has been cancelled
2026-07-10 13:33:46 -05:00
Classic298
f8c0d2fdd6
fix: don't mutate caller's payload when resolving a pipe base model (#26906)
generate_function_chat_completion rewrote form_data['model'] to the base
model id in place. Because that dict is the same object process_chat_response
later re-submits for the post-tool-call continuation, the continuation was
access-checked against the base model instead of the user-facing preset.
For a public preset over a private/unregistered pipe base, a non-admin's
first message succeeded but the continuation after a native tool call was
denied and silently swallowed, aborting the response (admins skip the
user-only check, so they were unaffected).

Operate on a shallow copy of form_data, mirroring the openai/ollama routers
which already substitute the base model on a copy. The continuation now
re-checks the preset the user actually has access to.


Claude-Session: https://claude.ai/code/session_018toPfJW1hMXAhokGaL43Ep

Co-authored-by: Claude <noreply@anthropic.com>
2026-07-10 13:33:24 -05:00
Classic298
274729aa47
fix: socket reconnect recovery never runs for chats started from the home page (#26913)
When a chat is started from the home page, the URL is switched to /c/{id} with
history.replaceState, so the Chat component is never remounted and chatIdProp
stays empty for the lifetime of that view. Both websocket recovery paths,
handleSocketConnect and the chat:active fallback, are gated on chatIdProp and
therefore never run for these chats. After any websocket drop during a response
(mobile backgrounding, VPN or IP change, wake from sleep) the completed response
is never fetched and the chat stays stuck in a loading state until a manual page
refresh. Chats opened directly via /c/{id} recover fine, which is why the bug
only reproduces reliably on freshly started chats.

Gate both recovery paths on the chatId store instead, which is set for every
persisted chat, and let loadChat fall back to it so the recovery reload also
works when chatIdProp is empty. Chats opened via /c/{id} behave exactly as
before and temporary chats stay excluded.

The same gate likely explains the remaining reports in #26315.

Fixes #26844
2026-07-10 13:30:55 -05:00
Classic298
65a5fad7b9
fix: gate allow-same-origin on the terminal file-preview iframe to prevent same-origin XSS (#26907)
The system-terminal HTML file preview (FilePreview.svelte, serveUrl branch) rendered served HTML in an iframe that hardcoded allow-same-origin. The terminal proxy serves the file root-relative (same origin as the app) and injects no CSP, and there is no default global CSP, so script in a previewed HTML file executed in the application's own origin and could read localStorage (the session token), enabling account takeover and, for admin or workspace.functions victims, server-side RCE via Functions. The sibling srcdoc branch already gates allow-same-origin behind the iframeSandboxAllowSameOrigin setting (off by default) and injects a CSP; the serveUrl branch never received that defense. Gate allow-same-origin on the serveUrl branch identically, so by default the preview runs at an opaque origin and its scripts cannot reach the parent context. Legitimate HTML preview rendering is unaffected.

Co-authored-by: manus-use <manus-use@users.noreply.github.com>
2026-07-10 13:30:18 -05:00
Classic298
f4a6ea9300
fix: Milvus multitenancy scalar index creation on Milvus Lite (#26911)
Enabling ENABLE_MILVUS_MULTITENANCY_MODE with the default MILVUS_URI (embedded Milvus Lite at DATA_DIR/vector_db/milvus.db) fails on the first embedding write: _create_shared_collection calls collection.create_index(RESOURCE_ID_FIELD) with no index params. A Milvus server auto-selects a scalar index type in that case, but Milvus Lite rejects the call with "create_index missing required 'index_type' parameter", so shared collection creation raises and every embedding write 500s (memory add, file upload, knowledge writes).

Keep the parameterless call as the first attempt so behavior on Milvus servers is unchanged, fall back to an explicit INVERTED scalar index, and if that also fails log a warning and continue. The scalar index only accelerates resource_id filters; inserts and filtered queries work without it, so a missing index must not break collection creation.

Verified against embedded Milvus Lite: shared collections now create (with the warning), and memory add, file upload and memory query succeed end to end. Against a Milvus server the first attempt is identical to the current code, so nothing changes where it works today.
2026-07-10 13:29:29 -05:00
Classic298
0f8846b7fc
fix: convert SecurityHeadersMiddleware to pure ASGI (#26924)
SecurityHeadersMiddleware was the last middleware in the stack still
subclassing BaseHTTPMiddleware, after CommitSession, AuthToken,
WebsocketUpgradeGuard and Redirect were all moved to pure ASGI in
utils/asgi_middleware.py. BaseHTTPMiddleware re-buffers the response
body through an anyio task group, which has known issues with
streaming and Content-Length-bearing responses (e.g. the FileResponse
returned by /api/v1/audio/speech).

Reimplement it as a pure-ASGI middleware that stamps the configured
security headers onto the http.response.start message via
MutableHeaders and forwards all body chunks untouched, matching the
pattern already used by its four siblings. set_security_headers() and
all its helpers are unchanged.

Co-authored-by: classic298 <classic298@users.noreply.github.com>
2026-07-10 13:29:14 -05:00
Classic298
42f5c3d6f7
Merge pull request #26914 from Classic298/srcdoc-embed-prompt-confirmation
fix: restore prompt confirmation for sandboxed tool result embeds
2026-07-10 13:28:39 -05:00
Timothy Jaeryang Baek
b854389951 refac 2026-07-09 18:46:40 -05:00
Timothy Jaeryang Baek
6e030e892b refac 2026-07-09 18:28:38 -05:00
Timothy Jaeryang Baek
5fe525b8e0 refac 2026-07-09 18:02:37 -05:00
Timothy Jaeryang Baek
f5b196c060 refac 2026-07-09 17:59:17 -05:00
Timothy Jaeryang Baek
247b866330 refac 2026-07-09 17:44:40 -05:00
Timothy Jaeryang Baek
285379d489 refac 2026-07-09 17:43:13 -05:00
Timothy Jaeryang Baek
5ab012e7ae refac 2026-07-09 17:38:52 -05:00
Timothy Jaeryang Baek
d3ea8eb7e7 refac 2026-07-09 17:38:19 -05:00
Timothy Jaeryang Baek
6be9d1e760 refac 2026-07-09 17:37:47 -05:00
Timothy Jaeryang Baek
5e1a337d6e refac 2026-07-09 17:37:43 -05:00
Timothy Jaeryang Baek
31996a5acf refac 2026-07-09 17:37:31 -05:00
Timothy Jaeryang Baek
c89b6c50bc refac 2026-07-09 17:37:17 -05:00
Timothy Jaeryang Baek
0a8492b15d refac 2026-07-09 17:37:12 -05:00
Timothy Jaeryang Baek
9951fbe549 refac 2026-07-09 17:37:04 -05:00
Timothy Jaeryang Baek
5c389ad93f refac 2026-07-09 17:33:30 -05:00
Timothy Jaeryang Baek
975f7b868a refac 2026-07-09 17:32:45 -05:00
Timothy Jaeryang Baek
ef8630d556 refac 2026-07-09 17:31:43 -05:00
Timothy Jaeryang Baek
252e6fd855 refac 2026-07-09 17:30:02 -05:00
Timothy Jaeryang Baek
951f96021a refac 2026-07-09 17:29:51 -05:00
Timothy Jaeryang Baek
db802e28d3 refac 2026-07-09 17:29:07 -05:00
Timothy Jaeryang Baek
a489e4f219 refac 2026-07-09 17:28:45 -05:00
Timothy Jaeryang Baek
8e46450acd refac 2026-07-09 17:28:34 -05:00
Timothy Jaeryang Baek
bd6e0b61c2 refac 2026-07-09 17:28:05 -05:00
Timothy Jaeryang Baek
44c2a27ce0 refac
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: 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 / notify-helm-charts (push) Blocked by required conditions
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 / 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 / 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 (-ollama, ollama) (push) Blocked by required conditions
Create and publish Docker images with specific build args / copy-to-dockerhub (-slim, slim) (push) Blocked by required conditions
2026-07-09 16:20:19 -05:00
Timothy Jaeryang Baek
10724d057a refac
Some checks failed
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/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_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/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 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
Frontend Build / Format & Build (push) Has been cancelled
Frontend Build / Unit Tests (push) Has been cancelled
Create and publish Docker images with specific build args / merge (map[name:cuda suffix:-cuda]) (push) Has been cancelled
Create and publish Docker images with specific build args / merge (map[name:cuda126 suffix:-cuda126]) (push) Has been cancelled
Create and publish Docker images with specific build args / merge (map[name:main suffix:]) (push) Has been cancelled
Create and publish Docker images with specific build args / merge (map[name:ollama suffix:-ollama]) (push) Has been cancelled
Create and publish Docker images with specific build args / merge (map[name:slim suffix:-slim]) (push) Has been cancelled
Create and publish Docker images with specific build args / notify-helm-charts (push) Has been cancelled
Create and publish Docker images with specific build args / copy-to-dockerhub (, main) (push) Has been cancelled
Create and publish Docker images with specific build args / copy-to-dockerhub (-cuda, cuda) (push) Has been cancelled
Create and publish Docker images with specific build args / copy-to-dockerhub (-cuda126, cuda126) (push) Has been cancelled
Create and publish Docker images with specific build args / copy-to-dockerhub (-ollama, ollama) (push) Has been cancelled
Create and publish Docker images with specific build args / copy-to-dockerhub (-slim, slim) (push) Has been cancelled
2026-07-02 12:38:22 -05:00
Tim Baek
ecd48e2f71
0.10.2 (#26642)
Some checks failed
Release to PyPI / release (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
Python CI / Ruff Format (3.11) (push) Has been cancelled
Python CI / Ruff Format (3.12) (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_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/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 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
Frontend Build / Format & Build (push) Has been cancelled
Frontend Build / Unit Tests (push) Has been cancelled
Release / publish (push) Has been cancelled
Create and publish Docker images with specific build args / merge (map[name:cuda suffix:-cuda]) (push) Has been cancelled
Create and publish Docker images with specific build args / merge (map[name:cuda126 suffix:-cuda126]) (push) Has been cancelled
Create and publish Docker images with specific build args / merge (map[name:main suffix:]) (push) Has been cancelled
Create and publish Docker images with specific build args / merge (map[name:ollama suffix:-ollama]) (push) Has been cancelled
Create and publish Docker images with specific build args / merge (map[name:slim suffix:-slim]) (push) Has been cancelled
Create and publish Docker images with specific build args / notify-helm-charts (push) Has been cancelled
Create and publish Docker images with specific build args / copy-to-dockerhub (, main) (push) Has been cancelled
Create and publish Docker images with specific build args / copy-to-dockerhub (-cuda, cuda) (push) Has been cancelled
Create and publish Docker images with specific build args / copy-to-dockerhub (-cuda126, cuda126) (push) Has been cancelled
Create and publish Docker images with specific build args / copy-to-dockerhub (-ollama, ollama) (push) Has been cancelled
Create and publish Docker images with specific build args / copy-to-dockerhub (-slim, slim) (push) Has been cancelled
* i18n: add pt-BR translations for newly added UI items and consistency pass (#26391)

New **pt-BR** translations for items introduced in the latest releases, plus a consistency/quality pass across existing strings (grammar, tone, capitalization, pluralization). Placeholders and hotkeys preserved. No logic changes.

* refac

* i18n(th-TH): translate missing Thai keys/fix typo (#26406)

* refac

* refac

* refac

* refac

* refac

* refac

* refac

* refac

* refac

* fix: use updated_at for sidebar chat timestamp (#26454)

The sidebar time-ago indicator rendered `created_at`, so the relative time stayed pinned to the chat's creation age and never reflected new activity. After sending a message the chat would jump to the top of the list (which sorts by `updated_at`) while still showing a stale label such as "3w", which is confusing.

The indicator was originally added using `updated_at` and was inadvertently switched to `created_at` during a later refactor. Restore `updated_at` (falling back to `created_at` when absent) so the timestamp matches the list ordering and updates whenever a chat is modified.

Fixes #26451

* fix: use absolute indexURL for pyodide sandbox (#26625)

* i18n: fix Spanish relative time labels (#26463)

* Update and fix Catalan translation.json (#26409)

* refac

* refac

Co-Authored-By: Syed Osama Ali Shah <86572800+osamaali313@users.noreply.github.com>

* refac

Co-Authored-By: Syed Osama Ali Shah <86572800+osamaali313@users.noreply.github.com>

* refac

* refac

* refac

* refac

* refac

* refac

* refac

* refac

* refac

* refac

* refac

* fix: derive content from output for search (#26405)

* refac

* refac

* refac

* refac

* refac

* refac

* refac

* refac

* refac

* refac

* Update CHANGELOG.md (#26641)

* Update CHANGELOG.md

* Update CHANGELOG.md

---------

Co-authored-by: Tim Baek <tim@openwebui.com>

---------

Co-authored-by: joaoback <156559121+joaoback@users.noreply.github.com>
Co-authored-by: Sicknine <156204309+SNaytiP@users.noreply.github.com>
Co-authored-by: Classic298 <27028174+Classic298@users.noreply.github.com>
Co-authored-by: Algorithm5838 <108630393+Algorithm5838@users.noreply.github.com>
Co-authored-by: JuanMa Diaz <torgus@gmail.com>
Co-authored-by: Aleix Dorca <aleixdorca@mac.com>
Co-authored-by: Syed Osama Ali Shah <86572800+osamaali313@users.noreply.github.com>
2026-07-01 03:40:54 -05:00
Classic298
4b55e69640
Update CHANGELOG.md (#26641)
Some checks failed
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 / notify-helm-charts (push) Blocked by required conditions
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 / 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 / copy-to-dockerhub (-cuda126, cuda126) (push) Blocked by required conditions
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
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/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 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/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 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 / merge (map[name:slim suffix:-slim]) (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 (-slim, slim) (push) Blocked by required conditions
Frontend Build / Unit Tests (push) Waiting to run
Python CI / Ruff Format (3.11) (push) Has been cancelled
Python CI / Ruff Format (3.12) (push) Has been cancelled
* Update CHANGELOG.md

* Update CHANGELOG.md

---------

Co-authored-by: Tim Baek <tim@openwebui.com>
2026-07-01 03:38:33 -05:00
Timothy Jaeryang Baek
90eca2ac25 refac 2026-07-01 03:37:35 -05:00
Timothy Jaeryang Baek
2e5b094cea refac 2026-07-01 03:36:30 -05:00
Timothy Jaeryang Baek
80af65c24a refac 2026-07-01 03:35:46 -05:00
Timothy Jaeryang Baek
7f182ea063 refac 2026-07-01 03:35:27 -05:00
Timothy Jaeryang Baek
54f31c630a refac 2026-07-01 03:32:12 -05:00
Timothy Jaeryang Baek
52ee5cb1b3 refac 2026-07-01 03:29:44 -05:00
Timothy Jaeryang Baek
4351c78b1e refac 2026-07-01 03:22:13 -05:00
Timothy Jaeryang Baek
fa2abe4cb6 refac 2026-07-01 03:17:06 -05:00
Timothy Jaeryang Baek
c98d8ecacc refac 2026-07-01 03:16:54 -05:00
Timothy Jaeryang Baek
092b5857bb refac 2026-07-01 03:09:26 -05:00
Algorithm5838
0e5540c2b2
fix: derive content from output for search (#26405) 2026-07-01 03:05:47 -05:00
Timothy Jaeryang Baek
73c0be8389 refac 2026-07-01 03:01:46 -05:00
Timothy Jaeryang Baek
645cd9327c refac 2026-07-01 02:59:14 -05:00
Timothy Jaeryang Baek
9562f036f8 refac 2026-07-01 02:57:29 -05:00
Timothy Jaeryang Baek
c416c6cad6 refac 2026-07-01 02:53:42 -05:00
Timothy Jaeryang Baek
4b08d65597 refac 2026-07-01 02:53:22 -05:00
Timothy Jaeryang Baek
0016266c06 refac 2026-07-01 02:49:33 -05:00
Timothy Jaeryang Baek
650b817925 refac 2026-07-01 02:49:28 -05:00
Timothy Jaeryang Baek
64b92ff08a refac 2026-07-01 02:48:29 -05:00
Timothy Jaeryang Baek
b6d4baeb7e refac 2026-07-01 02:28:22 -05:00
Timothy Jaeryang Baek
989c6c13f5 refac 2026-07-01 02:26:47 -05:00
Timothy Jaeryang Baek
8fe480250f refac 2026-07-01 02:25:20 -05:00
Timothy Jaeryang Baek
ab22fe64bd refac
Co-Authored-By: Syed Osama Ali Shah <86572800+osamaali313@users.noreply.github.com>
2026-07-01 02:20:50 -05:00
Timothy Jaeryang Baek
688bda09fb refac
Co-Authored-By: Syed Osama Ali Shah <86572800+osamaali313@users.noreply.github.com>
2026-07-01 02:20:16 -05:00
Timothy Jaeryang Baek
19d8f03bd2 refac 2026-07-01 02:15:16 -05:00
Aleix Dorca
9ed7af823c
Update and fix Catalan translation.json (#26409) 2026-07-01 02:11:24 -05:00
JuanMa Diaz
980786f574
i18n: fix Spanish relative time labels (#26463) 2026-07-01 02:10:37 -05:00
Algorithm5838
a78f0c0302
fix: use absolute indexURL for pyodide sandbox (#26625) 2026-07-01 02:09:19 -05:00
Classic298
2d7fc04bfb
fix: use updated_at for sidebar chat timestamp (#26454)
The sidebar time-ago indicator rendered `created_at`, so the relative time stayed pinned to the chat's creation age and never reflected new activity. After sending a message the chat would jump to the top of the list (which sorts by `updated_at`) while still showing a stale label such as "3w", which is confusing.

The indicator was originally added using `updated_at` and was inadvertently switched to `created_at` during a later refactor. Restore `updated_at` (falling back to `created_at` when absent) so the timestamp matches the list ordering and updates whenever a chat is modified.

Fixes #26451
2026-07-01 02:08:56 -05:00
Timothy Jaeryang Baek
9866a02863 refac 2026-07-01 02:08:34 -05:00
Timothy Jaeryang Baek
2ed8934f5b refac 2026-07-01 02:05:14 -05:00
Timothy Jaeryang Baek
56ee875e21 refac 2026-07-01 01:46:36 -05:00
Timothy Jaeryang Baek
0b0eec05f8 refac 2026-07-01 00:43:00 -05:00
Timothy Jaeryang Baek
58ef80d8f7 refac 2026-07-01 00:10:23 -05:00
Timothy Jaeryang Baek
af1c0eee89 refac 2026-06-30 20:20:15 -05:00
Timothy Jaeryang Baek
0b75445ff9 refac 2026-06-30 20:20:10 -05:00
Timothy Jaeryang Baek
139206f0fe refac 2026-06-30 16:59:02 -05:00
Timothy Jaeryang Baek
6ed56b07c4 refac 2026-06-30 16:55:25 -05:00
Sicknine
1014c212a4
i18n(th-TH): translate missing Thai keys/fix typo (#26406) 2026-06-30 15:18:18 -05:00
Timothy Jaeryang Baek
4067e357b2 refac
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: 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 / notify-helm-charts (push) Blocked by required conditions
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 / 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 / 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 (-ollama, ollama) (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 / Format & Build (push) Waiting to run
Frontend Build / Unit Tests (push) Waiting to run
2026-06-29 21:31:49 -05:00
joaoback
651a02ed2f
i18n: add pt-BR translations for newly added UI items and consistency pass (#26391)
New **pt-BR** translations for items introduced in the latest releases, plus a consistency/quality pass across existing strings (grammar, tone, capitalization, pluralization). Placeholders and hotkeys preserved. No logic changes.
2026-06-29 21:14:01 -05:00
Tim Baek
b711935dd5
Merge pull request #26388 from open-webui/dev
Some checks failed
Python CI / Ruff Format (3.11) (push) Has been cancelled
Python CI / Ruff Format (3.12) (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_CUDA=true free_disk:true name:cuda suffix:-cuda]) (push) Has been cancelled
Frontend Build / Format & Build (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
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
Frontend Build / Unit Tests (push) Has been cancelled
Release to PyPI / release (push) Has been cancelled
Release / publish (push) Has been cancelled
Create and publish Docker images with specific build args / notify-helm-charts (push) Has been cancelled
Create and publish Docker images with specific build args / merge (map[name:cuda suffix:-cuda]) (push) Has been cancelled
Create and publish Docker images with specific build args / merge (map[name:cuda126 suffix:-cuda126]) (push) Has been cancelled
Create and publish Docker images with specific build args / merge (map[name:main suffix:]) (push) Has been cancelled
Create and publish Docker images with specific build args / merge (map[name:ollama suffix:-ollama]) (push) Has been cancelled
Create and publish Docker images with specific build args / merge (map[name:slim suffix:-slim]) (push) Has been cancelled
Create and publish Docker images with specific build args / copy-to-dockerhub (, main) (push) Has been cancelled
Create and publish Docker images with specific build args / copy-to-dockerhub (-cuda, cuda) (push) Has been cancelled
Create and publish Docker images with specific build args / copy-to-dockerhub (-cuda126, cuda126) (push) Has been cancelled
Create and publish Docker images with specific build args / copy-to-dockerhub (-ollama, ollama) (push) Has been cancelled
Create and publish Docker images with specific build args / copy-to-dockerhub (-slim, slim) (push) Has been cancelled
0.10.1
2026-06-29 14:38:32 -05:00
Timothy Jaeryang Baek
86231e4438 refac
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: 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 / notify-helm-charts (push) Blocked by required conditions
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 / 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 / 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 (-ollama, ollama) (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 / Format & Build (push) Waiting to run
Frontend Build / Unit Tests (push) Waiting to run
2026-06-29 14:35:39 -05:00
Timothy Jaeryang Baek
f6baa1bb77 refac 2026-06-29 14:34:22 -05:00
Tim Baek
4d2e13cf2b
Merge pull request #26385 from open-webui/dev 2026-06-29 14:17:36 -05:00
Timothy Jaeryang Baek
5134e5ecfc refac 2026-06-29 14:03:11 -05:00
Timothy Jaeryang Baek
caadfdec0b refac
Co-Authored-By: Jannik S. <jannik@streidl.dev>
2026-06-29 13:47:39 -05:00
Timothy Jaeryang Baek
9af700cc4f chore: bump 2026-06-29 13:45:43 -05:00
Timothy Jaeryang Baek
0c7908b9f2 chore: format 2026-06-29 13:45:00 -05:00
Timothy Jaeryang Baek
06c169f73f chore: format 2026-06-29 13:42:24 -05:00
Timothy Jaeryang Baek
29d0113b37 refac 2026-06-29 13:41:57 -05:00
Classic298
6e3020b942
Update translation.json (#26386) 2026-06-29 13:40:25 -05:00
Timothy Jaeryang Baek
832fc3af84 doc: changelog 2026-06-29 13:39:46 -05:00
Timothy Jaeryang Baek
bc9e77384e doc 2026-06-29 13:39:32 -05:00
Timothy Jaeryang Baek
3dc526475d refac 2026-06-29 13:39:29 -05:00
Timothy Jaeryang Baek
89709f5f80 refac 2026-06-29 13:39:08 -05:00
Classic298
09a1c3a948
chore: CHANGELOG (#26016)
* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md
2026-06-29 13:37:36 -05:00
Timothy Jaeryang Baek
403392b41b chore: format 2026-06-29 13:35:39 -05:00
Timothy Jaeryang Baek
c33fadc266 refac 2026-06-29 13:30:32 -05:00
Timothy Jaeryang Baek
0443ab3a61 refac 2026-06-29 13:30:28 -05:00
Timothy Jaeryang Baek
22a44e67a8 refac 2026-06-29 13:15:29 -05:00
Timothy Jaeryang Baek
24b8619f64 refac 2026-06-29 13:15:11 -05:00
Timothy Jaeryang Baek
3319b6410e refac 2026-06-29 13:14:42 -05:00
Timothy Jaeryang Baek
37d45fdee3 refac 2026-06-29 13:14:13 -05:00
Timothy Jaeryang Baek
55cb98ff56 refac 2026-06-29 13:03:30 -05:00
Timothy Jaeryang Baek
517cd8d102 refac 2026-06-29 13:03:14 -05:00
Timothy Jaeryang Baek
7ea7680f56 refac 2026-06-29 12:59:05 -05:00
Timothy Jaeryang Baek
2c804b0ac4 refac 2026-06-29 12:55:10 -05:00
Timothy Jaeryang Baek
589b62b529 refac 2026-06-29 12:52:51 -05:00
Timothy Jaeryang Baek
21ac7e95a3 refac 2026-06-29 12:51:20 -05:00
Timothy Jaeryang Baek
fb27716186 refac 2026-06-29 12:46:37 -05:00
Timothy Jaeryang Baek
37a9da50df refac 2026-06-29 12:46:32 -05:00
Timothy Jaeryang Baek
db9977926c refac 2026-06-29 12:43:32 -05:00
Timothy Jaeryang Baek
c0c6c2181a refac 2026-06-29 12:40:20 -05:00
Timothy Jaeryang Baek
ae5d23f226 refac 2026-06-29 12:38:04 -05:00
G30
c584a4270c
perf(ui): non-blocking model and tool server loading for unreachable connections (#26289)
- Move setModels() and setToolServers() out of the loaded gate in
  +layout.svelte so the page renders immediately instead of blocking
  behind slow/unreachable connection timeouts
- Track failed Ollama backend URLs from /api/tags and skip them in the
  subsequent /api/ps fan-out, eliminating a redundant second timeout
- Silence the autocomplete toast that fires during the transient empty
  model state
2026-06-29 12:34:00 -05:00
Shirasawa
91aea7fe8c
fix: Fixed the error message display for non-standard SSE (#23228) 2026-06-29 12:31:41 -05:00
Timothy Jaeryang Baek
b4073f6378 refac 2026-06-29 12:29:10 -05:00
Timothy Jaeryang Baek
bb6b2db88b refac 2026-06-29 12:26:23 -05:00
Timothy Jaeryang Baek
248315de14 refac 2026-06-29 12:25:29 -05:00
Timothy Jaeryang Baek
75db531c12 refac 2026-06-29 12:16:58 -05:00
Timothy Jaeryang Baek
c89fd237b8 refac 2026-06-29 12:16:05 -05:00
G30
6fd8c599c1
fix(ui): workspace models bulk actions and search pagination (#26274) 2026-06-29 12:14:15 -05:00
Gateway
877221c118
chore(deps): patch frontend security dependencies (#26281) 2026-06-29 12:13:57 -05:00
Timothy Jaeryang Baek
2856def6c0 refac 2026-06-29 12:12:23 -05:00
Timothy Jaeryang Baek
d6cda4a04b refac 2026-06-29 12:12:12 -05:00
Timothy Jaeryang Baek
fe3300bd65 refac 2026-06-29 12:10:39 -05:00
Timothy Jaeryang Baek
783205a965 refac 2026-06-29 12:09:16 -05:00
G30
dc1bc41d2e
fix(ui): fix pinned note sidebar drag-and-drop into chat context (#26384) 2026-06-29 12:05:39 -05:00
Timothy Jaeryang Baek
655afbe90b refac 2026-06-29 11:59:50 -05:00
Timothy Jaeryang Baek
6f8221df58 refac 2026-06-29 11:59:29 -05:00
Timothy Jaeryang Baek
ff5cec43bd refac 2026-06-29 11:56:00 -05:00
Timothy Jaeryang Baek
10558173fb refac 2026-06-29 11:53:29 -05:00
Timothy Jaeryang Baek
754787f43d refac 2026-06-29 11:51:45 -05:00
Classic298
27c97bfe96
i18n: complete German (de-DE) translations (#26383)
Translate all remaining untranslated strings in the German locale: 228 empty
values plus the genuinely translatable members of the value-equals-key set (240
entries total). Covers webhooks/events, external knowledge sources, context
compaction, terminal servers, OAuth/OIDC claims, skills, and directory
management.

Translations follow the existing locale conventions: formal "Sie" address,
established German terms (Websuche, Wissen, Werkzeuge, Wissensspeicher), and
brand/proper nouns, format names, and identical-in-German words left untouched.
2026-06-29 11:47:56 -05:00
Timothy Jaeryang Baek
c6ec1a3484 refac 2026-06-29 11:46:08 -05:00
Timothy Jaeryang Baek
40b655e99e refac 2026-06-29 11:44:21 -05:00
Timothy Jaeryang Baek
b696c5deff refac 2026-06-29 11:38:59 -05:00
Algorithm5838
7572283517
fix: persist control revert to loaded value (#25793) 2026-06-29 11:37:30 -05:00
Timothy Jaeryang Baek
61cee42ded refac 2026-06-29 11:35:08 -05:00
Timothy Jaeryang Baek
815446d5bb refac 2026-06-29 11:33:49 -05:00
Timothy Jaeryang Baek
a146e17bdc refac 2026-06-29 11:33:32 -05:00
Timothy Jaeryang Baek
2c4e1fce8f refac 2026-06-29 11:13:36 -05:00
Timothy Jaeryang Baek
81e245548d refac 2026-06-29 11:00:53 -05:00
Timothy Jaeryang Baek
4ed45ce843 refac 2026-06-29 10:58:57 -05:00
Timothy Jaeryang Baek
2d3035a112 refac 2026-06-29 10:54:53 -05:00
Timothy Jaeryang Baek
39837e0a3a refac 2026-06-29 10:51:52 -05:00
G30
8c7428122b
fix(rag): allow special tokens in document text during chunk measurement (#26210)
Documents (especially AI/LLM documentation) legitimately contain special token strings like <|endoftext|> as literal text. The tiktoken encoder raises a ValueError when encountering these during chunk size measurement in merge_docs_to_target_size(), preventing the entire file from being indexed. Pass disallowed_special=() to encoding.encode() to treat all text as normal content.
2026-06-29 10:43:24 -05:00
Juan Calderon-Perez
51246bcb31
perf(backend): offload blocking calls in async paths to threads (#26381)
Audit of asyncio.sleep vs time.sleep and event-loop-blocking calls:

- utils/plugin.py: run pip `install_frontmatter_requirements`
  (subprocess.check_call) via asyncio.to_thread in load_tool_module_by_id,
  load_function_module_by_id, and install_tool_and_function_dependencies.
- retrieval/utils.py: move the synchronous SSRF-guarded requests probe and
  loader.load() in get_content_from_url into a sync helper run via
  asyncio.to_thread.
- routers/audio.py: write uploaded audio to disk off the event loop in
  transcription().
- routers/pipelines.py: write uploaded pipeline file off the event loop in
  upload_pipeline().

The existing time.sleep call sites are all in genuinely synchronous
functions (sync requests/DB drivers/daemon threads) with async
counterparts that already use asyncio.sleep, so no time.sleep -> asyncio.sleep
changes were needed.


Claude-Session: https://claude.ai/code/session_01LXR5bYfsfSS42RGHQZu2Ta

Co-authored-by: Claude <noreply@anthropic.com>
2026-06-29 10:42:26 -05:00
Timothy Jaeryang Baek
c7e634776d refac 2026-06-29 10:40:42 -05:00
Timothy Jaeryang Baek
a5c9459401 refac 2026-06-29 10:40:36 -05:00
Timothy Jaeryang Baek
5055fb85aa refac 2026-06-29 10:40:22 -05:00
Timothy Jaeryang Baek
7ed7e81e84 refac 2026-06-29 10:39:54 -05:00
Timothy Jaeryang Baek
303c426c3f refac 2026-06-29 10:39:47 -05:00
Timothy Jaeryang Baek
f9c3ccd869 refac 2026-06-29 10:04:29 -05:00
Timothy Jaeryang Baek
eb53281c9a refac
Co-Authored-By: Classic298 <27028174+Classic298@users.noreply.github.com>
2026-06-29 06:08:31 -05:00
Timothy Jaeryang Baek
a285a390c1 refac 2026-06-29 06:01:05 -05:00
Classic298
75df948f34
feat: forward client User-Agent to model backends via {{USER_AGENT}} placeholder (#26333)
Adds a {{USER_AGENT}} custom-header placeholder that relays the inbound
client's User-Agent to upstream model backends, so providers see the real
client instead of Open WebUI's internal aiohttp UA. This makes upstream
usage/cost attribution and backend telemetry possible, and is opt-in
per-connection (no global flag): admins add {{USER_AGENT}} to a connection's
custom headers in Admin > Settings > Connections.

The placeholder is sourced from the live inbound request (with a metadata
fallback for detached RAG/tool calls), so it resolves on every prompt-sending
path, not just chat completions:

- OpenAI completions, Responses API, and proxy — all route through
  get_headers_and_cookies, which now passes the request into get_custom_headers.
- Anthropic Messages API (/api/v1/messages) — already covered, it delegates
  to the chat completion handler.
- Ollama (/api/chat, /v1/completions, /v1/chat/completions, /v1/messages,
  /v1/responses) — previously had no custom-header support at all; send_request
  now applies per-connection custom headers (with templating) for every
  Ollama prompt endpoint.

Custom headers are applied after the built-in user-info headers so explicit
admin-configured headers take precedence. The other existing placeholders
({{CHAT_ID}}, {{USER_ID}}, ...) now also work on the newly covered paths.

Frontend: the connection editor's Headers field is now shown for Ollama
connections too (previously gated to non-Ollama), so the placeholder can be
configured there.

Ref: open-webui/open-webui#26159
2026-06-29 05:58:19 -05:00
Timothy Jaeryang Baek
260f3c3a22 refac 2026-06-29 05:56:08 -05:00
Timothy Jaeryang Baek
b0487dd6dd refac 2026-06-29 05:49:54 -05:00
Timothy Jaeryang Baek
70e4ffcc65 refac 2026-06-29 05:49:49 -05:00
Timothy Jaeryang Baek
0883638027 refac 2026-06-29 05:47:21 -05:00
Timothy Jaeryang Baek
ee5de69e37 refac 2026-06-29 05:46:55 -05:00
Timothy Jaeryang Baek
0cc331d1c6 refac 2026-06-29 05:46:51 -05:00
Timothy Jaeryang Baek
41f256321b refac 2026-06-29 05:46:47 -05:00
Timothy Jaeryang Baek
44b9463498 refac 2026-06-29 05:44:47 -05:00
Timothy Jaeryang Baek
e6d35fc4cc refac 2026-06-29 05:39:17 -05:00
Timothy Jaeryang Baek
396d9ac181 refac 2026-06-29 05:27:51 -05:00
Timothy Jaeryang Baek
67a7b23b85 refac 2026-06-29 05:26:34 -05:00
Timothy Jaeryang Baek
edf2c6c8f7 refac 2026-06-29 05:23:20 -05:00
Timothy Jaeryang Baek
0eba3df119 refac 2026-06-29 05:21:49 -05:00
Timothy Jaeryang Baek
aa851d93c6 refac 2026-06-29 05:18:12 -05:00
Timothy Jaeryang Baek
fa76764c3b refac 2026-06-29 05:16:57 -05:00
Timothy Jaeryang Baek
83ec36cd38 refac 2026-06-29 05:15:55 -05:00
Timothy Jaeryang Baek
cdd7b88bec refac 2026-06-29 05:14:57 -05:00
Timothy Jaeryang Baek
8927c9bb3d refac 2026-06-29 05:14:34 -05:00
Timothy Jaeryang Baek
422a4768ea refac 2026-06-29 05:14:03 -05:00
maco
c65b29ec0f
fix: handle MCP resource content type blob and URI variants (#25260) 2026-06-29 05:12:11 -05:00
Timothy Jaeryang Baek
0be069c165 refac 2026-06-29 05:04:00 -05:00
G30
5ffd4e53c3
fix(ui): prevent long default group name from overflowing admin settings layout (#25685) 2026-06-29 04:59:01 -05:00
G30
4fa3a74827
fix(chat): prevent sortable sidebar drags from triggering file upload overlay (#25675)
Add a custom MIME type (application/x-open-webui-drag) to intentional
chat and folder drag sources. The onDragOver handler in MessageInput
now checks for this type instead of the generic text/plain, which
SortableJS also sets during reorder operations for pinned menu items
(Notes, Workspace) and pinned Models.
2026-06-29 04:58:30 -05:00
Timothy Jaeryang Baek
416baef813 refac 2026-06-29 04:57:28 -05:00
Timothy Jaeryang Baek
45fea34bd0 refac
Co-Authored-By: Pedro Machado <45520277+pedrofcm@users.noreply.github.com>
2026-06-29 04:46:56 -05:00
Timothy Jaeryang Baek
953432b5fe refac 2026-06-29 04:43:40 -05:00
Timothy Jaeryang Baek
e5b5e5917b refac 2026-06-29 04:43:08 -05:00
Timothy Jaeryang Baek
67c9de8efd refac 2026-06-29 04:42:59 -05:00
Timothy Jaeryang Baek
a3b487422d refac 2026-06-29 04:42:02 -05:00
G30
f53ec857c0
feat: support folder, note, and model drag-and-drop from sidebar as context references (#25771)
* feat(ui): add drag-to-reorder for pinned notes in sidebar

* feat(chat): support folder, note, and model drag-and-drop from sidebar as context references

---------

Co-authored-by: Tim Baek <tim@openwebui.com>
2026-06-29 04:41:16 -05:00
Timothy Jaeryang Baek
b617d56c60 refac 2026-06-29 04:37:38 -05:00
Alberto de la Cruz
718b226177
fix: discover MCP Protected Resource Metadata when server does not return 401 (#25980)
get_protected_resource_metadata() only attempted RFC 9728 discovery when
the anonymous `initialize` probe returned 401 with a WWW-Authenticate
header. Some remote MCP servers — notably Google's Gmail/Drive/Calendar
MCPs (gmailmcp.googleapis.com, etc.) — answer 200 to an anonymous
initialize, so OAuth scope and authorization-server discovery silently
failed and connections to them could not be established.

Run the discovery regardless of the probe's HTTP status: prefer the
resource_metadata URL from WWW-Authenticate when present, and otherwise
fall back to the RFC 9728 §4.2 well-known URIs. The trade-off is a couple
of extra well-known GETs during MCP connection setup for servers that
expose no PRM document; behavior for 401-responding servers is unchanged.
2026-06-29 04:34:36 -05:00
Timothy Jaeryang Baek
f8ec63203c refac 2026-06-29 04:31:43 -05:00
Timothy Jaeryang Baek
fd7a59d37a refac 2026-06-29 04:27:48 -05:00
G30
ea6d02da0f
fix(ui): use correct field name for prompt clone title suffix (#25800) 2026-06-29 04:25:21 -05:00
Timothy Jaeryang Baek
ab84bbf08c refac 2026-06-29 04:19:33 -05:00
Timothy Jaeryang Baek
b58b0ea7ca refac 2026-06-29 04:13:17 -05:00
Timothy Jaeryang Baek
d3676b4f71 refac 2026-06-29 04:11:46 -05:00
Timothy Jaeryang Baek
c4688b958d refac 2026-06-29 04:03:06 -05:00
Timothy Jaeryang Baek
33b91bd8ae refac 2026-06-29 03:58:00 -05:00
Classic298
4c05abbe59
fix: respect model visibility in workspace base model selector (#25668)
The base model selector in the model creation workspace listed every
model from the global store, ignoring the per-model visibility (hidden)
flag. Models marked as not visible were correctly hidden from the chat
model selector but still appeared in the workspace 'Base Model (From)'
dropdown.

Filter out hidden models (info.meta.hidden) to match the chat selector
behavior, while still keeping a model visible if it is the currently
selected base, so editing a preset built on a now-hidden base does not
show a blank dropdown.

Fixes #25665

https://claude.ai/code/session_01CUvzYN9DjvwTpNCT5QL33B

Co-authored-by: Claude <noreply@anthropic.com>
2026-06-29 03:51:26 -05:00
Classic298
0fc630b34b
fix: per-user model cache used static key= (cross-user model exposure) (#25783)
routers/openai.py and routers/ollama.py decorated get_all_models with
`@cached(key=lambda _, user: ...)`. In aiocache 0.12, `key=` is a STATIC
cache key: get_cache_key returns `self.key` verbatim (the lambda object)
without calling it, so every caller collides to ONE shared entry within
the TTL. The intended per-user namespacing never happened — one user's
permission-filtered model list could be served to another user (or an
anonymous caller) during the cache window.

The per-call hook is `key_builder=` (called as key_builder(func, *args,
**kwargs)). Switch both sites to key_builder with a (func, request,
user=None) signature so the key is built per call. user=None mirrors
ollama's optional-user signature and stays correct whether user is passed
positionally, as a kwarg, or omitted.

Verified offline: old form returns the same key object for distinct users
(collision); new form yields distinct openai_all_models_<id> /
ollama_all_models_<id> keys, and the unauthenticated base key when no user.
These two were the only @cached(key=lambda ...) sites in the backend.
2026-06-29 03:51:02 -05:00
G30
ee11069ef2
perf(ollama): offload multi-GB model file I/O with asyncio.to_thread (#25829)
The Ollama model upload and download handlers perform three stages
of sync file I/O on multi-GB model files inside async handlers:

1. Persist upload — file.file.read() + write() in a loop
2. SHA-256 hash — calculate_sha256() reads the file sequentially
3. Read for blob push — open().read() loads entire model into memory

All three stages block the event loop for the duration of the I/O.
For a 4GB model, each stage freezes the event loop for 10+ seconds
while every other user's request stalls.

Wrap each blocking stage in asyncio.to_thread() in both handlers:

- upload_model(): persist upload, calculate_sha256, blob read
- download_file_stream(): calculate_sha256, blob read

Benchmark (full pipeline: write + SHA-256 + read back, 3 trials):
- 200MB: max jitter 145ms → 1ms (145x improvement)
- 500MB: max jitter 1,026ms → 1ms (1,026x improvement)

File I/O is pure I/O — no GIL contention. asyncio.to_thread()
completely eliminates event loop blocking.
2026-06-29 03:48:48 -05:00
G30
b05be8a907
fix(notes): refresh pinned notes sidebar when a note is deleted (#25640) 2026-06-29 03:48:22 -05:00
Classic298
a66477b710
fix: bind channel thread parent/reply to the URL channel (#25766)
GET /api/v1/channels/{id}/messages/{message_id}/thread authorized only the URL channel, but get_messages_by_parent_id() appended the thread parent (loaded by id) without checking it belonged to that channel, so a caller could read a message from a channel they cannot access by passing its id as the thread root. Require the parent to be in the requested channel before returning it, and reject a posted parent_id/reply_to_id that does not belong to the channel.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 03:46:50 -05:00
G30
7b1aa749eb
perf(retrieval): make URL fetch and file hash non-blocking with asyncio.to_thread (#25822)
- Wrap get_content_from_url() in asyncio.to_thread() inside
  get_sources_from_items() to prevent sync requests.get() from blocking
  the event loop when users attach URL sources to chat messages.
  The same function was already properly wrapped at retrieval.py:1839.

- Wrap hashlib.sha256(contents).hexdigest() in asyncio.to_thread()
  inside upload_file_handler() to prevent CPU-bound hashing from
  blocking the event loop during file uploads (44ms/100MB, scales
  linearly with file size).
2026-06-29 03:46:24 -05:00
Timothy Jaeryang Baek
958237473f refac 2026-06-29 03:42:49 -05:00
Timothy Jaeryang Baek
a70a6589af refac 2026-06-29 03:42:36 -05:00
Timothy Jaeryang Baek
4bc4630721 refac 2026-06-29 03:40:18 -05:00
G30
a8e5f0a54d
fix(ui): prevent space key from opening file/directory during rename in knowledge base (#25627) 2026-06-29 03:37:09 -05:00
maco
8bc4ac2641
fix: add aria-pressed and aria-label to Web Search and Image Generation toggles (#25258) 2026-06-29 03:36:37 -05:00
Classic298
58f9170319
fix: trim terminal bearer token so whitespace can't break the WebSocket (#25686)
A trailing space in the Open Terminal bearer token broke only the
interactive terminal: HTTP calls put the token in the Authorization
header, where the spec strips trailing whitespace, so the connection
test, file listing and tool calls all worked. The terminal WebSocket
can't set headers from the browser, so it sends the token inside a JSON
auth message that preserves the space verbatim, failing auth with
[Connection closed]. Normalize the key on save so whitespace never
enters storage, and trim it in the WebSocket auth message so existing
saved configs work without re-saving.


Fixes #25613

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 03:35:19 -05:00
Classic298
e98730b20d
fix: pass web search results to model when embedding & retrieval enabled (#25600)
Web search results stored as a vector collection were silently dropped
before retrieval when BYPASS_RETRIEVAL_ACCESS_CONTROL is False (the
default). The server-generated web_search file item carries a
'collection_name' but its 'web_search' type is not matched by any
explicit dispatch branch in get_sources_from_items, so it fell through
to the untrusted client-supplied collection_name branch and was ignored.

Add an explicit branch for type == 'web_search' items so the collection
is queried again. Access control is preserved: the collection still
passes through filter_accessible_collections, which already allowlists
web-search-* and bypasses only for admins.

Regression introduced when the retrieval access-control hardening gated
the bare collection_name fallback behind BYPASS_RETRIEVAL_ACCESS_CONTROL.
2026-06-29 03:34:55 -05:00
Timothy Jaeryang Baek
9802b0d135 refac 2026-06-29 03:33:49 -05:00
G30
ed2d7d4acd
fix(i18n): standardize title case for settings toggle labels (#25765) 2026-06-29 03:30:40 -05:00
Timothy Jaeryang Baek
f85e906dec refac 2026-06-29 03:28:50 -05:00
G30
70b89d01c2
feat(ui): add drag-to-reorder for pinned notes in sidebar (#25677) 2026-06-29 03:22:33 -05:00
Timothy Jaeryang Baek
3fd0384ffc refac 2026-06-29 03:21:41 -05:00
G30
78d276b4ff
fix(ui): update groups count to reflect filtered search results (#25689) 2026-06-29 03:21:31 -05:00
Classic298
5796d44363
fix: defer missing-local-embedding error so it can't crash boot (#25683)
get_embedding_function runs at import time (main.py), so the empty-engine
+ no-model guard added in 55ca719b raised ValueError during startup and
bricked the instance: a blank embedding model set via the UI made the app
unbootable, with no way back into settings to undo it. Move the check into
the returned coroutine so construction always succeeds and the error only
fires when something actually embeds, surfacing as a clear request error
instead of the old cryptic 'NoneType has no attribute encode'.


Fixes #25634
Fixes #25165

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 03:21:09 -05:00
G30
6e14e446cb
fix(api): handle orphaned shared_chat rows when unsharing (#25632) 2026-06-29 03:20:46 -05:00
Timothy Jaeryang Baek
c93d4f04aa refac 2026-06-29 03:20:02 -05:00
Timothy Jaeryang Baek
33cd199e6d refac 2026-06-29 03:16:59 -05:00
G30
4712544d5e
feat(search): add chat context menu to search modal (#25490) 2026-06-29 03:16:03 -05:00
maco
958fdbdc88
feat: add ariaLabel prop to Switch component (#25230) 2026-06-29 03:15:11 -05:00
Timothy Jaeryang Baek
390e200f76 refac 2026-06-29 03:13:17 -05:00
Kylapaallikko
462b66b807
Update fi-FI translation.json (#25672)
Added missing translations and improved existing ones.
2026-06-29 03:07:49 -05:00
Timothy Jaeryang Baek
388f62f8a0 refac 2026-06-29 03:03:36 -05:00
Timothy Jaeryang Baek
7be009649a refac 2026-06-29 02:57:58 -05:00
Classic298
534206095f
fix: route Account Pending sign out through backend signout flow (#25681)
The Sign Out button on the Account Pending overlay only cleared the
local token and redirected to /auth, skipping the backend signout. This
left the OAuth/OIDC session alive at the IdP, so re-login bounced the
user straight back into pending and never re-fetched updated role/group
claims. Call userSignOut() and honor the returned redirect_url, matching
the sidebar logout flow.


Fixes #25644

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 02:54:19 -05:00
G30
525c115a3a
fix(ui): remove unnecessary margin causing scrollbar on About settings page (#25802) 2026-06-29 02:54:00 -05:00
Timothy Jaeryang Baek
b34d6c836e refac 2026-06-29 02:50:55 -05:00
G30
6fdf9b4340
perf(auth): make password hashing non-blocking and batch CSV user import (#25804)
Co-authored-by: Tim Baek <tim@openwebui.com>
2026-06-29 02:45:39 -05:00
G30
18e6a10778
fix(ui): update model share URL to new community post endpoint (#25801) 2026-06-29 02:44:28 -05:00
Classic298
36d08fa2a7
fix: request file list/search without extracted content by default (#25774)
The file picker and Files modal only render filenames and metadata, but
searchFiles()/getFiles() never passed content=false, so the backend
serialized data.content (full extracted text) of every file into each
response. On instances with many large documents this turns a metadata
list into tens of MB per request, re-issued on every picker keystroke.

The backend already supports content=false on GET /files/ and
GET /files/search; this just makes the list/search callers opt out of the
payload by default. The param stays overridable for callers that need it.


Fixes #25741

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 02:43:56 -05:00
Timothy Jaeryang Baek
2bdd2ab94e refac 2026-06-29 02:43:14 -05:00
Classic298
2414dfca70
fix: mark in-place created chats as read on new chat (#25782)
A chat created in place (new chat -> first message) keeps chatIdProp
undefined because the URL is swapped via history.replaceState without a
remount, so none of the chatIdProp-gated updateLastReadAt paths fire and
the chat is never persisted with a last_read_at. Starting another new
chat via initNewChat reset $chatId without marking the outgoing chat
read, so on refresh updated_at > last_read_at (NULL) and the chat shows
as unread in the sidebar.

Mark the outgoing chat read in initNewChat, mirroring navigateHandler.


Fixes #25108

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 02:32:00 -05:00
G30
6ea591491e
perf(images): offload validate_url() DNS resolution with asyncio.to_thread (#25825)
validate_url() calls socket.getaddrinfo() for SSRF protection, which
blocks the event loop for 100-700ms per DNS lookup. This affects:

- Image generation (get_image_data) — every external image URL
- Image editing (load_url_image) — every external image URL
- OAuth profile pictures (_process_picture_url) — every login
- Webhook delivery (post_webhook) — every notification
- Image base64 conversion (get_image_base64_from_url) — chat images

Wrap all 5 async call sites in asyncio.to_thread() so DNS resolution
runs in the thread pool. The event loop remains free to serve other
requests during the lookup.

Benchmark (3 domains, 3 trials averaged):
- BEFORE: max jitter 479ms, 1 blocked ping per trial
- AFTER:  max jitter 1ms, 0 blocked pings (324x improvement)

Co-authored-by: Tim Baek <tim@openwebui.com>
2026-06-29 02:31:24 -05:00
Aleix Dorca
d4d9786434
i18n: Update catalan translation.json (#25595) 2026-06-29 02:27:11 -05:00
Timothy Jaeryang Baek
ac3449cac9 refac 2026-06-29 02:26:27 -05:00
G30
71d6212ab8
fix(redis): use await asyncio.sleep() instead of time.sleep() in async generator (#25823)
time.sleep() inside the _wrap_async_gen() async generator blocks the
entire event loop during Sentinel failover retries. The sibling method
_wrap_async_call() already correctly uses await asyncio.sleep().
2026-06-29 02:25:46 -05:00
Timothy Jaeryang Baek
2308b59f13 refac 2026-06-29 02:21:24 -05:00
Timothy Jaeryang Baek
61a2672215 refac 2026-06-29 02:20:54 -05:00
Classic298
386ac95814
fix: scope Socket.IO event-caller to the requesting user's own session (#25763)
get_event_call() routed execute:python / execute:tool events to a client-supplied session_id after only checking the session was connected, not that it belonged to the requester. Verify the target session is owned by the requesting user (metadata user_id) before delivering, so a client cannot route code/tool execution into another user's session.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 02:17:40 -05:00
Classic298
914039ac81
Escape voice-derived attributes in Azure TTS SSML (#25776)
The Azure TTS handler (_tts_azure) interpolated the user-supplied voice,
and the locale derived from it, into the SSML xml:lang and <voice name>
attributes without XML-escaping, while the text body was already escaped
(2e75c6dbd). Escape both attributes too, so every user-derived value in
the SSML document is consistently encoded.

Co-authored-by: alanturing881 <alanturing881@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 02:16:37 -05:00
G30
ff25ccca65
feat: add repeat/recurrence dropdown to calendar event modal (#25865)
* feat(calendar): add repeat/recurrence dropdown to event modal

* fix(calendar): anchor recurring event expansion to event start date

The expand_recurring_event() utility used the view range start as dtstart,
causing FREQ=WEEKLY events to land on the wrong day of the week. Use the
event's original start date instead so recurrence patterns stay correct.
2026-06-29 02:16:19 -05:00
Classic298
01198eaeef
Close DNS-rebinding SSRF gap in get_content_from_url probe (#25775)
The web-ingest probe in get_content_from_url validated the URL at resolve
time (validate_url) but then fetched with a bare requests.get, which
re-resolves the hostname at connect time. An attacker-controlled name
server can answer with a public IP during validation and an internal IP
at connect, reaching cloud metadata / loopback / internal services (blind
always; binary content-types are read back to the caller). The
connection-layer guard (#24759) that closes this for the SafeWebBaseLoader
path was never mounted on this probe.

Route the probe through the same _SSRFSafeAdapter the loader uses, so the
resolution that feeds the TCP connect is re-validated against the
global-IP check.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 02:15:31 -05:00
Classic298
15d96b1f2a
fix: chroma has_collection always returns False (name vs Collection) (#25780)
has_collection did `collection_name in self.client.list_collections()`,
but chromadb's list_collections() returns Collection objects (1.x), not
name strings — so the membership test is always False, even when the
collection exists. Compare against the collection names instead (with a
hasattr guard tolerating versions that yield plain names).

Found via the dependency-contract test suite (unit/deps/test_chromadb.py).
2026-06-29 02:15:05 -05:00
G30
342539f1e1
perf(audio): make ML model loading non-blocking with asyncio.to_thread (#25806) 2026-06-29 02:14:15 -05:00
Classic298
c31694af09
chore: Update SECURITY.md (#25773)
* Update SECURITY.md

* Update SECURITY.md

* Update SECURITY.md

* Update SECURITY.md

* Update SECURITY.md

* Update SECURITY.md

* Update SECURITY.md

* Update SECURITY.md

* Extend the already-fixed/monitoring rule to public PRs and credit

Broaden the rule from "already fixed" to also cover issues already being fixed in
the open (e.g. an open pull request), extend the commit-monitoring pattern to PRs,
and fold in the credit consequence on provable grounds rather than an unprovable
bad-faith claim: a report of an already-public, already-fixed-or-being-fixed issue
filed strictly last is a duplicate we cannot distinguish from scraping, so it earns
no advisory. Credit belongs to whoever found or fixed it, who forfeits it by
disclosing publicly instead of reporting confidentially first — so a public fix
earns no advisory and no credit for anyone.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Remove Rule 14 (One Vulnerability Per Report)

The one-CVE-per-vulnerability constraint it restated is a CVE Program counting
rule, already binding through the "Alignment with the CVE Program" section.
Dropping the standalone rule removes the duplication; bundled reports are still
split on that basis when they arise.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* security policy: surface "What a Valid Report Gets You" near the top, refresh date

Move the "What a Valid Report Gets You" section up to directly under the good-faith
reporting section (it leads with what reporters receive, rather than burying it
below the rules), and update the last-updated date.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Update SECURITY.md

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 02:11:17 -05:00
G30
964a098a4b
fix(chat): use code block language tag to detect Vega-Lite specs for rendering (#25843) 2026-06-29 02:07:11 -05:00
Classic298
c3394288bb
fix: repair Mistral OCR async upload (aiohttp.streams.FilePayload removed) (#25779)
_upload_file_async built its multipart body with
`aiohttp.streams.FilePayload(...)`, which no longer exists in aiohttp
(payload classes live in aiohttp.payload, and there is no FilePayload).
The reference sits inside a lazy closure, so import succeeds and only the
async Mistral OCR file-upload path blows up at runtime with
AttributeError on every call.

Mirror the working sync path: open the file in a context manager and let
MultipartWriter.append(file_obj, {...}) build a streaming
BufferedReaderPayload, with the POST issued inside the open() block so
the handle stays valid for the whole upload. Preserves the streaming /
memory-efficiency intent.

Found via the dependency-contract test suite (unit/deps/test_aiohttp.py),
which pins aiohttp.streams.FilePayload as absent.
2026-06-29 02:05:54 -05:00
Classic298
8a016931f1
refac(telemetry): drop deprecated semconv SpanAttributes subclass (#25784)
constants.py subclassed opentelemetry.semconv.trace.SpanAttributes, which
is deprecated since semconv 1.25.0 (emits a DeprecationWarning; the pinned
0.63b1 has it live). Source the legacy span-attribute keys from the
non-deprecated _incubating attribute modules instead:
  http.url / http.method / http.status_code  <- _incubating http_attributes
  db.name / db.statement / db.operation      <- _incubating db_attributes
The stable http module renamed these (http.request.method, ...), so only
the incubating module preserves the original values. Custom keys
(db.instance/type/ip/port, error.*, result.*) stay literals.

Verified: emitted attribute keys are byte-identical before/after for every
key instrumentors.py reads, and importing constants no longer emits a
DeprecationWarning.
2026-06-29 02:05:34 -05:00
G30
e69ce6e1c6
perf(channels): batch N+1 queries for reactions and thread replies (#25831)
Replace per-message database queries with batch IN-clause queries in
channel message handlers. This eliminates the N+1 query pattern that
caused ~102 queries per channel page load (50 messages × 2 queries each).

Changes:
- Add get_reactions_by_message_ids() to MessageTable: single query
  fetches all reactions for multiple messages using IN clause with
  User JOIN, returns dict[message_id, list[Reactions]]
- Add get_thread_reply_counts_by_message_ids() to MessageTable: single
  GROUP BY aggregate query returns (count, max_created_at) per parent,
  replacing full object loads just to call len()
- Refactor get_channel_messages(): 102 → 4 queries per page
- Refactor get_pinned_channel_messages(): 22 → 3 queries per page
- Refactor get_channel_thread_messages(): 53 → 4 queries per page
- Refactor send_notification(): N+1 membership check → batch set lookup
2026-06-29 02:05:16 -05:00
Timothy Jaeryang Baek
6050a94d77 refac 2026-06-29 02:03:58 -05:00
Classic298
5fd26b7549
docs: note pydub/audioop Python 3.13 constraint at the import (#25785)
pydub imports the stdlib `audioop`, removed in Python 3.13, so audio
preprocessing would break there. requires-python is already capped at
< 3.13; this one-line pointer flags what to handle (audioop-lts, or drop
pydub) before raising that cap.
2026-06-29 02:03:11 -05:00
G30
2a9a023172
fix(ui): add Edit option to prompts 3-dots menu (#25789) 2026-06-29 02:02:39 -05:00
Classic298
b295a20b9d
chore: bump Python backend dependencies, drop unused peewee (#25786)
* chore: bump Python backend dependencies, drop unused peewee

Minor/patch + reviewed major bumps across requirements.txt,
requirements-min.txt, pyproject.toml and uv.lock; playwright image bumped in
docker-compose.playwright.yaml. peewee/peewee-migrate removed (zero imports).

Security-relevant: cryptography 46->48, authlib 1.6.10->1.7.2, PyJWT 2.11->2.13,
requests 2.33.1->2.34.2, RestrictedPython 8.1->8.2, pillow 12.1.1->12.2.0.
Reviewed majors: redis 7->8, pymilvus ->2.6.14, azure-search-documents 11->12,
chardet 5->7, unstructured 0.18->0.22, pycrdt 0.12->0.13.

Testing:
- Resolution: `uv lock` resolves the full bumped set with no conflicts; uv.lock
  regenerated to match (peewee dropped, every pin including
  azure-search-documents==12.0.0 resolves).
- Per-dependency contract tests (external tests repo, unit/deps/): 105 files,
  2205 passed / 6 skipped, ruff-clean. One file per dependency pins the symbols,
  signatures and behaviour the backend actually uses, so an API removal/rename in
  a bumped version fails loudly instead of at runtime. Offline/deterministic.
- End-to-end embed->retrieve test driving transformers + sentence-transformers +
  chromadb together through Open WebUI's real RAG path (cached model, in-memory
  chroma, semantic retrieval asserted).
- Install/startup/health resolution gate added to the dep-bump workflow and the
  integration suite (uv/pip resolve + uvicorn /health + Playwright dev visibility).
- Bugs surfaced while testing each got an isolated fix branch + regression test:
  Mistral OCR aiohttp FilePayload (#25779), chroma has_collection (#25780),
  aiocache per-user model-cache key (security), otel semconv deprecation,
  pydub/audioop <3.13 note.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Bump python-multipart 0.0.22 -> 0.0.27 (CVE-2026-42561, CVE-2026-40347)

0.0.22 is affected by two DoS CVEs in the multipart parser that
Starlette/FastAPI run for every multipart/form-data request, so any
authenticated user hitting an upload endpoint can trigger them:
- CVE-2026-42561: unbounded part-header count/size -> CPU exhaustion (fixed 0.0.27)
- CVE-2026-40347: large multipart preamble/epilogue DoS (fixed 0.0.26)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 02:02:18 -05:00
G30
f923edcaaa
feat(ui): add Clone option to automations 3-dots menu (#25790) 2026-06-29 02:00:23 -05:00
Timothy Jaeryang Baek
c8260745a6 refac 2026-06-29 01:59:06 -05:00
G30
3c67774eb3
fix(auth): enforce settings.interface permission on /user/settings/update endpoint (#25996) 2026-06-29 01:53:20 -05:00
Timothy Jaeryang Baek
ce4a323f43 refac 2026-06-29 01:52:07 -05:00
Timothy Jaeryang Baek
b7934e9182 refac 2026-06-29 01:45:50 -05:00
Timothy Jaeryang Baek
46c1d6591b refac 2026-06-29 01:41:43 -05:00
Timothy Jaeryang Baek
3730a9eaac refac 2026-06-29 01:38:41 -05:00
Classic298
a4f7ec1fb3
Chore: Add a security-reporting link to the issue chooser (#26007)
* Add a security-reporting link to the issue chooser

With blank issues disabled, the New Issue chooser only offered Bug Report and
Feature Request, leaving security reporters no obvious route and nudging them
toward filing vulnerabilities as public issues. Add a contact_links entry that
points to the Security Policy (/security/policy), where the "Report a
vulnerability" button opens a private GitHub advisory — keeping security
reports out of public issues.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Update config.yml

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 01:36:44 -05:00
G30
677e164f29
feat(permissions): add workspace.skills_import and workspace.skills_export permissions (#25921) 2026-06-29 01:36:27 -05:00
G30
da0fd0da0d
fix(ui): add max height and scroll to chat menu tags section (#26031) 2026-06-29 01:36:03 -05:00
Timothy Jaeryang Baek
1da3b7f7e8 refac 2026-06-29 01:32:27 -05:00
Timothy Jaeryang Baek
8c57cfa645 refac 2026-06-29 01:32:09 -05:00
Timothy Jaeryang Baek
00924fbf79 refac 2026-06-29 01:32:05 -05:00
Timothy Jaeryang Baek
9df25b6932 refac 2026-06-29 01:31:31 -05:00
Timothy Jaeryang Baek
452954ff1e refac 2026-06-29 01:28:05 -05:00
Timothy Jaeryang Baek
ec8e20af35 refac 2026-06-29 01:22:14 -05:00
Classic298
89629b8f03
perf: make ToolCallDisplay parseJSONString iterative (#26146)
Replace the recursive parseJSONString with an equivalent iterative unwrap.
The recursion re-parsed its own already-parsed result until JSON.parse threw;
on scalar JSON values (e.g. "5" -> 5) that recursed until a stack overflow
which was then silently caught — wasted work on every complete tool-call
payload. The loop returns the identical value in all cases (verified
byte-identical across 29 inputs incl. double/triple-encoded and partial JSON)
without the stack churn.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 01:08:59 -05:00
Timothy Jaeryang Baek
7240517807 refac 2026-06-29 01:01:27 -05:00
G30
0502494e9a
fix(ui): fix keyboard shortcuts modal Enter key emoji and menu capitalization (#26173)
- Append Unicode text variation selector U+FE0E to the ↩ character in
  ShortcutItem.svelte to prevent iOS/iPadOS rendering it as a colorful
  emoji instead of a plain text glyph.
- Fix 'Keyboard shortcuts' → 'Keyboard Shortcuts' (capital S) in the
  user menu to match the modal title and standard title case.
2026-06-29 00:59:07 -05:00
Hrushikesh Yadav
1f6336fd98
fix: strip whitespace from user info headers to prevent MCP connection failures (#26182) 2026-06-29 00:57:07 -05:00
alvarellos
368b4a5b22
solve-valves-icon-disappear-issue (#26256) 2026-06-29 00:56:44 -05:00
Timothy Jaeryang Baek
b308391527 refac 2026-06-29 00:47:49 -05:00
Timothy Jaeryang Baek
b854eb09b1 refac 2026-06-29 00:46:45 -05:00
Algorithm5838
62b153749a
perf: skip re-sanitizing unchanged HTML tokens (#26325) 2026-06-29 00:43:54 -05:00
Timothy Jaeryang Baek
7292cee868 refac 2026-06-29 00:42:39 -05:00
Timothy Jaeryang Baek
bc70696f4f refac 2026-06-29 00:40:28 -05:00
Timothy Jaeryang Baek
dbdcfd8c60 refac 2026-06-29 00:35:54 -05:00
Timothy Jaeryang Baek
7e13fd7ad1 refac 2026-06-29 00:26:35 -05:00
Timothy Jaeryang Baek
124c7a3283 refac 2026-06-29 00:21:37 -05:00
Timothy Jaeryang Baek
cfb49c4c18 refac 2026-06-29 00:19:47 -05:00
Timothy Jaeryang Baek
2560533c1a refac 2026-06-29 00:18:40 -05:00
Timothy Jaeryang Baek
5b1c42e81a refac 2026-06-29 00:05:10 -05:00
Yogi
6f5f263244
fix: ignore IME composition Enter in chat search (#26172) (#26238)
Pressing Enter to confirm an IME composition (e.g. Japanese, Chinese, or Korean)
in the chat search modal and the sidebar search input was treated as a normal
Enter and triggered a search action such as starting a new chat, instead of just
confirming the composed text.

Guard the search keydown handlers so a keydown fired while composing is ignored:
- SearchInput.svelte on:keydown
- SearchModal.svelte document-level onKeyDown
using e.isComposing (with a keyCode === 229 fallback), mirroring the IME guard
already used by MessageInput. A second Enter, after the composition is confirmed,
still triggers the action as before.

Fixes #26172

Co-authored-by: Yogi <251807336+ybnjm1234-source@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 23:47:02 -05:00
Timothy Jaeryang Baek
5922727402 refac 2026-06-28 23:42:04 -05:00
Timothy Jaeryang Baek
03a8363583 refac 2026-06-28 23:28:40 -05:00
Timothy Jaeryang Baek
97901220f2 refac 2026-06-28 23:28:03 -05:00
Timothy Jaeryang Baek
8977a10a2b refac 2026-06-28 23:24:24 -05:00
joaoback
cd1ec31957
i18n: add pt-BR translations for newly added UI items and consistency pass (#26318)
New **pt-BR** translations for items introduced in the latest releases, plus a consistency/quality pass across existing strings (grammar, tone, capitalization, pluralization). Placeholders and hotkeys preserved. No logic changes.
2026-06-28 23:23:43 -05:00
Timothy Jaeryang Baek
ef8c9c063c refac 2026-06-28 23:22:10 -05:00
Timothy Jaeryang Baek
dd4f43bfdb refac 2026-06-28 23:21:05 -05:00
Timothy Jaeryang Baek
3a232f5e9a refac 2026-06-28 23:20:54 -05:00
Timothy Jaeryang Baek
23d03d6aae refac 2026-06-28 23:10:14 -05:00
Timothy Jaeryang Baek
d99ac7d3f8 refac 2026-06-28 23:02:38 -05:00
Timothy Jaeryang Baek
c7be66626f refac 2026-06-28 22:53:47 -05:00
Timothy Jaeryang Baek
464e703e47 refac 2026-06-28 22:50:31 -05:00
Timothy Jaeryang Baek
518702caae refac 2026-06-28 22:35:59 -05:00
Timothy Jaeryang Baek
62ae206918 refac 2026-06-28 22:35:44 -05:00
Timothy Jaeryang Baek
516051304e refac 2026-06-28 22:33:59 -05:00
Timothy Jaeryang Baek
0130b49514 refac 2026-06-28 22:32:10 -05:00
Algorithm5838
5aeb1ca708
fix: persist control edits across navigation (#26336) 2026-06-28 22:16:46 -05:00
Timothy Jaeryang Baek
df634bb64f refac 2026-06-28 22:11:01 -05:00
Algorithm5838
6729e64f30
perf: skip re-splitting unchanged text tokens (#26326) 2026-06-28 22:09:58 -05:00
Timothy Jaeryang Baek
ea3f5f22d2 refac 2026-06-28 22:09:15 -05:00
Aindriú Mac Giolla Eoin
0b0910bee2
i18n: update Irish translation (#26360) 2026-06-28 22:02:11 -05:00
Timothy Jaeryang Baek
7c3802a55e refac 2026-06-28 21:59:19 -05:00
G30
08f64f7908
fix(calendar): prevent all-day event date drift on save (#25864)
Some checks failed
Python CI / Ruff Format (3.11) (push) Has been cancelled
Python CI / Ruff Format (3.12) (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_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/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
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
Frontend Build / Format & Build (push) Has been cancelled
Frontend Build / Unit Tests (push) Has been cancelled
Create and publish Docker images with specific build args / copy-to-dockerhub (-cuda126, cuda126) (push) Has been cancelled
Create and publish Docker images with specific build args / copy-to-dockerhub (-ollama, ollama) (push) Has been cancelled
Create and publish Docker images with specific build args / merge (map[name:main suffix:]) (push) Has been cancelled
Create and publish Docker images with specific build args / copy-to-dockerhub (-cuda, cuda) (push) Has been cancelled
Create and publish Docker images with specific build args / merge (map[name:cuda suffix:-cuda]) (push) Has been cancelled
Create and publish Docker images with specific build args / merge (map[name:cuda126 suffix:-cuda126]) (push) Has been cancelled
Create and publish Docker images with specific build args / merge (map[name:ollama suffix:-ollama]) (push) Has been cancelled
Create and publish Docker images with specific build args / merge (map[name:slim suffix:-slim]) (push) Has been cancelled
Create and publish Docker images with specific build args / notify-helm-charts (push) Has been cancelled
Create and publish Docker images with specific build args / copy-to-dockerhub (, main) (push) Has been cancelled
Create and publish Docker images with specific build args / copy-to-dockerhub (-slim, slim) (push) Has been cancelled
2026-06-25 18:40:25 -04:00
Timothy Jaeryang Baek
e3ba698453 refac 2026-06-25 17:34:41 -04:00
Timothy Jaeryang Baek
741b64edb6 refac 2026-06-25 17:23:53 -04:00
Timothy Jaeryang Baek
7c0b0e42f5 refac 2026-06-25 17:14:22 -04:00
Timothy Jaeryang Baek
8f890f0b43 refac
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/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/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 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 / 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 (-ollama, ollama) (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 / Format & Build (push) Waiting to run
Frontend Build / Unit Tests (push) Waiting to run
2026-06-25 15:56:10 +01:00
Timothy Jaeryang Baek
ede39d82de refac 2026-06-25 15:49:36 +01:00
Timothy Jaeryang Baek
1a8e1a9939 refac 2026-06-25 15:22:31 +01:00
Timothy Jaeryang Baek
7b55a63fc7 refac 2026-06-25 15:22:20 +01:00
Timothy Jaeryang Baek
7d9b249671 refac 2026-06-25 14:40:35 +01:00
Timothy Jaeryang Baek
e124c2656a refac 2026-06-25 14:37:05 +01:00
Timothy Jaeryang Baek
5576e6ed8a refac 2026-06-25 14:34:22 +01:00
Timothy Jaeryang Baek
7453968678 refac 2026-06-25 14:19:30 +01:00
Timothy Jaeryang Baek
47a1bfdd15 refac 2026-06-25 14:19:21 +01:00
Timothy Jaeryang Baek
b5c43968db refac 2026-06-25 03:31:45 +01:00
Timothy Jaeryang Baek
35f8bf97e3 refac 2026-06-25 03:26:31 +01:00
Timothy Jaeryang Baek
1457f2dec8 refac 2026-06-25 03:26:17 +01:00
Timothy Jaeryang Baek
1111a3a222 refac 2026-06-25 03:26:07 +01:00
Timothy Jaeryang Baek
f812072215 refac 2026-06-25 03:24:50 +01:00
Timothy Jaeryang Baek
8934bfb04b refac
Some checks are pending
Python CI / Ruff Format (3.12) (push) Waiting to run
Python CI / Ruff Format (3.11) (push) Waiting to run
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 / 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 / copy-to-dockerhub (-cuda, cuda) (push) Blocked by required conditions
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 / 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 / copy-to-dockerhub (-cuda126, cuda126) (push) Blocked by required conditions
Create and publish Docker images with specific build args / copy-to-dockerhub (-ollama, ollama) (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 / Format & Build (push) Waiting to run
Frontend Build / Unit Tests (push) Waiting to run
2026-06-24 14:13:58 +02:00
Timothy Jaeryang Baek
fd56086e79 refac 2026-06-24 13:19:55 +02:00
Timothy Jaeryang Baek
95391221df refac 2026-06-23 23:35:44 +02:00
Timothy Jaeryang Baek
19db873603 refac 2026-06-23 23:35:40 +02:00
Timothy Jaeryang Baek
7f08376f0c refac 2026-06-23 23:18:05 +02:00
Timothy Jaeryang Baek
15c7e37438 refac 2026-06-23 23:13:32 +02:00
Timothy Jaeryang Baek
b1c2536ed2 refac 2026-06-23 23:13:28 +02:00
Timothy Jaeryang Baek
91762ed807 refac
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: 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 / copy-to-dockerhub (, main) (push) Blocked by required conditions
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 / 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 / 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 (-ollama, ollama) (push) Blocked by required conditions
Create and publish Docker images with specific build args / copy-to-dockerhub (-slim, slim) (push) Blocked by required conditions
2026-06-23 00:25:21 +02:00
Timothy Jaeryang Baek
a0c2ec3d2c refac
Some checks failed
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: 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 / merge (map[name:main suffix:]) (push) Blocked by required conditions
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 / copy-to-dockerhub (-cuda126, cuda126) (push) Blocked by required conditions
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 / 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: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 / 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 (-ollama, ollama) (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 / Format & Build (push) Has been cancelled
Frontend Build / Unit Tests (push) Has been cancelled
2026-06-22 17:02:14 +02:00
Timothy Jaeryang Baek
7e8153e889 refac 2026-06-22 16:47:48 +02:00
Timothy Jaeryang Baek
223f484ded refac 2026-06-22 16:10:19 +02:00
Timothy Jaeryang Baek
88901bfa04 refac 2026-06-22 14:42:23 +02:00
Timothy Jaeryang Baek
928eb015bd refac
Some checks failed
Python CI / Ruff Format (3.11) (push) Has been cancelled
Python CI / Ruff Format (3.12) (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_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/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
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
Frontend Build / Format & Build (push) Has been cancelled
Frontend Build / Unit Tests (push) Has been cancelled
Create and publish Docker images with specific build args / merge (map[name:main suffix:]) (push) Has been cancelled
Create and publish Docker images with specific build args / copy-to-dockerhub (-cuda126, cuda126) (push) Has been cancelled
Create and publish Docker images with specific build args / merge (map[name:cuda suffix:-cuda]) (push) Has been cancelled
Create and publish Docker images with specific build args / merge (map[name:cuda126 suffix:-cuda126]) (push) Has been cancelled
Create and publish Docker images with specific build args / merge (map[name:ollama suffix:-ollama]) (push) Has been cancelled
Create and publish Docker images with specific build args / merge (map[name:slim suffix:-slim]) (push) Has been cancelled
Create and publish Docker images with specific build args / copy-to-dockerhub (, main) (push) Has been cancelled
Create and publish Docker images with specific build args / copy-to-dockerhub (-cuda, cuda) (push) Has been cancelled
Create and publish Docker images with specific build args / copy-to-dockerhub (-ollama, ollama) (push) Has been cancelled
Create and publish Docker images with specific build args / copy-to-dockerhub (-slim, slim) (push) Has been cancelled
2026-06-19 16:28:25 +02:00
Timothy Jaeryang Baek
a54878b14f refac 2026-06-19 15:34:43 +02:00
Timothy Jaeryang Baek
8b9e28b503 refac 2026-06-19 15:28:52 +02:00
Timothy Jaeryang Baek
3f0c0e0a0d refac 2026-06-19 00:16:06 +02:00
Timothy Jaeryang Baek
8958b64b5a refac
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: 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 / copy-to-dockerhub (, main) (push) Blocked by required conditions
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 / 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 / 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 (-ollama, ollama) (push) Blocked by required conditions
Create and publish Docker images with specific build args / copy-to-dockerhub (-slim, slim) (push) Blocked by required conditions
2026-06-18 11:02:14 +02:00
Timothy Jaeryang Baek
21f9e5295b refac 2026-06-18 10:47:25 +02:00
_00_
0ffc04797f
i18n: Update es-ES Spanish translations v0.9.6 (#25836)
Some checks failed
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/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/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 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 / 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 / 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 (-ollama, ollama) (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 / Format & Build (push) Has been cancelled
Frontend Build / Unit Tests (push) Has been cancelled
i18n: Update Spanish translations v0.9.6

Update of new strings
2026-06-17 03:09:26 +02:00
G30
b2809e6293
perf(ui): hoist marked.use() to module scope to prevent extension stacking (#25837) 2026-06-17 03:09:09 +02:00
G30
beb9bf60e4
feat(memory): default user memory toggle to admin's global ENABLE_MEMORIES config (#25909) 2026-06-17 03:08:43 +02:00
G30
2f9b28a57d
fix(ui): run initChatList API calls concurrently in Promise.all (#25838) 2026-06-17 03:08:02 +02:00
Classic298
d501e3d6b5
Update milvus_multitenancy.py (#25857) 2026-06-17 03:07:27 +02:00
G30
819ad1d904
fix(chat): convert recursive findClosingDelimiter to iterative loop to prevent stack overflow (#25845) 2026-06-17 03:07:12 +02:00
G30
9fe3a00dba
perf(chat): cache KaTeX rendered HTML in reactive statement to avoid redundant renders (#25847) 2026-06-17 03:06:56 +02:00
G30
4584adf900
feat(ui): add 'Unshare All Shared Chats' button to Shared Chats modal (#25848) 2026-06-17 03:06:41 +02:00
G30
dfdb76cc46
fix(ui): prevent Code Editor drawer from collapsing when content is empty (#25855) 2026-06-17 03:06:15 +02:00
Classic298
17df026492
Confer object-derived file write only for files the object owner owns (#26032)
has_access_to_file() derives file access from the objects a file is attached to
(knowledge bases, workspace models). Those branches returned True for any access_type
whenever the user held that permission on the object, write/delete included. Since a
user can create their own KB or model and attach any file they can merely READ (KB
attach and the model meta.knowledge validator both gate on read access only), a user
with read access to a victim file could launder it into write/delete: attach it to an
object they own, then rename, overwrite or delete it via the write-gated file routes
(POST /files/{id}/rename, /data/content/update, DELETE /files/{id}). This is the
residual of GHSA-vjqm-6gcc-62cr (CVE-2026-54012) left open by the read-only attach
validator (CWE-863).

An object now confers write/delete on a file only when the object's owner owns that
file, so delegation originates from the file's own owner. Read is unchanged (RAG and
shared-object reads still work), and legitimate delegation is preserved: a write grant
on an object whose owner owns the attached file still confers write. Applied to all
three object branches: knowledge base, file home collection, and workspace model.

Co-authored-by: rexpository <30176934+rexpository@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 03:05:57 +02:00
Classic298
e038bab66d
Authorize POST /api/v1/images/edit (enforce ENABLE_IMAGE_EDIT + image-gen permission) (#26009)
The direct image-edit route was the only image-edit surface with no authorization: it ran
on get_verified_user alone, while POST /generations enforces ENABLE_IMAGE_GENERATION +
features.image_generation and the built-in edit_image tool enforces ENABLE_IMAGE_EDIT +
features.image_generation. A verified non-admin user could therefore reach the configured
image-edit provider (spending IMAGES_EDIT_OPENAI_API_KEY) even when the administrator had
globally disabled image editing (ENABLE_IMAGE_EDIT=False) or denied the user image
generation.

Split the route from the shared impl (mirroring generate_images/image_generations): the new
/edit wrapper enforces ENABLE_IMAGE_EDIT and the per-user image-generation permission, then
delegates to image_edits(). The internal callers (the edit_image tool and the chat
middleware) already gate themselves and call image_edits() directly, so they are unaffected.

Co-authored-by: jagstack <52110932+jagstack@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 03:05:41 +02:00
Classic298
c39be0e2d6
Update milvus.py (#25858) 2026-06-17 03:05:27 +02:00
Taylor Wilsdon
6cd0ba0b6b
implement date picker and additional columns for analytics (#25922)
Co-authored-by: Tim Baek <tim@openwebui.com>
2026-06-17 03:03:18 +02:00
Timothy Jaeryang Baek
e473ab1231 refac 2026-06-17 03:01:11 +02:00
Timothy Jaeryang Baek
4dbb2f94a6 refac 2026-06-17 02:58:11 +02:00
G30
dee07d8a30
feat(ui): show total archived chat count in ChatsModal title (#25872) 2026-06-17 02:57:27 +02:00
Algorithm5838
e031fadc35
perf: skip highlight.js auto-detection in read-only code blocks (#25824) 2026-06-17 02:57:02 +02:00
G30
b4aef82401
feat(ui): show total file count in FilesModal title (#25873) 2026-06-17 02:55:13 +02:00
Timothy Jaeryang Baek
5cdcdbaeec refac 2026-06-17 02:52:35 +02:00
Timothy Jaeryang Baek
e8d55c0a8b refac 2026-06-17 02:36:20 +02:00
G30
57a5e43696
feat(permissions): add per-group features.user_webhooks permission (#25923) 2026-06-17 00:47:44 +02:00
Timothy Jaeryang Baek
7b29834d42 refac 2026-06-17 00:43:59 +02:00
G30
2b9b956dad
fix(notes): sync pinned note state in sidebar and editor menu (#25918) 2026-06-17 00:42:20 +02:00
G30
25090dbf17
fix(chat): prevent false-positive unread indicators from title generation, pin/archive/folder actions, and new chat creation (#25912) 2026-06-17 00:41:44 +02:00
G30
bb2663aad1
fix(ui): gate all chat export formats by chat.export permission (#25914) 2026-06-17 00:41:18 +02:00
G30
3571db34e2
fix(ui): enforce allow_users permission in share chat modal (#25915) 2026-06-17 00:40:57 +02:00
Timothy Jaeryang Baek
7d1f941580 refac 2026-06-17 00:40:04 +02:00
Timothy Jaeryang Baek
ed4cb358a0 refac 2026-06-17 00:39:11 +02:00
Timothy Jaeryang Baek
caedcbae49 refac 2026-06-17 00:36:34 +02:00
Timothy Jaeryang Baek
9ccda6715c refac 2026-06-17 00:35:08 +02:00
Timothy Jaeryang Baek
edf3ae9209 refac 2026-06-17 00:33:30 +02:00
G30
0726db7217
fix(analytics): remove duplicate onMount causing double API requests on dashboard load (#25935) 2026-06-17 00:29:07 +02:00
G30
9c6c375dfe
fix(images): add null guard for config in onMount to prevent crash on API failure (#25933) 2026-06-17 00:28:42 +02:00
G30
3e3c5b6d78
fix(permissions): add admin bypass to memories permission checks (#25924) 2026-06-17 00:28:17 +02:00
G30
2bc91e8f52
fix(ui): add missing skills permission to workspace menu visibility check (#25925) 2026-06-17 00:27:25 +02:00
G30
e5ed45fb20
fix(ui): handle null chat in folder drop handler for cross-account drag (#25928) 2026-06-17 00:26:35 +02:00
Timothy Jaeryang Baek
232421f40b refac 2026-06-17 00:25:35 +02:00
G30
6b2d962cd6
feat(permissions): add reset to defaults button in permissions modals (#25931)
Adds a 'Reset to Defaults' button to both the Edit Default Permissions
modal and the Edit User Group permissions modal.

- Default permissions modal: resets to stock/env-var configuration
- User group modal: resets to current global default permissions

Backend: new GET /api/v1/users/default/permissions/defaults endpoint
that returns DEFAULT_USER_PERMISSIONS (env-var-based initial defaults).
2026-06-17 00:25:20 +02:00
Timothy Jaeryang Baek
7ee75a0c04 refac 2026-06-17 00:17:48 +02:00
Timothy Jaeryang Baek
fc9c2ea191 refac 2026-06-17 00:17:14 +02:00
G30
6f2e97aa58
fix(evaluations): prevent duplicate leaderboard API requests on navigation (#25934) 2026-06-17 00:16:27 +02:00
Classic298
5f3a628a8d
Encode terminal ws session_id to block upstream user_id query injection (#26042)
ws_terminal() interpolated the path parameter session_id directly into the upstream
terminal WebSocket URL and then appended ?user_id=<caller>, with no encoding or
validation (the HTTP sibling proxy_terminal runs _sanitize_proxy_path; this path ran
nothing). An encoded '?'/'&' smuggled through session_id survives Open WebUI's single
decode and is re-decoded by the upstream, injecting an attacker-chosen user_id ahead
of the appended one. Query parsing binds the first occurrence, so the orchestrator
resolves the spoofed user's terminal scope, letting a normal authenticated user
present another user's identity to the upstream (CWE-116/863).

Encode session_id as an opaque path segment with urllib.parse.quote(session_id,
safe=''). This neutralises '?'/'#'/'&' at any decode depth (the upstream's single
decode reverses only the quote, leaving the original delimiters inert as path
content), while legitimate UUID session ids pass through unchanged. The appended
user_id is then the only query parameter the upstream binds.

The separate concern that the forwarded identity is a bearer claim with no integrity
binding spans Open WebUI and the upstream terminal server and is not addressed here.

Co-authored-by: rexpository <30176934+rexpository@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 00:15:39 +02:00
Timothy Jaeryang Baek
8b35ce924b refac 2026-06-17 00:15:04 +02:00
G30
966b3fdb57
fix(ui): deduplicate layout-level API requests (#25942) 2026-06-17 00:09:50 +02:00
G30
ee3a49a88d
fix(ui): deduplicate store fetches with null-guards and mutex (#25943) 2026-06-17 00:07:11 +02:00
Classic298
22f2fe1ffb
Require auth on ydoc:awareness:update and ydoc:document:leave handlers (CWE-306) (#25946)
These were the only two ydoc Socket.IO handlers missing the SESSION_POOL guard that
every sibling (join/state/update) enforces. With always_connect=True admitting
unauthenticated sockets, a client that knew a note's document_id could broadcast
spoofed awareness (cursors/presence) and fake ydoc:user:left events into a live
editing room, attributed to any client-supplied user_id.

Both handlers now require an authenticated SESSION_POOL session; the awareness handler
additionally requires prior ydoc:document:join (room membership); and the broadcast
user_id is fixed to the authenticated identity instead of the client-supplied value,
removing the impersonation. Document content was never reachable (ydoc:document:update
already enforces write permission).

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 00:06:13 +02:00
Timothy Jaeryang Baek
993e749121 refac 2026-06-17 00:05:45 +02:00
Classic298
4c06b392da
Don't let SCIM's active flag demote an admin (defense-in-depth) (#25948)
The SCIM update_user (PUT) and patch_user (PATCH) handlers mapped the SCIM active field
unconditionally onto the role column (role = 'user' if active else 'pending'), so a routine
IdP sync or a misconfigured IdP that marked a locally-provisioned admin inactive would
silently strip that admin's role and could lock an instance out of its own administration.

Gate both active->role assignments on user.role != 'admin' so SCIM provisioning can
activate/deactivate ordinary users but never demotes an existing admin; admin role changes
continue to go through the dedicated admin endpoints. SCIM already cannot promote to admin
(active only maps to user/pending), so this is symmetric.

Credit to @HOHK0923 for surfacing the admin-demotion footgun.

Co-authored-by: HOHK0923 <118590749+HOHK0923@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 23:59:55 +02:00
Timothy Jaeryang Baek
fbcdcf146b refac 2026-06-16 23:59:24 +02:00
Jamie Lin
ec86ce5cf7
fix(oauth): use Protected Resource Metadata scopes in MCP DCR flow (#25958)
* fix(oauth): use Protected Resource Metadata scopes in MCP DCR flow

The Dynamic Client Registration flow seeded the registration request
`scope` from the Authorization Server's `scopes_supported` (RFC 8414),
which is a full catalog of every scope the AS can grant across all
resources. Per RFC 9728 and the MCP Scope Selection Strategy, the
resource-specific Protected Resource Metadata `scopes_supported` is the
correct, least-privilege source.

The PRM is already fetched in this function; this change prefers its
`scopes_supported` and keeps the AS `scopes_supported` only as a
fallback when the PRM advertises none. Mirrors the static-credentials
fix in #24690.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore(oauth): trim DCR scope comment per review feedback

Shortens the inline comment per review feedback on #25958.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 23:53:38 +02:00
Classic298
087878ce84
Match WEB_FETCH_FILTER_LIST on hostnames with label boundaries, not URL suffix (CWE-693) (#25949)
is_string_allowed does endswith() matching and was called with the full URL
(retrieval/web/utils.py) against WEB_FETCH_FILTER_LIST, so a blocklisted host with any
path (https://blocked.example/x) ended with /x, not the host, and slipped through; the
allowlist direction false-rejected legitimate URLs and admitted attacker URLs ending in
an allowed string. The same endswith caused label confusion at the hostname call site
(retrieval/web/main.py): corp.com matched evilcorp.com, 10.0.0.1 matched 110.0.0.1.

Add is_host_allowed(host, ...) matching on DNS label boundaries (host == pattern or
host.endswith('.' + pattern)), called with the parsed hostname at both web-fetch call
sites. is_string_allowed is left unchanged for the unrelated function-name filters
(utils/middleware.py, utils/tools.py).

The separate is_global guard (validate_url / _ssrf_safe_new_conn, active when
ENABLE_RAG_LOCAL_WEB_FETCH is off) already blocks RFC1918/loopback/link-local, so this
restores the admin's intended blocking of specific public hosts.

Co-authored-by: addcontent <59762500+addcontent@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 23:53:08 +02:00
Timothy Jaeryang Baek
4f69c33de0 refac 2026-06-16 23:52:33 +02:00
Timothy Jaeryang Baek
c205d3a353 refac 2026-06-16 23:48:01 +02:00
Timothy Jaeryang Baek
4210cae68e refac 2026-06-16 23:41:17 +02:00
Classic298
de8ea08f5c
Route user-supplied image-URL fetches through an SSRF-safe session (DNS rebinding, CWE-918) (#25960)
The connection-layer DNS-rebinding guard (_SSRFSafeResolver / _SSRFSafeAdapter, PR #24759) was
mounted only on SafeWebBaseLoader. Two user-reachable image fetches validate the URL then fetch
it through the shared get_session() pool with the default resolver, so a TTL-0 rebinding answer
that passed validate_url reaches an internal address at connect:

- get_image_base64_from_url (utils/files.py): user image_url on every chat completion.
- load_url_image (routers/images.py, POST /api/v1/images/edit): user-supplied image field.

Add get_ssrf_safe_session() (a one-off aiohttp session mounting _SSRFSafeResolver) and use it
for both fetches, so the connect-time IP is re-validated and a rebound loopback / RFC1918 /
metadata address is rejected. The shared pool is left untouched for the admin-configured
image-generation callers, which legitimately reach internal hosts.

Co-authored-by: dhyabi2 <32069256+dhyabi2@users.noreply.github.com>
Co-authored-by: geo-chen <2404584+geo-chen@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 23:36:53 +02:00
Algorithm5838
d07e4154fe
fix: prevent model edit loss from access auto-save (#26004) 2026-06-16 23:36:36 +02:00
Timothy Jaeryang Baek
bb1419328b refac 2026-06-16 23:30:24 +02:00
Timothy Jaeryang Baek
40c09167cd refac 2026-06-16 23:24:27 +02:00
Timothy Jaeryang Baek
56ae99e96a refac 2026-06-16 23:02:20 +02:00
G30
1eecbc1ac2
fix(ui): collapse Code Execution settings when toggle is disabled (#25970) 2026-06-16 23:01:22 +02:00
Timothy Jaeryang Baek
78a5015846 refac 2026-06-16 23:00:31 +02:00
G30
b93d560788
feat(ui): pre-select last model when adding a new model slot (#25974) 2026-06-16 22:56:56 +02:00
Classic298
b7626f05fb
Authorize KB write access before auto-linking an uploaded file (CWE-862/863) (#26001)
process_uploaded_file auto-links an uploaded file to the knowledge base named in
client-supplied metadata.knowledge_id, but it called Knowledges.add_file_to_knowledge_by_id
directly with no authorization, while the dedicated POST /knowledge/{id}/file/add endpoint
gates the same operation on owner / admin / write grant. A read-only collaborator (or any
verified user who knows the KB id) could therefore attach arbitrary files to a knowledge base
they cannot write: the KnowledgeFile membership row was committed, and the later vector-index
write check failed closed but left the row in place.

Gate the auto-link on the same owner/admin/write check before inserting the membership row;
otherwise skip the link and log. The file still uploads as the user's own file, it just is
not linked to a KB they cannot write.

Co-authored-by: jagstack <52110932+jagstack@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 22:56:24 +02:00
Martin Gronek
1fc0e3ade7
fix: correct German date format token for dayjs in de-DE translation (#25985)
TT and JJJJ are German placeholders but not valid dayjs format tokens.
Replaced with DD and YYYY which dayjs understands correctly.
2026-06-16 22:55:23 +02:00
G30
84e4110538
fix(auth): enforce chat.tts permission on OpenAI /audio/speech proxy endpoint (#25993) 2026-06-16 22:54:35 +02:00
Timothy Jaeryang Baek
19a176fd36 refac 2026-06-16 22:53:57 +02:00
G30
bb66d435b7
fix(auth): enforce features.api_keys permission on GET and DELETE /api_key endpoints (#25992) 2026-06-16 22:48:44 +02:00
Classic298
dc4924b66e
Enforce per-model access on arena fallback before bypass_filter dispatch (#26046)
generate_chat_completion() checks model access on line 200 only when bypass_filter is
False. When an arena model reaches this function without a pre-resolved selected_model_id,
which is the task and background path (the /api/v1/tasks/* endpoints call
generate_chat_completion directly rather than through process_chat_payload), the fallback
resolves the arena to an underlying model and recurses with bypass_filter=True, so the
resolved model's access check is skipped. An authenticated user with access to an arena
could therefore reach a model they are denied directly, and for the default or exclude
arena, whose candidate pool is every non-arena model, any model on the instance (CWE-862).
The normal chat path resolves the arena in process_chat_payload before this function, so its
resolved model is checked on line 200; the task path was not, which is the inconsistency.

Enforce check_model_access() on the resolved model in the fallback, before the
bypass_filter=True recursion, mirroring the normal-path check. Admins and already-bypassed
recursive calls are unaffected, and legitimate arena use of accessible models is unchanged.

Co-authored-by: rexpository <30176934+rexpository@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 22:45:42 +02:00
Classic298
920b655f46
Gate scheduled automations and fail closed on per-model access for non-user roles (#26047)
Two lifecycle/authorization gaps let a deactivated (pending) account keep acting:

1. The background automation scheduler (execute_automation) rehydrated the owner by ID and
   dispatched the chat pipeline without re-checking the owner. A user later set to pending,
   or one whose features.automations permission was revoked, kept running scheduled
   automations on the operator's provider credentials, even though the HTTP create/update/run
   routes already gate on get_verified_user + features.automations. Re-gate the rehydrated
   owner before dispatch: require role user/admin and, for non-admins, the features.automations
   permission; otherwise record an error and skip the run.

2. check_model_access enforced per-model ACLs only for exactly role == 'user', so any other
   non-admin role (a pending principal) fell through and was granted access. Enforce for every
   non-admin role (admins still bypass), so the check fails closed (CWE-862, CWE-863).

Co-authored-by: rexpository <30176934+rexpository@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 22:45:27 +02:00
Classic298
05098d25a5
Fail closed when the proxy/redirect path decode cap is exceeded (#26050)
The decode-until-stable loops in _sanitize_proxy_path (terminals.py, cap 8) and
_safe_static_redirect_path (models.py, cap 2) proceed with whatever remains after the
cap instead of rejecting it. A path encoded more times than the cap therefore exits the
loop still percent-encoded, passes the literal '..' / prefix checks (the dots are still
%2E, not '..'), and is forwarded to the upstream terminal server, or emitted as a
redirect Location, which then decodes it once more and resolves the traversal. This is
the residual of the decode-until-stable hardening added for CVE-2026-54017: the cap is a
fixed depth, not a true stability guarantee.

Reject when the value is still not stable after the cap (unquote(x) != x), so anything
encoded more deeply than the cap fails closed rather than being forwarded. Legitimate
paths stabilize within a pass or two and are unaffected.

Co-authored-by: DavidCarliez <271374756+DavidCarliez@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 22:44:11 +02:00
Timothy Jaeryang Baek
3266a8c9eb refac 2026-06-16 22:43:40 +02:00
Andrew Baek
ecdb6f353a
Merge pull request #26060 from andrewbbaek/dev
feat: add auth-login-card ID for custom CSS targeting
2026-06-16 22:35:25 +02:00
Timothy Jaeryang Baek
084d040e22 refac
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: 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 / merge (map[name:main suffix:]) (push) Blocked by required conditions
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 / copy-to-dockerhub (-cuda126, cuda126) (push) Blocked by required conditions
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 / 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: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 / 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 (-ollama, ollama) (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 / Format & Build (push) Waiting to run
Frontend Build / Unit Tests (push) Waiting to run
2026-06-16 00:05:14 +02:00
Timothy Jaeryang Baek
76854d1424 refac 2026-06-15 23:53:52 +02:00
Timothy Jaeryang Baek
45fcf272ef refac 2026-06-15 23:42:29 +02:00
Timothy Jaeryang Baek
c783fd30f2 refac 2026-06-15 23:37:38 +02:00
Timothy Jaeryang Baek
d65ac445a4 refac 2026-06-15 23:34:24 +02:00
Timothy Jaeryang Baek
38920c0ed1 refac 2026-06-15 23:32:06 +02:00
Timothy Jaeryang Baek
5019af79a0 refac 2026-06-15 23:31:59 +02:00
Timothy Jaeryang Baek
f85cb27ef8 refac
Some checks failed
Python CI / Ruff Format (3.11) (push) Has been cancelled
Python CI / Ruff Format (3.12) (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_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/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
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
Frontend Build / Format & Build (push) Has been cancelled
Frontend Build / Unit Tests (push) Has been cancelled
Create and publish Docker images with specific build args / merge (map[name:main suffix:]) (push) Has been cancelled
Create and publish Docker images with specific build args / copy-to-dockerhub (-cuda126, cuda126) (push) Has been cancelled
Create and publish Docker images with specific build args / merge (map[name:cuda suffix:-cuda]) (push) Has been cancelled
Create and publish Docker images with specific build args / merge (map[name:cuda126 suffix:-cuda126]) (push) Has been cancelled
Create and publish Docker images with specific build args / merge (map[name:ollama suffix:-ollama]) (push) Has been cancelled
Create and publish Docker images with specific build args / merge (map[name:slim suffix:-slim]) (push) Has been cancelled
Create and publish Docker images with specific build args / copy-to-dockerhub (, main) (push) Has been cancelled
Create and publish Docker images with specific build args / copy-to-dockerhub (-cuda, cuda) (push) Has been cancelled
Create and publish Docker images with specific build args / copy-to-dockerhub (-ollama, ollama) (push) Has been cancelled
Create and publish Docker images with specific build args / copy-to-dockerhub (-slim, slim) (push) Has been cancelled
2026-06-13 02:13:51 +01:00
Timothy Jaeryang Baek
4602abe5c6 refac 2026-06-13 02:13:39 +01:00
Timothy Jaeryang Baek
b1d40f3409 refac
Some checks failed
Python CI / Ruff Format (3.11) (push) Has been cancelled
Python CI / Ruff Format (3.12) (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_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/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
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
Frontend Build / Format & Build (push) Has been cancelled
Frontend Build / Unit Tests (push) Has been cancelled
Create and publish Docker images with specific build args / merge (map[name:cuda suffix:-cuda]) (push) Has been cancelled
Create and publish Docker images with specific build args / merge (map[name:cuda126 suffix:-cuda126]) (push) Has been cancelled
Create and publish Docker images with specific build args / merge (map[name:main suffix:]) (push) Has been cancelled
Create and publish Docker images with specific build args / merge (map[name:ollama suffix:-ollama]) (push) Has been cancelled
Create and publish Docker images with specific build args / merge (map[name:slim suffix:-slim]) (push) Has been cancelled
Create and publish Docker images with specific build args / copy-to-dockerhub (, main) (push) Has been cancelled
Create and publish Docker images with specific build args / copy-to-dockerhub (-cuda, cuda) (push) Has been cancelled
Create and publish Docker images with specific build args / copy-to-dockerhub (-cuda126, cuda126) (push) Has been cancelled
Create and publish Docker images with specific build args / copy-to-dockerhub (-ollama, ollama) (push) Has been cancelled
Create and publish Docker images with specific build args / copy-to-dockerhub (-slim, slim) (push) Has been cancelled
2026-06-05 16:58:16 -04:00
Tim Baek
02dc3e689c
Merge pull request #25590 from open-webui/dev
Some checks failed
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
Python CI / Ruff Format (3.11) (push) Has been cancelled
Python CI / Ruff Format (3.12) (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_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/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 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
Frontend Build / Unit Tests (push) Has been cancelled
Frontend Build / Format & Build (push) Has been cancelled
Release to PyPI / release (push) Has been cancelled
Release / publish (push) Has been cancelled
Create and publish Docker images with specific build args / copy-to-dockerhub (, main) (push) Has been cancelled
Create and publish Docker images with specific build args / merge (map[name:cuda suffix:-cuda]) (push) Has been cancelled
Create and publish Docker images with specific build args / merge (map[name:cuda126 suffix:-cuda126]) (push) Has been cancelled
Create and publish Docker images with specific build args / merge (map[name:main suffix:]) (push) Has been cancelled
Create and publish Docker images with specific build args / merge (map[name:ollama suffix:-ollama]) (push) Has been cancelled
Create and publish Docker images with specific build args / merge (map[name:slim suffix:-slim]) (push) Has been cancelled
Create and publish Docker images with specific build args / copy-to-dockerhub (-cuda, cuda) (push) Has been cancelled
Create and publish Docker images with specific build args / copy-to-dockerhub (-cuda126, cuda126) (push) Has been cancelled
Create and publish Docker images with specific build args / copy-to-dockerhub (-ollama, ollama) (push) Has been cancelled
Create and publish Docker images with specific build args / copy-to-dockerhub (-slim, slim) (push) Has been cancelled
refac
2026-06-01 19:10:46 -07:00
Timothy Jaeryang Baek
1a97751e37 refac
Some checks failed
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
Python CI / Ruff Format (3.11) (push) Has been cancelled
Python CI / Ruff Format (3.12) (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_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/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 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
Frontend Build / Format & Build (push) Has been cancelled
Frontend Build / Unit Tests (push) Has been cancelled
Create and publish Docker images with specific build args / merge (map[name:cuda suffix:-cuda]) (push) Has been cancelled
Create and publish Docker images with specific build args / merge (map[name:cuda126 suffix:-cuda126]) (push) Has been cancelled
Create and publish Docker images with specific build args / merge (map[name:main suffix:]) (push) Has been cancelled
Create and publish Docker images with specific build args / merge (map[name:ollama suffix:-ollama]) (push) Has been cancelled
Create and publish Docker images with specific build args / merge (map[name:slim suffix:-slim]) (push) Has been cancelled
Create and publish Docker images with specific build args / copy-to-dockerhub (, main) (push) Has been cancelled
Create and publish Docker images with specific build args / copy-to-dockerhub (-cuda, cuda) (push) Has been cancelled
Create and publish Docker images with specific build args / copy-to-dockerhub (-cuda126, cuda126) (push) Has been cancelled
Create and publish Docker images with specific build args / copy-to-dockerhub (-ollama, ollama) (push) Has been cancelled
Create and publish Docker images with specific build args / copy-to-dockerhub (-slim, slim) (push) Has been cancelled
2026-06-01 19:09:44 -07:00
Tim Baek
b868da6bcd
Merge pull request #25589 from open-webui/dev
refac
2026-06-01 19:05:05 -07:00
Timothy Jaeryang Baek
dc4dab7a07 refac 2026-06-01 19:01:14 -07:00
Tim Baek
90f4b4fcda
Merge pull request #25524 from open-webui/dev
refac
2026-06-01 15:01:27 -07:00
Timothy Jaeryang Baek
4be25c4fdd refac 2026-06-01 15:01:01 -07:00
Tim Baek
b3e9d16b6f
0.9.6 (#25497)
* refac

* refac

* refac: reorganize scripts and ci workflows

* chore: remove unused cypress tests

* chore: remove unused legacy test suite

* refac: deprecate peewee migration layer

The Alembic init migration (7e5b5dc7342b) already creates the
equivalent schema. Peewee migrations are no longer needed for
any version >= 0.3.6.

* refac: remove dead peewee connection wrappers

* refac

* refac

* style: ruff format

* style: standardize os.environ.get to os.getenv

* refac: modernize imports, standardize type hints and docstrings

* refac: modernize type annotations (PEP 604 / PEP 585)

* refac

* feat: kb_exec

* refac

* refac

* refac

* refac

* upd:i18n: es-ES Translation update v0.9.5 (#24651)

es-ES Translation. Update v0.9.5

Added translation of new strings.

* feat: knowledge directory

* refac

* refac

* refac

* refac

* refac

* refac

* refac

* refac

* refac

* refac

* refac

* refac

* fix: German i18n translations (#24668)

* i18n: Update Swedish (sv-SE) translation — merge with latest dev (#24665)

Co-authored-by: Daniel Nylander <daniel@danielnylander.se>

* refac

* Fix translation for 'Authentication' in Danish (#24645)

* refac

* fix: korean i18n

* i18n: Update catalan translation.json (#24569)

* refac

* fix: validate folder_id ownership on chat create + folder-update endpoints (#24588)

POST /api/v1/chats/new and POST /api/v1/chats/{id}/folder accepted a
caller-supplied folder_id with no validation — neither ownership, nor
existence, nor UUID format. The row was persisted with the supplied
value verbatim, so the DB ended up with chat rows whose folder_id
referenced another user's folder, a non-existent UUID, or even a
non-UUID string.

No read path surfaces this across users — every chat-folder read is
user_id-filtered on both sides — so this is referential-integrity
hardening rather than a security boundary fix. But there's no reason
to accept dangling references either, and the downstream consumers
shouldn't have to assume the column is clean.

Add a Folders.get_folder_by_id_and_user_id() lookup at both writers:
if a folder_id is supplied, it must match a folder owned by the
caller. None remains allowed (chat-without-folder is the default).
Non-existent and non-UUID values fall through to 404.

Reported by ShigekiTsuchiyama in GHSA-4vrg-2vcq-q7jc.

Co-authored-by: ShigekiTsuchiyama <ShigekiTsuchiyama@users.noreply.github.com>

* refac

* refac

* refac

* refac

* fix: enforce features.direct_tool_servers on chat-completion tool_servers (#24693)

* fix: enforce features.direct_tool_servers on chat-completion tool_servers

The features.direct_tool_servers per-user permission was correctly
enforced on the storage path (routers/users.py user/settings/update,
which strips toolServers from saved settings when the caller lacks the
permission), but the inference path (/api/chat/completions) popped
tool_servers straight from the request body into metadata with no
permission check. The middleware (utils/middleware.py:2799) then
consumed direct_tool_servers to inject system_prompt into the message
array and register external tool specs that get invoked during the
completion. End result: any authenticated user could bypass the
admin-set per-user feature toggle and use inline tool_servers in their
chat-completion requests, even when admin had explicitly denied the
permission.

Default for USER_PERMISSIONS_FEATURES_DIRECT_TOOL_SERVERS is False
(config.py:2750), so under default config no regular user is supposed
to be able to use direct tool servers — making this a real boundary
bypass on out-of-the-box deployments rather than a corner case.

Mirror the storage-side behaviour at the inference entry point: pop
tool_servers from the request body, then silently drop the value if
the caller is non-admin and lacks features.direct_tool_servers. Admins
always pass; users with the explicit grant always pass; everyone else
gets None propagated into metadata, which the middleware already
handles as the no-tool-servers case.

Reported by berkant-koc in GHSA-f582-c373-jjf6.

Co-authored-by: berkant-koc <berkant-koc@users.noreply.github.com>

* chore: trim verbose comment on tool_servers permission check

---------

Co-authored-by: berkant-koc <berkant-koc@users.noreply.github.com>

* refac

* refac

* refac

* refac

* refac

* refac

* refac

* refac

* refac

* refac

* refac

* refac

* refac

* refac

* refac

* refac

* refac

* refac

* refac

* refac

* fix: legacy peewee tables fk

* refac

Co-Authored-By: Classic298 <27028174+Classic298@users.noreply.github.com>

* fix: default optional env vars used with bash `,,` in start.sh (#24683)

start.sh runs with `set -euo pipefail`, but three call sites added in
070ab2650 (refac: reorganize scripts and ci workflows) reference
optional env vars via bash's `,,` lowercase expansion without any
default. Containers that don't set these vars — the default for every
deployment that isn't explicitly opting into Playwright / bundled
Ollama / CUDA — crash on startup with:

    start.sh: line 15: WEB_LOADER_ENGINE: unbound variable

(and the same for USE_OLLAMA_DOCKER, USE_CUDA_DOCKER once the first
were set in turn.) Reported in open-webui#24560 by urbenlegend.

The same refactor correctly defaulted every other optional env var
with `${VAR:-…}`. The three `,,` references slipped through because
bash can't combine `:-default` with `,,` in a single substitution —
`${VAR:-default,,}` makes the default literal `,,`, not what's wanted.

Fix: normalise the three vars in a one-line preamble with `${VAR:=}`,
which assigns an empty default if unset. The downstream `${VAR,,}`
expressions stay exactly as Tim wrote them, preserving the file's
visual style and matching the existing `${VAR:-…}` idiom for "this
variable is optional".

* i18n: update Russian translations (#24728)

* Update SECURITY.md (#24726)

* fix: tag composite pk in migration (#24722)

* feat(ui): add emoji picker to rich text formatting toolbar (#24704)

* fix: wire workspace.skills into the sidebar + workspace-index gates (#24729)

Reported by bwgabrielsusai on #24719: granting a user only
`workspace.skills` doesn't show the Workspace menu, and visiting
`/workspace` directly bounces them to `/`.

The per-route guard in `/workspace/+layout.svelte` already covered
skills, but two earlier gates in the chain didn't:

* `Sidebar.svelte` case 'workspace' OR'd models/knowledge/prompts/tools
  to decide menu visibility — skills was missing, so the entry never
  rendered for skills-only users.
* `/workspace/+page.svelte` redirect chain picked the first available
  section — skills was missing, so the fallback `goto('/')` fired.

Adding skills to both.

* refac

* refac

* chore: pyodide

* fix: get_image_base64_from_file_id

* refac

* i18n: update Irish translation (#24883)

* refac

* refactor: remove dead frontend API wrappers with no backend route (#24792)

These 19 exported wrappers are dead: each appears exactly once in the
codebase (its own definition), nothing imports or calls any of them, and
none has a corresponding backend route. They are leftovers from settings
that were consolidated server-side into /auths/admin/config,
/openai/config, /ollama/config and /api/config:

- index.ts: getModelFilterConfig, updateModelFilterConfig,
  getCommunitySharingEnabledStatus, toggleCommunitySharingEnabledStatus,
  getModelConfig, updateModelConfig (+ orphaned GlobalModelConfig type)
- auths: getSignUpEnabledStatus, toggleSignUpEnabledStatus,
  getDefaultUserRole, updateDefaultUserRole, getJWTExpiresDuration,
  updateJWTExpiresDuration
- openai: getOpenAIUrls, updateOpenAIUrls, getOpenAIKeys, updateOpenAIKeys
- ollama: updateOllamaUrls
- prompts: restorePromptFromHistory
- folders: updateFolderItemsById (+ orphaned FolderItems type)

Shared types (ModelConfig/ModelMeta/ModelParams) and all live wrappers
are untouched. Removal is import-safe: nothing referenced these.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor: remove unused GET /evaluations/feedbacks/all endpoint (#24778)

GET /evaluations/feedbacks/all returned the entire feedback table in a
single response (flagged as a Medium OOM risk for admins in
open-webui#22206). Its only frontend wrapper, getAllFeedbacks in
src/lib/apis/evaluations/index.ts, is dead: nothing imports or calls it
anywhere in the codebase. The endpoint is a redundant view-only twin of
GET /evaluations/feedbacks/all/export, which is what the admin Feedbacks
UI actually uses.

Removes the endpoint, the now-unused FeedbackResponse import in the
evaluations router, and the dead getAllFeedbacks frontend wrapper. The
shared Feedbacks.get_all_feedbacks data-layer method is kept, since the
live /feedbacks/all/export endpoint still uses it.

Ref: open-webui#22206

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor: remove unused POST /api/v1/utils/markdown endpoint (#24779)

POST /utils/markdown rendered a markdown string to HTML server-side. Its
only frontend wrapper, getHTMLFromMarkdown in src/lib/apis/utils/index.ts,
is dead: nothing imports or calls it, the route is hit by no other code
path, and the path string appears nowhere else in the repo (no direct
fetch, no test, no docs). Markdown is rendered client-side in the UI, so
this endpoint was redundant.

Fully self-contained removal: the endpoint, its MarkdownForm model, the
now-orphaned 'import markdown' in the utils router (used only here), and
the dead getHTMLFromMarkdown wrapper. Nothing else depends on any of them.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refac

* refac

* refac

* refac

Co-Authored-By: Algorithm5838 <108630393+Algorithm5838@users.noreply.github.com>

* refactor: remove unused DELETE /chats/{id}/tags/all endpoint (#24785)

The bulk-clear-chat-tags endpoint's only frontend wrapper,
deleteTagsById in src/lib/apis/chats/index.ts, is dead: nothing imports
or calls it, the path is referenced nowhere else, and the route handler
has no internal caller. Removes the route handler, the dead wrapper, and
the now-orphaned Chats.delete_all_tags_by_id_and_user_id model method
(its sole caller was this route). The shared
Chats.delete_orphan_tags_for_user method is untouched.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refac: audio

* fix: pass subscription_key and endpoint in bing.py CLI search_bing() call (#24768)

The __main__ block called search_bing() with 4 positional arguments, but
the function requires 5 (subscription_key, endpoint, locale, query,
count). Running `python -m open_webui.retrieval.web.bing` raised a
TypeError and, before failing, silently misrouted every argument. Read
the key/endpoint from environment variables, matching config.py defaults.

Closes #24765

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix: check destination calendar write access on event update (#24764)

update_event only verified write access on the event's source calendar.
CalendarEventUpdateForm accepts a new calendar_id which the model layer
applies unconditionally, so a user with write access to their own calendar
could move (inject) an event into any other user's calendar. Mirror the
destination check create_event already performs.

* refactor: remove dead generateFollowUps frontend wrapper (#24794)

generateFollowUps in src/lib/apis/index.ts is dead: it appears only at
its own definition, nothing imports or calls it, and it targets a
non-existent path (/api/v1/tasks/follow_ups/completions, plural) while
the real route is /tasks/follow_up/completions (singular). Follow-up
suggestions are generated server-side in the chat-completion middleware
and delivered over the chat:message:follow_ups websocket event, so this
wrapper was never on the live path.

Removes only the dead wrapper. The backend POST /tasks/follow_up/completions
endpoint is intentionally kept: it is a member of the actively-used
/tasks/*/completions family (title, tags, emoji, queries, moa) and its
handler delegates to the core generate_follow_ups function.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix: validate Playwright navigations and gate redirects in web loader (#24756)

SafePlaywrightURLLoader validated only the initially submitted URL and
then let the browser follow HTTP redirects and client-side navigations
without re-checking them, so a public URL could redirect into the
internal network (cloud metadata, RFC1918, loopback). Intercept
document-type requests, re-run validate_url on each, and apply the same
redirect policy as the requests loader (blocked unless
AIOHTTP_CLIENT_ALLOW_REDIRECTS). Sub-resource requests pass through
unchanged so page rendering performance is unaffected.

Co-authored-by: POV9en <POV9en@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refac

* refac

* refac

* enh: linkup

* fix: log expected fetch/transcript/tool-server failures as warnings (#24903)

* fix: emit [DONE] for AsyncGenerator pipe returns (#24763)

* fix: respect access_type in shared-chat file authorization branch (#24755)

has_access_to_file granted access whenever the file was attached to a
shared chat the user could read, ignoring the requested access_type. A
read-only shared-chat recipient therefore satisfied write and delete
checks and could delete or mutate the chat owner's attached file. Gate
the shared-chat branch on read access, matching the channels branch
directly above it.

Co-authored-by: oxsignal <oxsignal@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refac

* refac

* fix: mitigate DNS rebinding in web loader fetch paths (#24759)

validate_url() resolves DNS to check IPs but discards the result; the
HTTP client resolves again independently.  Between those two lookups an
attacker can swap the DNS record from a public IP to an internal one
(DNS rebinding).

Push the IP-is-global check into the actual connection layer so the
validated resolution is the one used for the TCP connect:

- aiohttp (_fetch): _SSRFSafeResolver wraps DefaultResolver and rejects
  non-global IPs at resolve time (zero TOCTOU window).
- requests (_scrape): _SSRFSafeAdapter mounts custom urllib3 connection
  classes whose _new_conn resolves, validates, and connects to the
  validated IP in one shot (zero TOCTOU window).

Both paths respect ENABLE_RAG_LOCAL_WEB_FETCH (skip validation when on).

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

* fix: disable redirect following in OAuth picture fetch (SSRF) (#24809)

_process_picture_url validated the initial picture URL with validate_url()
but then aiohttp followed 3xx redirects without re-validating the target,
so a validate_url-passing public URL could 302 to an internal address and
the body was base64-stored in the user's profile_image_url. This is the
sixth call site of the CVE-2026-45401 redirect-bypass cohort; the other
five already pass allow_redirects=AIOHTTP_CLIENT_ALLOW_REDIRECTS. Apply
the same.

* refac

Co-Authored-By: Classic298 <27028174+Classic298@users.noreply.github.com>

* Run transcode_audio_to_mp3 in a thread to avoid blocking (#24876)

This incorporates the transcoding implementation in #24145.

* refac

Co-Authored-By: Sergey Zinchenko <sergey.zinchenko.rnd@gmail.com>

* refactor: remove unused GET /prompts/command/{command} endpoint (#24782)

The lookup-prompt-by-command endpoint's only frontend wrapper,
getPromptByCommand, is dead: nothing imports or calls it, the path is
referenced nowhere else, and the route handler has no internal caller
(slash-command resolution happens client-side from the loaded prompt
list). Removes the route handler, its section header, and the dead
wrapper.

The shared Prompts.get_prompt_by_command data-layer method is kept: it
is still used by create/update prompt validation (prompts.py:175, 275,
340). PromptAccessResponse and AccessGrants are untouched.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refac

* refac: kb sync

* refac

* refac

* refac

* refac

* refac

* refac: clean up Redis sentinel utilities and import grouping

* fix: resolve NameError for redis_sentinels in session_cleanup_lock

The variable was renamed to ws_sentinels but session_cleanup_lock
still referenced the old name, causing a startup crash.

* refac

* refac

* refac

* refac

* refac

* refac

* refac

* Update knowledge.py (#25053)

* refac

* refac

* fix(prompts): resolve undefined session variable in _get_access_grants and _to_prompt_model (#25129)

Both _get_access_grants and _to_prompt_model referenced an undefined
local variable 'session' instead of the 'db' parameter passed to each
method. Because these helpers are called outside of any
'async with get_async_db_context()' block, 'session' did not exist in
their scope, causing a NameError on every prompt fetch.

The NameError was silently swallowed by the broad 'except Exception'
clause in get_prompt_by_id, which returned None — causing the frontend
[id]/+page.svelte to immediately redirect back to /workspace/prompts
rather than rendering the prompt editor.

Also adds the missing 'logging' import and module-level 'log' logger,
which was referenced (but never imported) in insert_new_prompt,
update_prompt_version, and delete_prompt_by_id.

* fix: add knowledge_id access check in search_knowledge_files (BOLA) (#25113)

When called without attached model knowledge and given a caller-supplied knowledge_id, search_knowledge_files passed it straight to Knowledges.search_files_by_id, which does not enforce ownership on knowledge_id. An authenticated user who happened to know a target UUID could enumerate file metadata (filename, file id, KB id, KB name, updated_at) from any knowledge base, bypassing the AccessGrants permission model.

Mirror the same admin/owner/AccessGrants check the attached-KB branch already uses, matching the sibling query_knowledge_files function.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refac

* fix(auth): use request.scope["path"] to prevent CVE-2026-48710 (BadHost) (#25123)

Starlette reconstructs request.url.path from the HTTP Host header without
validation. An attacker can inject a path into the Host header to make
request.url.path return a different value than the path Starlette routes on.

The API key endpoint restriction check was using request.url.path to decide
whether to allow or deny access — making it bypassable via a crafted Host
header on any Starlette version prior to 1.0.1.

Fix: replace request.url.path with request.scope["path"], which reads the
raw ASGI scope path that Starlette uses for routing. This value is set by
the ASGI server from the actual request path and cannot be injected via
HTTP headers, making it safe regardless of Starlette version.

Affected code path:
  get_current_user_by_api_key() in backend/open_webui/utils/auth.py
  (only triggered when ENABLE_API_KEYS_ENDPOINT_RESTRICTIONS is enabled)

References:
  CVE-2026-48710 / BadHost
  https://arstechnica.com/information-technology/2026/05/millions-of-ai-agents-imperiled-by-critical-vulnerability-in-open-source-package/

* I18n/improve chinese translation (#25114)

* i18n: improve zh-CN translation

* i18n: improve zh-TW translation

* refac

* refac

* refac

* refac

* fix: harden model profile image against SVG stored XSS (#25060)

ModelMeta.profile_image_url now runs validate_profile_image_url, rejecting SVG/script data URIs (matching UserUpdateForm and ChannelWebhookForm). The /model/profile/image endpoint enforces the PROFILE_IMAGE_ALLOWED_MIME_TYPES allowlist and sets X-Content-Type-Options: nosniff, so an SVG data URI can no longer be served inline on-origin. Closes the fourth profile-image XSS sink missed by the user and webhook fixes.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix: gate chat-file links by caller access + repair insert_chat_files db arg (#25054)

insert_chat_files() stored any caller-supplied file_id with no ownership
check, so a user could attach another user's file to their own chat and
then read it through the shared-chat access path in has_access_to_file().
Filter file_ids to those the caller owns, is admin for, or can read.

Also repairs an UnboundLocalError introduced in 260ead64d: the existing
duplicate-check referenced `session` before it was assigned (db=session),
so the function threw on every call and no chat_file rows were persisted.

* Update fi-FI translation.json (#24963)

Added missing translations and improved existing ones.

* Update main.py (#25271)

* fix(i18n): add missing Korean plural _one keys for selected/sources/minutes (#25228)

* fix: sanitize mermaid SVG output to prevent stored XSS in file preview (#25219)

renderMermaidDiagram returned raw mermaid SVG, which FilePreview.svelte injects
via wrapper.innerHTML = svg. Mermaid runs with securityLevel: 'loose', so it
neither sanitizes click hrefs (formatUrl skips sanitizeUrl) nor DOMPurifies its
output; a .md file with a click X href "javascript:..." directive (or an
HTML-label payload) therefore executes script in the app origin when previewed.
The chat path was already safe because SVGPanZoom DOMPurifies before rendering;
file preview was not.

Sanitize at the source: renderMermaidDiagram now returns DOMPurify-cleaned SVG
via a shared sanitizeSvg helper (same policy as SVGPanZoom), so every consumer
including the FilePreview innerHTML sink receives safe output.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* fix: preserve parent_id on chat_message upsert (#25205)

* fix(ui): include reasoning_tags in user settings advanced params save handler (#25204)

* fix(ui): prevent long usernames from overflowing Edit User modal, User Preview modal, and sidebar (#25185)

Long usernames overflow the Edit User modal, User Preview modal header,
and the sidebar user area because the flex containers lack width
constraints.

- EditUserModal: add min-w-0 to the flex-1 container so the existing
  truncate class takes effect
- UserPreviewModal: add min-w-0 and truncate to the title container,
  flex-shrink-0 to the close button so it stays visible
- Sidebar: add truncate to the username display and flex-shrink-0 to
  the avatar container to prevent it from being squeezed

* fix(ui): add voice mode mute shortcut to keyboard shortcuts modal (#25193)

* fix(types): add missing markdown rendering settings to Settings type (#25198)

* i18n: complete Turkish (tr-TR) translation (#25210)

* fix: remove hardcoded WEBUI_SECRET_KEY fallback, require key explicitly (#25218)

The 't0p-s3cr3t' default was dead code on every supported startup path:
start.sh, start_windows.bat and `open-webui serve` all set or
auto-generate WEBUI_SECRET_KEY before the backend imports env.py. It was
only ever reachable by invoking uvicorn directly, which is unsupported
and unsafe (the app would then sign tokens/cookies with a public,
hardcoded key). It also keeps getting reported as a vulnerability because
it looks dangerous, even though it is unreachable in practice.

Drop the fallback (default to '') so an unset key is caught by the
existing WEBUI_AUTH guard, and replace the vague error with a clear,
actionable message explaining that the key is a hard requirement and how
the supported start methods provide it. Exit cleanly via SystemExit
instead of raising a ValueError traceback.

WEBUI_AUTH=False keeps working unchanged (key defaults to '').

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix: move bypass_system_prompt off query parameter onto request.state (#25156)

bypass_system_prompt is an internal flag used by utils/middleware.py and utils/chat.py to skip applying the model system prompt on recursive base-model calls, but it was still declared as a positional argument on the openai/ollama chat-completion route handlers, so FastAPI bound it from the query string. Move it to request.state so external clients cannot set it, matching how bypass_filter is handled.

Drop the argument from both route signatures and read getattr(request.state, 'bypass_system_prompt', False); utils/chat.py sets request.state.bypass_system_prompt alongside bypass_filter and drops the kwarg from the two route-handler calls (the recursive self-calls keep it). Mirrors c0385f60b.

Co-authored-by: anishgirianish <161533316+anishgirianish@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* i18n(pl-PL): add missing polish translations (#25176)

* fix(models): gracefully handle legacy svg profile_image_url in ModelMeta validator (#25173)

The SVG-XSS hardening introduced in f5f4b5895 correctly rejects
data:image/svg+xml URIs on new input, but also caused a
pydantic_core.ValidationError when reading pre-existing models from
the database that had SVG data URIs stored as their profile images.

This ValidationError propagated unhandled through _to_model_model and
get_all_models, crashing the entire /api/models endpoint with HTTP 500
and leaving users with no models available in the UI.

Fix:
- Wrap validate_profile_image_url() in a try/except ValueError inside
  ModelMeta.check_profile_image_url. Legacy entries are cleared to None
  with a warning log instead of raising — the /model/profile/image API
  endpoint already falls back to /static/favicon.png when the value is
  empty.
- Default ModelMeta.profile_image_url to None instead of hardcoding
  /static/favicon.png, since the serving endpoint handles the fallback.
- Add a per-model try/except in ModelsTable.get_all_models so that any
  future unexpected validation failure on a single record skips that
  model with an error log rather than aborting the entire list.

* refac

* fix: add null guards to channel Thread and PinnedMessagesModal components (#25209)

Thread.svelte: Add null check for messagesContainerElement in scrollToBottom()
to match the existing pattern in Channel.svelte. Prevents potential TypeError
when the DOM element is not yet bound during rapid thread switches.

PinnedMessagesModal.svelte: Move res.length check inside the if (res) block.
Previously, res.length was accessed unconditionally after a guarded block,
causing TypeError when the API call fails and the .catch() returns null.

* fix(settings): correct presence_penalty and repeat_penalty saving wrong values (#25183)

Both presence_penalty and repeat_penalty in the saveHandler read from
params.frequency_penalty instead of their own values due to a
copy-paste error. This causes users adjusting either parameter to
silently save the frequency_penalty value instead.

* fix(ui): enable custom parameters in user settings and admin model settings (#25200)

Add missing custom={true} prop to AdvancedParams in General.svelte and
ModelSettingsModal.svelte so the 'Add Custom Parameter' option appears
consistently across all advanced parameter surfaces.

Also forward custom_params in the General.svelte save handler so custom
parameters are persisted instead of silently dropped on save.

* i18n : (ms-MY) refine translation and standardise terminology (#25164)

* refac

* refac

* fix: decode terminal proxy path until stable to block multi-encoded traversal (#25157)

_sanitize_proxy_path decoded the proxy path once before the '..' check, so a double-encoded payload (%252e%252e) survived the check as %2e%2e and was then re-decoded into '..' by the upstream terminal server, defeating the traversal guard. Decode until stable so no encoded traversal sequence can reach the upstream. Single-encoded payloads were already rejected; this closes the double (and deeper) encoding bypass.

Co-authored-by: sermikr0 <230672901+sermikr0@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refac

* refac

* chore: Update CHANGELOG.md (#24680)

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* fix: use db instead of undefined session in chats model (#25455)

* fix(ui): use correct 'blur' event name instead of 'blur-sm' in window listeners (#25459)

* fix(ui): correct inverted high-contrast text colors for user message timestamp (#25461)

* refac

* refac

* refac

* refac

* fix: clear usage interval in finally so it cannot leak on send failure (#25478)

getChatEventEmitter starts a setInterval emitting a `usage` socket event
every second; clearInterval ran only after sendMessageSocket resolved, so
any throw/reject left the interval firing for the page lifetime. Each
failed send added another orphaned interval, inflating server-side usage
accounting and growing CPU/memory over a session.

Wrap the send in try/finally so the interval is always cleared, on both
the happy path and any thrown/rejected path. The exception still
propagates unchanged.

Fixes #25465

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat: cap profile image data URI size to bound model/avatar bloat (#25476)

* feat: cap profile image data URI size to bound model/avatar bloat

validate_profile_image_url() validated data-URI format (MIME allowlist,
SVG rejection, scheme checks) but never its length, so a valid
data:image/...;base64,<huge> passed for both custom-model icons and user
avatars. Large inline images bloat Postgres and the Redis MODELS hash and
degrade model-list latency.

Add PROFILE_IMAGE_MAX_DATA_URI_SIZE (default 256 KiB, 0 disables) and
reject oversized data URIs in the shared validator, so both model meta
(ModelMeta.profile_image_url) and user avatars (UpdateProfileForm) are
bounded at one chokepoint. ModelMeta already clears invalid values to
None on read, so existing oversized icons stop propagating into the
MODELS hash on the next refresh.

Fixes #25468

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix: default PROFILE_IMAGE_MAX_DATA_URI_SIZE to None (no cap)

Per review: opt-in rather than a 256 KiB default. Unset leaves data URIs
uncapped; the validator already skips the check on a falsy value.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refac

* fix: don't hang terminal proxy when one forwarding pump exits first (#25479)

The proxy gathered _client_to_upstream and _upstream_to_client with
return_exceptions=True. When upstream sends a graceful CLOSE,
_upstream_to_client returns but gather keeps waiting on
_client_to_upstream, which is blocked in ws.receive() until the browser
disconnects. The handler stays pending and the finally: session.close()
cleanup is deferred, leaking a ClientSession and an open browser socket.

Use asyncio.wait(return_when=FIRST_COMPLETED) and cancel the pending
sibling, so the proxy unwinds as soon as either direction finishes. The
pumps' bare except Exception already lets CancelledError (a BaseException)
propagate, so cancellation is clean and they need no change.

Fixes #25464

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refac

Co-Authored-By: bannert <58707896+bannert1337@users.noreply.github.com>

* refac

Co-Authored-By: Boris Rybalkin <ribalkin@gmail.com>

* fix: cache path traversal via sibling-prefix bypass in serve_cache_file (#25086)

serve_cache_file gated the resolved path with file_path.startswith(os.path.abspath(CACHE_DIR)) without a trailing os.sep, so any path resolving to a sibling whose name starts with the cache-dir basename (e.g. cache_backup, cached_models) passed the prefix check. Authenticated users could read files from such siblings via /cache/../<sibling>/<file>. Appending os.sep to the prefix closes the bypass; deep traversal and absolute paths were already correctly blocked.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix: persist outlet filter changes to message output (#24884)

* refac

* fix(images): pass content_type=None to r.json() to accept non-standard MIME types (#24838)

aiohttp's ClientResponse.json() validates the Content-Type header against
'application/json' by default and raises ContentTypeError for any other
value — including 'application/x-ndjson', which Ollama returns for its
OpenAI-compatible /v1/images/generations endpoint.

Pass content_type=None to skip this check while keeping all other parsing
behaviour unchanged.  The fix covers image generation (openai, gemini,
automatic1111 engines) and image editing (openai, gemini engines).

* fix(ui): guard JSON.parse(localStorage) calls with try/catch to prevent UI crashes (#25481)

* fix: don't crash on startup when stdout can't encode the banner (#25482)

The startup banner uses Unicode box-drawing characters. On a stdout that
can't encode them (Windows cp1252, or redirected/headless/pythonw output)
print() raises UnicodeEncodeError and aborts startup. This blocks running
open-webui serve headless on Windows.

Guard the banner print and fall back to a plain ASCII line so startup
always proceeds regardless of the console encoding.

Fixes #24965

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refac

* fix(knowledge): remove premature drag-and-drop upload toast (#25484)

* refac

* fix: don't block first-admin signup on stale ENABLE_SIGNUP (#24821)

Symptom
On a fresh install (zero users) the frontend shows the mandatory
"Create Admin Account" onboarding screen, but POST
/api/v1/auths/signup returns 403 ACCESS_PROHIBITED ("You do not have
permission to access this resource."). Wiping the database does not
help when the config layer is backed by Redis (the value survives in
the Redis/valkey volume), or when only the user table is cleared (the
config row survives in Postgres). The instance is then unrecoverable
through the UI.

Root cause
signup_handler() auto-sets request.app.state.config.ENABLE_SIGNUP =
False immediately after the first admin is created. That value is
persisted by the config layer (the Postgres config table, and Redis
when REDIS_URL is set). On a later zero-user database the persisted
False is read back, so ENABLE_SIGNUP resolves False even though no
users exist. The old gate was:

    if WEBUI_AUTH:
        if not ENABLE_SIGNUP or not ENABLE_LOGIN_FORM:
            if has_users or not ENABLE_INITIAL_ADMIN_SIGNUP:
                403

ENABLE_INITIAL_ADMIN_SIGNUP defaults to False, so with zero users the
inner test (has_users or not ENABLE_INITIAL_ADMIN_SIGNUP) is True, and
a stale ENABLE_SIGNUP=False trips the outer test, producing a 403 on
the only UI path that can create the first admin. The frontend decides
to show onboarding purely from user_count == 0, so frontend and
backend disagree and the instance bricks.

Change
Split the gate by has_users. Subsequent signups (has_users True) are
unchanged: still gated by ENABLE_SIGNUP and ENABLE_LOGIN_FORM. The
first user (has_users False, the bootstrap admin the onboarding screen
invites) is gated only by the admin-chosen ENABLE_LOGIN_FORM (the
documented SSO-only hard-disable) unless ENABLE_INITIAL_ADMIN_SIGNUP is
set. It is no longer gated by ENABLE_SIGNUP, which in the zero-user
state is never an admin decision but the post-first-admin auto-disable
leaking across a database reset.

Why this is safe (full case analysis)
For WEBUI_AUTH the gate has 16 input combinations over (has_users,
ENABLE_SIGNUP, ENABLE_LOGIN_FORM, ENABLE_INITIAL_ADMIN_SIGNUP). Old and
new are identical in 15 of them:
  * All 8 has_users=True cases: both reduce to "403 iff not
    ENABLE_SIGNUP or not ENABLE_LOGIN_FORM". Unchanged.
  * 7 of the 8 has_users=False cases: identical.
The only changed case is has_users=False, ENABLE_SIGNUP=False,
ENABLE_LOGIN_FORM=True, ENABLE_INITIAL_ADMIN_SIGNUP=False: old
behaviour 403, new behaviour allow. The new condition is a strict
subset of the old (new-403 implies old-403), so the change never newly
blocks any request that previously succeeded; it only stops blocking
that one bootstrap state.

That state has no legitimate deployment. With the login form enabled
and zero users the onboarding form is already served, and the only
operator-configurable way to keep the first signup closed (SSO-only:
ENABLE_LOGIN_FORM=False, optionally with ENABLE_INITIAL_ADMIN_SIGNUP)
is preserved byte for byte. ENABLE_SIGNUP=False with zero users is not
an operator choice, it is the automatic post-first-admin disable, so
the old behaviour there was purely a brick with no recovery path. No
security control is weakened: ENABLE_LOGIN_FORM and
ENABLE_INITIAL_ADMIN_SIGNUP keep their exact meaning, and the
WEBUI_AUTH=False path is untouched.

This is not Redis-specific: it reproduces with Redis disabled through
the Postgres config table alone (clear the user table, keep the config
row).

Verification
Drove the real signup endpoint across a 10-case matrix on freshly
migrated databases, including the full end-to-end first-admin creation
(returns role=admin, row persisted as admin) and the preserved
SSO-only, subsequent-signup and no-auth behaviours. All pass.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix: don't revert replace/outlet content on chat save (#25485)

update_chat_by_id re-derived assistant content from `output` on every
save (serialize_output) so frontend edits to output items reflect in
content. But it ran unconditionally, so content set independently of
output — an __event_emitter__ {"type":"replace"} from an Action, or an
outlet filter footer — was reverted to the original output-derived text
on the next save. The reload reads chat.chat directly, so the change
vanished after navigating away (regression vs 0.9.2, which predates the
output mechanism).

Re-derive only when the message's `output` actually changed versus what's
stored, which still reflects genuine output edits but leaves
independently-set content intact.

Fixes #24585

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix: reject collection names with unsafe characters in RAG ACL (#24982)

Open WebUI's collection ACL accepted any unknown name as a
legacy/ephemeral collection. In Milvus multi-tenancy mode that name
becomes the `resource_id` and is interpolated unescaped into a SQL-like
Milvus expression — `resource_id == '<name>'` — so a name like
  x' or resource_id != '' or resource_id == 'x
turns the filter into a tautology and returns every tenant's chunks
from the shared collection.

All collection names Open WebUI generates are UUIDs, SHA-256 hex
digests, or fixed-prefix variants of those — they all fit
[A-Za-z0-9_-]. Add a strict format check in
filter_accessible_collections (utils.py) that drops any name outside
that set before any ACL or vector-store lookup, applied even on the
admin bypass path. _validate_collection_access then surfaces the dropped
name as a 403.

As defense in depth, MilvusClient now validates resource_id at every
expression-construction site and escapes single quotes / backslashes in
any other string interpolated into a filter (delete ids, metadata
filter values). Non-string filter values are typed-checked instead of
str()-formatted.

Co-authored-by: Claude <noreply@anthropic.com>

* refac

* refac: mirror native FC code_interpreter authz gates onto legacy XML-tag path (#24724)

The native function-calling tool resolver in utils/tools.py applies five
gates before exposing execute_code as a builtin tool: builtin-category
enable, ENABLE_CODE_INTERPRETER global config, model capability,
features.code_interpreter request flag, and the per-user
features.code_interpreter permission.

The legacy XML-tag detection path in streaming_chat_response_handler
applied only the request-flag gate. Brings the legacy path to parity by
running the same five-gate check before activating tag detection.
Behaviour change is limited to deployments that previously relied on
the asymmetry — admins who set ENABLE_CODE_INTERPRETER=False or revoked
the per-user permission, on the legacy tool-calling mode, with the
client supplying features.code_interpreter=true. Any of those three
conditions met now correctly disables tag detection.

Co-authored-by: sfwani <sfwani@users.noreply.github.com>

* fix: handle list-shape data in Firecrawl /search response (#24712)

Firecrawl /search returns either `{"data": [...]}` (flat list — v1, and
what frost19k reported on #23966) or `{"data": {"web": [...]}}` (v2,
current production). The parser only handled the dict shape:

    data = response.get('data') or {}
    results = data.get('web') or []

On a list-shape response, `data.get('web')` raised AttributeError,
caught by the function's outer try/except, and `search_firecrawl`
silently returned []. Web search worked against v2 endpoints but is one
upstream-format-change away from failing closed again. Accept either.

* fix: bind prompt history/version ops to the authorized prompt (#25056)

The history diff, delete, and version-restore routes authorize the URL
prompt_id but then act on a caller-supplied history/version id without
checking it belongs to that prompt (IDOR). Filter by prompt_id in
compute_diff and delete_history_entry, and reject a cross-prompt version_id
in update_prompt_version.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refac

Co-Authored-By: Classic298 <27028174+Classic298@users.noreply.github.com>

* feat: add support for Valkey vector database (#24769)

* feat: add support for Valkey vector database

Signed-off-by: Riley Des <riley.desserre@improving.com>

* feat: add CLIENT SETNAME to Valkey vector store connections

Set client_name on GlideClientConfiguration for both the main client
and batch client so connections are identifiable in CLIENT LIST,
monitoring dashboards, and CloudWatch metrics.

Signed-off-by: Riley Des <riley.desserre@improving.com>

---------

Signed-off-by: Riley Des <riley.desserre@improving.com>

* refac

* refac

Co-Authored-By: Classic298 <27028174+Classic298@users.noreply.github.com>

* refac

Co-Authored-By: Zixin Yu <183055163+ivvi0927@users.noreply.github.com>

* refac

* refac

* refac

* Update Kagi API endpoint and request method (#25015)

Co-authored-by: russelg <russelg@users.noreply.github.com>

* feat: add skills management to chat component (#25037)

- Introduced skills functionality in Chat.svelte, MessageInput.svelte, and related components.
- Added SkillsModal for displaying and managing available skills.
- Updated state management to include selectedSkillIds and integrate skills API.
- Enhanced UI to show available skills and their descriptions.
- Updated translations to support skills-related text.

* refac

* refac

* refac

Co-Authored-By: Zaid Marji <91486926+zaid-marji@users.noreply.github.com>

* refac

* refac

* refac

* refac

* refac

* fix: apply RAG_EMBEDDING_QUERY_PREFIX to memory search queries (#24921)

The query_memory endpoint embeds the search query without the configured
RAG_EMBEDDING_QUERY_PREFIX, while every RAG retrieval path in
retrieval/utils.py correctly passes it. Instruction-tuned embedding
models (e.g. Qwen3-Embedding) produce poor results without the prefix,
causing memory search to return semantically unrelated results.

* refac

* fix: gate chat_completion channel: branch on channel access + message scoping (#24725)

* fix: gate chat_completion channel: branch on channel access + message scoping

When chat_id starts with 'channel:' the chat-completion handler skips
the chat ownership / storage block below it. Nothing replaced that
gate. The downstream channel emitter in socket/main.py:_make_channel_
emitter writes to Messages.update_message_by_id using a caller-supplied
message_id pulled from form_data['id'], with no membership check, no
write-access check, and no validation that the message_id belongs to
the channel.

Net effect: any authenticated user could submit
chat_id='channel:<any-channel-uuid>' + id='<any-message-uuid>' and
overwrite that message with attacker-controlled LLM output. Cross-
channel writes worked too — private channels, DMs, channels the
caller has no access to. Original author attribution stayed intact on
the overwritten row.

Add the missing checks at the channel: branch:

1. Channel must exist (404 otherwise).
2. Non-admin caller must have write access to the channel — membership
   for group/dm channels, AccessGrants permission='write' for others.
3. The message_id (if supplied) must belong to the same channel — a
   caller with write access to channel A cannot use this path to
   overwrite a message in channel B.

Behaviour change is limited to callers who were exploiting the gap:
legitimate flows that supply a message_id under their own channel
membership continue to work unchanged.

Co-authored-by: sfwani <sfwani@users.noreply.github.com>

* chore: trim verbose comment on channel: branch gate

---------

Co-authored-by: sfwani <sfwani@users.noreply.github.com>

* refac

* feat(retrieval): add Perplexity attribution header (#24833)

Signed-off-by: James Liounis <james.liounis@perplexity.ai>

* refac

* refac

* Update CHANGELOG.md (#25453)

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* fix: polyfill readable stream async iteration for Safari PDF extraction (#25473)

* refactor: move background tasks handler call to ensure consistent execution in chat response handlers (#24717)

* refac

* fix(oauth): use Protected Resource Metadata scopes in static OAuth 2.1 flow (#24690)

The static credentials OAuth flow currently sets scope=None, relying on
the OAuth provider's default scopes. This breaks providers like GitHub
that default to minimal/public-only access when no scope is requested.

This change reads scopes_supported from the Protected Resource Metadata
document (RFC 9728) and uses them in the authorization request. Unlike
the Authorization Server's scopes_supported (a full catalog of every
scope the AS can grant), the PRM scopes_supported represents what the
specific resource requires — making it safe to request without breaking
providers like Entra ID that reject broad scope requests.

Fixes the regression introduced in 349ea4ea where all scope handling was
removed from the static flow.

* refac

* chore: bump

* chore

* chore: format

* Update CHANGELOG.md (#25491)

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* refac

* refac

Co-Authored-By: Syed Mustafa Quadri <175467872+code-quad3@users.noreply.github.com>

* chore: format

* refac

Co-Authored-By: Jacob Leksan <63938553+jmleksan@users.noreply.github.com>

* fix: delete Qdrant points by ID so memory deletions don't orphan vectors (#25495)

The Qdrant backends implemented delete(ids=...) as a payload filter on
metadata.id, but points are stored with the item id as the Qdrant point id
(see _create_points), and not every point carries an id in its payload.
Memory points store only {created_at} in metadata (KB metadata embeddings
likewise), so deleting a single memory matched nothing and left an orphaned
vector that kept being injected into RAG context.

Delete by point id instead: PointIdsList for the standard backend, and a
tenant-scoped HasIdCondition for multitenancy (point ids are unique, so tenant
isolation is preserved). Filter-based deletion is unchanged.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* i18n(fr-fr): update frensh translations (#24614)

Co-authored-by: Marina Pantazis <marina.pantazis@bit.admin.ch>
Co-authored-by: Tim Baek <tim@openwebui.com>

* fix: block private-IP webhook URLs to close SSRF on caller-controlled URL (#24587)

* fix: block private-IP webhook URLs to close SSRF on caller-controlled URL

post_webhook(url, ...) in utils/webhook.py forwards the URL straight to
aiohttp.ClientSession.post with no SSRF gate. The URL is caller-controlled
on two surfaces:

- User notification settings under ENABLE_USER_WEBHOOKS=true — any
  authenticated user can set the URL their notifications POST to.
- Automation notification triggers (calendar alerts, etc.).

Without a gate, the URL can target cloud metadata (169.254.169.254 /
fd00:ec2::254), localhost-bound services, RFC1918 internal hosts, or any
other private address reachable from the server process. Blind SSRF — no
response body returned to the caller — but enough to enumerate internal
services via response timing / status codes, and on cloud deployments
enough to issue requests against IMDSv1 if available.

Call validate_url() at the top of post_webhook. The function blocks
private/reserved IPs when ENABLE_RAG_LOCAL_WEB_FETCH is False (the
default), is the project's chosen SSRF gate, and is already applied to
the equivalent fetch surfaces (retrieval, image-load, OAuth profile
picture). Operators who legitimately need to webhook to private IPs
(internal monitoring, self-hosted Slack alternatives, etc.) can set
ENABLE_RAG_LOCAL_WEB_FETCH=True — same opt-out as the other gated
surfaces.

Scope intentionally limited to webhooks. The OAuth discovery and
external reranker paths cwanglab also flagged are admin-configured with
intentional private-IP defaults (reranker defaults to
http://localhost:8080/v1/rerank) and are out of scope per Rule 9 — the
admin owns the URL choice and the operator opt-out exists for them too.

Reported by cwanglab in GHSA-5x9f-85cg-w3hf (cluster canonical with six
closed siblings: g36v-23gj-j69x, 6j8f-h58v-xgmw, xpwv-52pm-p8hj,
v9gp-hv2c-9qv8, fw7w-jrw7-p3v9, x7xq-74rg-m8mf).

Co-authored-by: cwanglab <cwanglab@users.noreply.github.com>

* fix: also pass allow_redirects=False on webhook post_webhook session.post

Companion to the previous commit. validate_url() only validates the
initial URL; aiohttp's default allow_redirects=True would still follow
a 302 to a private-IP target. Same redirect-bypass class as the rh5x
cluster's five call sites, sixth call site to receive the same gate.

Co-authored-by: cwanglab <cwanglab@users.noreply.github.com>

---------

Co-authored-by: cwanglab <cwanglab@users.noreply.github.com>

* refac

* chore: format

* refac

* refac

* refac

---------

Signed-off-by: Riley Des <riley.desserre@improving.com>
Signed-off-by: James Liounis <james.liounis@perplexity.ai>
Co-authored-by: _00_ <131402327+rgaricano@users.noreply.github.com>
Co-authored-by: Taey <pythontogoplease@gmail.com>
Co-authored-by: Daniel Nylander <po@danielnylander.se>
Co-authored-by: Daniel Nylander <daniel@danielnylander.se>
Co-authored-by: Asbjørn Dyhrberg Thegler <asbjoern@dyhrbergthegler.dk>
Co-authored-by: Aleix Dorca <aleixdorca@mac.com>
Co-authored-by: Classic298 <27028174+Classic298@users.noreply.github.com>
Co-authored-by: ShigekiTsuchiyama <ShigekiTsuchiyama@users.noreply.github.com>
Co-authored-by: berkant-koc <berkant-koc@users.noreply.github.com>
Co-authored-by: mayamsin <58122670+mayamsin@users.noreply.github.com>
Co-authored-by: Algorithm5838 <108630393+Algorithm5838@users.noreply.github.com>
Co-authored-by: G30 <50341825+silentoplayz@users.noreply.github.com>
Co-authored-by: Aindriú Mac Giolla Eoin <aindriu80@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: POV9en <POV9en@users.noreply.github.com>
Co-authored-by: oxsignal <oxsignal@users.noreply.github.com>
Co-authored-by: Dara Adib <dara@quietapple.org>
Co-authored-by: Sergey Zinchenko <sergey.zinchenko.rnd@gmail.com>
Co-authored-by: Shirasawa <764798966@qq.com>
Co-authored-by: Kylapaallikko <Kylapaallikko@users.noreply.github.com>
Co-authored-by: maco <gosarmarcel7@gmail.com>
Co-authored-by: Sakıp Han Dursun <100518315+sakiphan@users.noreply.github.com>
Co-authored-by: anishgirianish <161533316+anishgirianish@users.noreply.github.com>
Co-authored-by: Mateusz Hajder <6783135+mhajder@users.noreply.github.com>
Co-authored-by: Amir Subhi <amirsubhi@hotmail.com>
Co-authored-by: sermikr0 <230672901+sermikr0@users.noreply.github.com>
Co-authored-by: bannert <58707896+bannert1337@users.noreply.github.com>
Co-authored-by: Boris Rybalkin <ribalkin@gmail.com>
Co-authored-by: HW <hwinkler@first-it-consulting.de>
Co-authored-by: sfwani <sfwani@users.noreply.github.com>
Co-authored-by: rileydes-improving <riley.desserre@improving.com>
Co-authored-by: Zixin Yu <183055163+ivvi0927@users.noreply.github.com>
Co-authored-by: Lukáš Kucharczyk <lukas@kucharczyk.xyz>
Co-authored-by: russelg <russelg@users.noreply.github.com>
Co-authored-by: Mr. Meowgi <ovehbe@gmail.com>
Co-authored-by: Zaid Marji <91486926+zaid-marji@users.noreply.github.com>
Co-authored-by: Craig <66838006+TipKnuckle@users.noreply.github.com>
Co-authored-by: James Liounis <james.liounis@perplexity.ai>
Co-authored-by: Chane Lu <2522992009@qq.com>
Co-authored-by: Jacob Leksan <63938553+jmleksan@users.noreply.github.com>
Co-authored-by: Justin Williams <justinjohnwilliams@gmail.com>
Co-authored-by: Syed Mustafa Quadri <175467872+code-quad3@users.noreply.github.com>
Co-authored-by: hungryBird <pantazis.marina@gmail.com>
Co-authored-by: Marina Pantazis <marina.pantazis@bit.admin.ch>
Co-authored-by: cwanglab <cwanglab@users.noreply.github.com>
2026-06-01 14:56:48 -07:00
Timothy Jaeryang Baek
de2360ac07 refac 2026-06-01 14:38:54 -07:00
Timothy Jaeryang Baek
8862210139 refac 2026-06-01 14:38:49 -07:00
Timothy Jaeryang Baek
5062f9958f refac 2026-06-01 14:35:45 -07:00
Timothy Jaeryang Baek
eb629866fe chore: format 2026-06-01 14:33:43 -07:00
Timothy Jaeryang Baek
54149903f0 refac 2026-06-01 14:23:27 -07:00
Classic298
02b2a391e9
fix: block private-IP webhook URLs to close SSRF on caller-controlled URL (#24587)
* fix: block private-IP webhook URLs to close SSRF on caller-controlled URL

post_webhook(url, ...) in utils/webhook.py forwards the URL straight to
aiohttp.ClientSession.post with no SSRF gate. The URL is caller-controlled
on two surfaces:

- User notification settings under ENABLE_USER_WEBHOOKS=true — any
  authenticated user can set the URL their notifications POST to.
- Automation notification triggers (calendar alerts, etc.).

Without a gate, the URL can target cloud metadata (169.254.169.254 /
fd00:ec2::254), localhost-bound services, RFC1918 internal hosts, or any
other private address reachable from the server process. Blind SSRF — no
response body returned to the caller — but enough to enumerate internal
services via response timing / status codes, and on cloud deployments
enough to issue requests against IMDSv1 if available.

Call validate_url() at the top of post_webhook. The function blocks
private/reserved IPs when ENABLE_RAG_LOCAL_WEB_FETCH is False (the
default), is the project's chosen SSRF gate, and is already applied to
the equivalent fetch surfaces (retrieval, image-load, OAuth profile
picture). Operators who legitimately need to webhook to private IPs
(internal monitoring, self-hosted Slack alternatives, etc.) can set
ENABLE_RAG_LOCAL_WEB_FETCH=True — same opt-out as the other gated
surfaces.

Scope intentionally limited to webhooks. The OAuth discovery and
external reranker paths cwanglab also flagged are admin-configured with
intentional private-IP defaults (reranker defaults to
http://localhost:8080/v1/rerank) and are out of scope per Rule 9 — the
admin owns the URL choice and the operator opt-out exists for them too.

Reported by cwanglab in GHSA-5x9f-85cg-w3hf (cluster canonical with six
closed siblings: g36v-23gj-j69x, 6j8f-h58v-xgmw, xpwv-52pm-p8hj,
v9gp-hv2c-9qv8, fw7w-jrw7-p3v9, x7xq-74rg-m8mf).

Co-authored-by: cwanglab <cwanglab@users.noreply.github.com>

* fix: also pass allow_redirects=False on webhook post_webhook session.post

Companion to the previous commit. validate_url() only validates the
initial URL; aiohttp's default allow_redirects=True would still follow
a 302 to a private-IP target. Same redirect-bypass class as the rh5x
cluster's five call sites, sixth call site to receive the same gate.

Co-authored-by: cwanglab <cwanglab@users.noreply.github.com>

---------

Co-authored-by: cwanglab <cwanglab@users.noreply.github.com>
2026-06-01 14:15:51 -07:00
hungryBird
c73d9da773
i18n(fr-fr): update frensh translations (#24614)
Co-authored-by: Marina Pantazis <marina.pantazis@bit.admin.ch>
Co-authored-by: Tim Baek <tim@openwebui.com>
2026-06-01 14:15:38 -07:00
Classic298
eb38389636
fix: delete Qdrant points by ID so memory deletions don't orphan vectors (#25495)
The Qdrant backends implemented delete(ids=...) as a payload filter on
metadata.id, but points are stored with the item id as the Qdrant point id
(see _create_points), and not every point carries an id in its payload.
Memory points store only {created_at} in metadata (KB metadata embeddings
likewise), so deleting a single memory matched nothing and left an orphaned
vector that kept being injected into RAG context.

Delete by point id instead: PointIdsList for the standard backend, and a
tenant-scoped HasIdCondition for multitenancy (point ids are unique, so tenant
isolation is preserved). Filter-based deletion is unchanged.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-01 14:13:46 -07:00
Timothy Jaeryang Baek
eebbc48f80 refac
Co-Authored-By: Jacob Leksan <63938553+jmleksan@users.noreply.github.com>
2026-06-01 14:13:28 -07:00
Timothy Jaeryang Baek
cff51f05f5 chore: format 2026-06-01 14:10:40 -07:00
Timothy Jaeryang Baek
a4735e46b9 refac
Co-Authored-By: Syed Mustafa Quadri <175467872+code-quad3@users.noreply.github.com>
2026-06-01 14:09:54 -07:00
Timothy Jaeryang Baek
6c8dfd8175 refac 2026-06-01 14:08:34 -07:00
Classic298
caaa99c7d3
Update CHANGELOG.md (#25491)
* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md
2026-06-01 14:07:20 -07:00
Timothy Jaeryang Baek
6fce92aa12 chore: format 2026-06-01 13:56:55 -07:00
Timothy Jaeryang Baek
1f49ebf431 chore 2026-06-01 13:56:06 -07:00
Timothy Jaeryang Baek
bfe5523c52 chore: bump 2026-06-01 13:54:10 -07:00
Timothy Jaeryang Baek
77c8c54b1e refac 2026-06-01 13:53:19 -07:00
Justin Williams
478bc9e3f1
fix(oauth): use Protected Resource Metadata scopes in static OAuth 2.1 flow (#24690)
The static credentials OAuth flow currently sets scope=None, relying on
the OAuth provider's default scopes. This breaks providers like GitHub
that default to minimal/public-only access when no scope is requested.

This change reads scopes_supported from the Protected Resource Metadata
document (RFC 9728) and uses them in the authorization request. Unlike
the Authorization Server's scopes_supported (a full catalog of every
scope the AS can grant), the PRM scopes_supported represents what the
specific resource requires — making it safe to request without breaking
providers like Entra ID that reject broad scope requests.

Fixes the regression introduced in 349ea4ea where all scope handling was
removed from the static flow.
2026-06-01 13:52:18 -07:00
Timothy Jaeryang Baek
675e9bee5a refac 2026-06-01 13:51:03 -07:00
Jacob Leksan
80da840ae5
refactor: move background tasks handler call to ensure consistent execution in chat response handlers (#24717) 2026-06-01 13:50:15 -07:00
Chane Lu
974b13839e
fix: polyfill readable stream async iteration for Safari PDF extraction (#25473) 2026-06-01 13:47:57 -07:00
Classic298
886c132a10
Update CHANGELOG.md (#25453)
* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md
2026-06-01 13:46:17 -07:00
Timothy Jaeryang Baek
c7de057a4a refac 2026-06-01 13:45:23 -07:00
Timothy Jaeryang Baek
750604a11d refac 2026-06-01 13:43:05 -07:00
James Liounis
69c88e163d
feat(retrieval): add Perplexity attribution header (#24833)
Signed-off-by: James Liounis <james.liounis@perplexity.ai>
2026-06-01 13:40:52 -07:00
Timothy Jaeryang Baek
c8eb8edca4 refac 2026-06-01 13:38:40 -07:00
Classic298
33e4e0dcc4
fix: gate chat_completion channel: branch on channel access + message scoping (#24725)
* fix: gate chat_completion channel: branch on channel access + message scoping

When chat_id starts with 'channel:' the chat-completion handler skips
the chat ownership / storage block below it. Nothing replaced that
gate. The downstream channel emitter in socket/main.py:_make_channel_
emitter writes to Messages.update_message_by_id using a caller-supplied
message_id pulled from form_data['id'], with no membership check, no
write-access check, and no validation that the message_id belongs to
the channel.

Net effect: any authenticated user could submit
chat_id='channel:<any-channel-uuid>' + id='<any-message-uuid>' and
overwrite that message with attacker-controlled LLM output. Cross-
channel writes worked too — private channels, DMs, channels the
caller has no access to. Original author attribution stayed intact on
the overwritten row.

Add the missing checks at the channel: branch:

1. Channel must exist (404 otherwise).
2. Non-admin caller must have write access to the channel — membership
   for group/dm channels, AccessGrants permission='write' for others.
3. The message_id (if supplied) must belong to the same channel — a
   caller with write access to channel A cannot use this path to
   overwrite a message in channel B.

Behaviour change is limited to callers who were exploiting the gap:
legitimate flows that supply a message_id under their own channel
membership continue to work unchanged.

Co-authored-by: sfwani <sfwani@users.noreply.github.com>

* chore: trim verbose comment on channel: branch gate

---------

Co-authored-by: sfwani <sfwani@users.noreply.github.com>
2026-06-01 13:37:32 -07:00
Timothy Jaeryang Baek
7f7cd21018 refac 2026-06-01 13:34:50 -07:00
Craig
ce4dca47cb
fix: apply RAG_EMBEDDING_QUERY_PREFIX to memory search queries (#24921)
The query_memory endpoint embeds the search query without the configured
RAG_EMBEDDING_QUERY_PREFIX, while every RAG retrieval path in
retrieval/utils.py correctly passes it. Instruction-tuned embedding
models (e.g. Qwen3-Embedding) produce poor results without the prefix,
causing memory search to return semantically unrelated results.
2026-06-01 13:23:15 -07:00
Timothy Jaeryang Baek
160a6694e4 refac 2026-06-01 13:20:33 -07:00
Timothy Jaeryang Baek
4705c2d988 refac 2026-06-01 13:20:26 -07:00
Timothy Jaeryang Baek
778dba1d6b refac 2026-06-01 13:18:44 -07:00
Timothy Jaeryang Baek
27fb20c13a refac 2026-06-01 13:15:21 -07:00
Timothy Jaeryang Baek
eb4eebc3ce refac 2026-06-01 13:10:19 -07:00
Timothy Jaeryang Baek
d64ef1803d refac
Co-Authored-By: Zaid Marji <91486926+zaid-marji@users.noreply.github.com>
2026-06-01 13:07:49 -07:00
Timothy Jaeryang Baek
346dab3d8f refac 2026-06-01 13:03:13 -07:00
Timothy Jaeryang Baek
01810e32ad refac 2026-06-01 13:02:48 -07:00
Mr. Meowgi
a9da054572
feat: add skills management to chat component (#25037)
- Introduced skills functionality in Chat.svelte, MessageInput.svelte, and related components.
- Added SkillsModal for displaying and managing available skills.
- Updated state management to include selectedSkillIds and integrate skills API.
- Enhanced UI to show available skills and their descriptions.
- Updated translations to support skills-related text.
2026-06-01 12:53:30 -07:00
Lukáš Kucharczyk
42c2393f8e
Update Kagi API endpoint and request method (#25015)
Co-authored-by: russelg <russelg@users.noreply.github.com>
2026-06-01 12:48:44 -07:00
Timothy Jaeryang Baek
e90a618f45 refac 2026-06-01 12:47:59 -07:00
Timothy Jaeryang Baek
4297c02b12 refac 2026-06-01 12:44:16 -07:00
Timothy Jaeryang Baek
9035601bdb refac 2026-06-01 12:41:30 -07:00
Timothy Jaeryang Baek
e3ab4bd212 refac
Co-Authored-By: Zixin Yu <183055163+ivvi0927@users.noreply.github.com>
2026-06-01 12:37:34 -07:00
Timothy Jaeryang Baek
fd76b51ab2 refac
Co-Authored-By: Classic298 <27028174+Classic298@users.noreply.github.com>
2026-06-01 12:27:08 -07:00
Timothy Jaeryang Baek
c0f1aa2919 refac 2026-06-01 12:24:45 -07:00
rileydes-improving
567c4aabe9
feat: add support for Valkey vector database (#24769)
* feat: add support for Valkey vector database

Signed-off-by: Riley Des <riley.desserre@improving.com>

* feat: add CLIENT SETNAME to Valkey vector store connections

Set client_name on GlideClientConfiguration for both the main client
and batch client so connections are identifiable in CLIENT LIST,
monitoring dashboards, and CloudWatch metrics.

Signed-off-by: Riley Des <riley.desserre@improving.com>

---------

Signed-off-by: Riley Des <riley.desserre@improving.com>
2026-06-01 12:20:01 -07:00
Timothy Jaeryang Baek
8644532f5b refac
Co-Authored-By: Classic298 <27028174+Classic298@users.noreply.github.com>
2026-06-01 12:13:04 -07:00
Classic298
9a3eea6448
fix: bind prompt history/version ops to the authorized prompt (#25056)
The history diff, delete, and version-restore routes authorize the URL
prompt_id but then act on a caller-supplied history/version id without
checking it belongs to that prompt (IDOR). Filter by prompt_id in
compute_diff and delete_history_entry, and reject a cross-prompt version_id
in update_prompt_version.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-01 12:07:52 -07:00
Classic298
e623081b2b
fix: handle list-shape data in Firecrawl /search response (#24712)
Firecrawl /search returns either `{"data": [...]}` (flat list — v1, and
what frost19k reported on #23966) or `{"data": {"web": [...]}}` (v2,
current production). The parser only handled the dict shape:

    data = response.get('data') or {}
    results = data.get('web') or []

On a list-shape response, `data.get('web')` raised AttributeError,
caught by the function's outer try/except, and `search_firecrawl`
silently returned []. Web search worked against v2 endpoints but is one
upstream-format-change away from failing closed again. Accept either.
2026-06-01 12:07:31 -07:00
Classic298
507b8b213c
refac: mirror native FC code_interpreter authz gates onto legacy XML-tag path (#24724)
The native function-calling tool resolver in utils/tools.py applies five
gates before exposing execute_code as a builtin tool: builtin-category
enable, ENABLE_CODE_INTERPRETER global config, model capability,
features.code_interpreter request flag, and the per-user
features.code_interpreter permission.

The legacy XML-tag detection path in streaming_chat_response_handler
applied only the request-flag gate. Brings the legacy path to parity by
running the same five-gate check before activating tag detection.
Behaviour change is limited to deployments that previously relied on
the asymmetry — admins who set ENABLE_CODE_INTERPRETER=False or revoked
the per-user permission, on the legacy tool-calling mode, with the
client supplying features.code_interpreter=true. Any of those three
conditions met now correctly disables tag detection.

Co-authored-by: sfwani <sfwani@users.noreply.github.com>
2026-06-01 12:07:15 -07:00
Timothy Jaeryang Baek
c93f071700 refac 2026-06-01 11:58:16 -07:00
Classic298
76947ff926
fix: reject collection names with unsafe characters in RAG ACL (#24982)
Open WebUI's collection ACL accepted any unknown name as a
legacy/ephemeral collection. In Milvus multi-tenancy mode that name
becomes the `resource_id` and is interpolated unescaped into a SQL-like
Milvus expression — `resource_id == '<name>'` — so a name like
  x' or resource_id != '' or resource_id == 'x
turns the filter into a tautology and returns every tenant's chunks
from the shared collection.

All collection names Open WebUI generates are UUIDs, SHA-256 hex
digests, or fixed-prefix variants of those — they all fit
[A-Za-z0-9_-]. Add a strict format check in
filter_accessible_collections (utils.py) that drops any name outside
that set before any ACL or vector-store lookup, applied even on the
admin bypass path. _validate_collection_access then surfaces the dropped
name as a 403.

As defense in depth, MilvusClient now validates resource_id at every
expression-construction site and escapes single quotes / backslashes in
any other string interpolated into a filter (delete ids, metadata
filter values). Non-string filter values are typed-checked instead of
str()-formatted.

Co-authored-by: Claude <noreply@anthropic.com>
2026-06-01 11:48:43 -07:00
Classic298
a089842368
fix: don't revert replace/outlet content on chat save (#25485)
update_chat_by_id re-derived assistant content from `output` on every
save (serialize_output) so frontend edits to output items reflect in
content. But it ran unconditionally, so content set independently of
output — an __event_emitter__ {"type":"replace"} from an Action, or an
outlet filter footer — was reverted to the original output-derived text
on the next save. The reload reads chat.chat directly, so the change
vanished after navigating away (regression vs 0.9.2, which predates the
output mechanism).

Re-derive only when the message's `output` actually changed versus what's
stored, which still reflects genuine output edits but leaves
independently-set content intact.

Fixes #24585

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-01 11:45:05 -07:00
Classic298
558ea2a152
fix: don't block first-admin signup on stale ENABLE_SIGNUP (#24821)
Symptom
On a fresh install (zero users) the frontend shows the mandatory
"Create Admin Account" onboarding screen, but POST
/api/v1/auths/signup returns 403 ACCESS_PROHIBITED ("You do not have
permission to access this resource."). Wiping the database does not
help when the config layer is backed by Redis (the value survives in
the Redis/valkey volume), or when only the user table is cleared (the
config row survives in Postgres). The instance is then unrecoverable
through the UI.

Root cause
signup_handler() auto-sets request.app.state.config.ENABLE_SIGNUP =
False immediately after the first admin is created. That value is
persisted by the config layer (the Postgres config table, and Redis
when REDIS_URL is set). On a later zero-user database the persisted
False is read back, so ENABLE_SIGNUP resolves False even though no
users exist. The old gate was:

    if WEBUI_AUTH:
        if not ENABLE_SIGNUP or not ENABLE_LOGIN_FORM:
            if has_users or not ENABLE_INITIAL_ADMIN_SIGNUP:
                403

ENABLE_INITIAL_ADMIN_SIGNUP defaults to False, so with zero users the
inner test (has_users or not ENABLE_INITIAL_ADMIN_SIGNUP) is True, and
a stale ENABLE_SIGNUP=False trips the outer test, producing a 403 on
the only UI path that can create the first admin. The frontend decides
to show onboarding purely from user_count == 0, so frontend and
backend disagree and the instance bricks.

Change
Split the gate by has_users. Subsequent signups (has_users True) are
unchanged: still gated by ENABLE_SIGNUP and ENABLE_LOGIN_FORM. The
first user (has_users False, the bootstrap admin the onboarding screen
invites) is gated only by the admin-chosen ENABLE_LOGIN_FORM (the
documented SSO-only hard-disable) unless ENABLE_INITIAL_ADMIN_SIGNUP is
set. It is no longer gated by ENABLE_SIGNUP, which in the zero-user
state is never an admin decision but the post-first-admin auto-disable
leaking across a database reset.

Why this is safe (full case analysis)
For WEBUI_AUTH the gate has 16 input combinations over (has_users,
ENABLE_SIGNUP, ENABLE_LOGIN_FORM, ENABLE_INITIAL_ADMIN_SIGNUP). Old and
new are identical in 15 of them:
  * All 8 has_users=True cases: both reduce to "403 iff not
    ENABLE_SIGNUP or not ENABLE_LOGIN_FORM". Unchanged.
  * 7 of the 8 has_users=False cases: identical.
The only changed case is has_users=False, ENABLE_SIGNUP=False,
ENABLE_LOGIN_FORM=True, ENABLE_INITIAL_ADMIN_SIGNUP=False: old
behaviour 403, new behaviour allow. The new condition is a strict
subset of the old (new-403 implies old-403), so the change never newly
blocks any request that previously succeeded; it only stops blocking
that one bootstrap state.

That state has no legitimate deployment. With the login form enabled
and zero users the onboarding form is already served, and the only
operator-configurable way to keep the first signup closed (SSO-only:
ENABLE_LOGIN_FORM=False, optionally with ENABLE_INITIAL_ADMIN_SIGNUP)
is preserved byte for byte. ENABLE_SIGNUP=False with zero users is not
an operator choice, it is the automatic post-first-admin disable, so
the old behaviour there was purely a brick with no recovery path. No
security control is weakened: ENABLE_LOGIN_FORM and
ENABLE_INITIAL_ADMIN_SIGNUP keep their exact meaning, and the
WEBUI_AUTH=False path is untouched.

This is not Redis-specific: it reproduces with Redis disabled through
the Postgres config table alone (clear the user table, keep the config
row).

Verification
Drove the real signup endpoint across a 10-case matrix on freshly
migrated databases, including the full end-to-end first-admin creation
(returns role=admin, row persisted as admin) and the preserved
SSO-only, subsequent-signup and no-auth behaviours. All pass.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-01 11:44:36 -07:00
Timothy Jaeryang Baek
ad9f2eeb15 refac 2026-06-01 11:34:46 -07:00
G30
1010fd3142
fix(knowledge): remove premature drag-and-drop upload toast (#25484) 2026-06-01 11:09:58 -07:00
Timothy Jaeryang Baek
1bbb2b933d refac 2026-06-01 11:08:58 -07:00
Classic298
cc15a01778
fix: don't crash on startup when stdout can't encode the banner (#25482)
The startup banner uses Unicode box-drawing characters. On a stdout that
can't encode them (Windows cp1252, or redirected/headless/pythonw output)
print() raises UnicodeEncodeError and aborts startup. This blocks running
open-webui serve headless on Windows.

Guard the banner print and fall back to a plain ASCII line so startup
always proceeds regardless of the console encoding.

Fixes #24965

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-01 10:41:43 -07:00
G30
ceb1cbc009
fix(ui): guard JSON.parse(localStorage) calls with try/catch to prevent UI crashes (#25481) 2026-06-01 10:33:54 -07:00
HW
bddadafa07
fix(images): pass content_type=None to r.json() to accept non-standard MIME types (#24838)
aiohttp's ClientResponse.json() validates the Content-Type header against
'application/json' by default and raises ContentTypeError for any other
value — including 'application/x-ndjson', which Ollama returns for its
OpenAI-compatible /v1/images/generations endpoint.

Pass content_type=None to skip this check while keeping all other parsing
behaviour unchanged.  The fix covers image generation (openai, gemini,
automatic1111 engines) and image editing (openai, gemini engines).
2026-06-01 10:33:24 -07:00
Timothy Jaeryang Baek
936d5f2676 refac 2026-06-01 10:26:39 -07:00
Algorithm5838
309caa82fb
fix: persist outlet filter changes to message output (#24884) 2026-06-01 10:24:40 -07:00
Classic298
b0fa4384ea
fix: cache path traversal via sibling-prefix bypass in serve_cache_file (#25086)
serve_cache_file gated the resolved path with file_path.startswith(os.path.abspath(CACHE_DIR)) without a trailing os.sep, so any path resolving to a sibling whose name starts with the cache-dir basename (e.g. cache_backup, cached_models) passed the prefix check. Authenticated users could read files from such siblings via /cache/../<sibling>/<file>. Appending os.sep to the prefix closes the bypass; deep traversal and absolute paths were already correctly blocked.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-01 10:17:02 -07:00
Timothy Jaeryang Baek
07cbc91a8e refac
Co-Authored-By: Boris Rybalkin <ribalkin@gmail.com>
2026-06-01 10:16:01 -07:00
Timothy Jaeryang Baek
0e73f7af09 refac
Co-Authored-By: bannert <58707896+bannert1337@users.noreply.github.com>
2026-06-01 10:13:50 -07:00
Classic298
02b65ea582
fix: don't hang terminal proxy when one forwarding pump exits first (#25479)
The proxy gathered _client_to_upstream and _upstream_to_client with
return_exceptions=True. When upstream sends a graceful CLOSE,
_upstream_to_client returns but gather keeps waiting on
_client_to_upstream, which is blocked in ws.receive() until the browser
disconnects. The handler stays pending and the finally: session.close()
cleanup is deferred, leaking a ClientSession and an open browser socket.

Use asyncio.wait(return_when=FIRST_COMPLETED) and cancel the pending
sibling, so the proxy unwinds as soon as either direction finishes. The
pumps' bare except Exception already lets CancelledError (a BaseException)
propagate, so cancellation is clean and they need no change.

Fixes #25464

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-01 10:10:04 -07:00
Timothy Jaeryang Baek
7594823edd refac 2026-06-01 10:09:40 -07:00
Classic298
83890f18b9
feat: cap profile image data URI size to bound model/avatar bloat (#25476)
* feat: cap profile image data URI size to bound model/avatar bloat

validate_profile_image_url() validated data-URI format (MIME allowlist,
SVG rejection, scheme checks) but never its length, so a valid
data:image/...;base64,<huge> passed for both custom-model icons and user
avatars. Large inline images bloat Postgres and the Redis MODELS hash and
degrade model-list latency.

Add PROFILE_IMAGE_MAX_DATA_URI_SIZE (default 256 KiB, 0 disables) and
reject oversized data URIs in the shared validator, so both model meta
(ModelMeta.profile_image_url) and user avatars (UpdateProfileForm) are
bounded at one chokepoint. ModelMeta already clears invalid values to
None on read, so existing oversized icons stop propagating into the
MODELS hash on the next refresh.

Fixes #25468

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix: default PROFILE_IMAGE_MAX_DATA_URI_SIZE to None (no cap)

Per review: opt-in rather than a 256 KiB default. Unset leaves data URIs
uncapped; the validator already skips the check on a falsy value.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-01 09:57:06 -07:00
Classic298
e2502ec80f
fix: clear usage interval in finally so it cannot leak on send failure (#25478)
getChatEventEmitter starts a setInterval emitting a `usage` socket event
every second; clearInterval ran only after sendMessageSocket resolved, so
any throw/reject left the interval firing for the page lifetime. Each
failed send added another orphaned interval, inflating server-side usage
accounting and growing CPU/memory over a session.

Wrap the send in try/finally so the interval is always cleared, on both
the happy path and any thrown/rejected path. The exception still
propagates unchanged.

Fixes #25465

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-01 09:56:49 -07:00
Timothy Jaeryang Baek
6f0277db52 refac 2026-06-01 09:53:04 -07:00
Timothy Jaeryang Baek
55ca719bbf refac 2026-06-01 09:48:29 -07:00
Timothy Jaeryang Baek
9e3e24e304 refac 2026-06-01 09:42:54 -07:00
Timothy Jaeryang Baek
b64fd988f0 refac 2026-06-01 09:30:15 -07:00
G30
229e65b9f0
fix(ui): correct inverted high-contrast text colors for user message timestamp (#25461) 2026-06-01 09:26:18 -07:00
G30
bbd3e13094
fix(ui): use correct 'blur' event name instead of 'blur-sm' in window listeners (#25459) 2026-06-01 09:25:55 -07:00
Algorithm5838
16e7827134
fix: use db instead of undefined session in chats model (#25455) 2026-06-01 09:25:35 -07:00
Classic298
5028ba18ee
chore: Update CHANGELOG.md (#24680)
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 / merge (map[name:slim suffix:-slim]) (push) Blocked by required conditions
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 / merge (map[name:main suffix:]) (push) Blocked by required conditions
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 / merge (map[name:ollama suffix:-ollama]) (push) Blocked by required conditions
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 / 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 / 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 (-ollama, ollama) (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 / Format & Build (push) Waiting to run
Frontend Build / Unit Tests (push) Waiting to run
* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md
2026-05-31 18:42:49 -07:00
Timothy Jaeryang Baek
7139797be0 refac 2026-05-31 18:42:28 -07:00
Timothy Jaeryang Baek
ee47c9c833 refac 2026-05-31 18:34:37 -07:00
Classic298
0354775917
fix: decode terminal proxy path until stable to block multi-encoded traversal (#25157)
_sanitize_proxy_path decoded the proxy path once before the '..' check, so a double-encoded payload (%252e%252e) survived the check as %2e%2e and was then re-decoded into '..' by the upstream terminal server, defeating the traversal guard. Decode until stable so no encoded traversal sequence can reach the upstream. Single-encoded payloads were already rejected; this closes the double (and deeper) encoding bypass.

Co-authored-by: sermikr0 <230672901+sermikr0@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-31 15:11:19 -07:00
Timothy Jaeryang Baek
d4030a8aa5 refac 2026-05-31 15:10:48 -07:00
Timothy Jaeryang Baek
1f0948bcbe refac 2026-05-31 15:06:22 -07:00
Amir Subhi
746979d012
i18n : (ms-MY) refine translation and standardise terminology (#25164) 2026-05-31 15:05:01 -07:00
G30
18b1a041dd
fix(ui): enable custom parameters in user settings and admin model settings (#25200)
Add missing custom={true} prop to AdvancedParams in General.svelte and
ModelSettingsModal.svelte so the 'Add Custom Parameter' option appears
consistently across all advanced parameter surfaces.

Also forward custom_params in the General.svelte save handler so custom
parameters are persisted instead of silently dropped on save.
2026-05-31 15:02:32 -07:00
G30
1608a04fa9
fix(settings): correct presence_penalty and repeat_penalty saving wrong values (#25183)
Both presence_penalty and repeat_penalty in the saveHandler read from
params.frequency_penalty instead of their own values due to a
copy-paste error. This causes users adjusting either parameter to
silently save the frequency_penalty value instead.
2026-05-31 15:02:07 -07:00
G30
5112e0b62c
fix: add null guards to channel Thread and PinnedMessagesModal components (#25209)
Thread.svelte: Add null check for messagesContainerElement in scrollToBottom()
to match the existing pattern in Channel.svelte. Prevents potential TypeError
when the DOM element is not yet bound during rapid thread switches.

PinnedMessagesModal.svelte: Move res.length check inside the if (res) block.
Previously, res.length was accessed unconditionally after a guarded block,
causing TypeError when the API call fails and the .catch() returns null.
2026-05-31 15:01:17 -07:00
Timothy Jaeryang Baek
f16b5c4460 refac 2026-05-31 14:59:28 -07:00
G30
34b8844ccf
fix(models): gracefully handle legacy svg profile_image_url in ModelMeta validator (#25173)
The SVG-XSS hardening introduced in f5f4b5895 correctly rejects
data:image/svg+xml URIs on new input, but also caused a
pydantic_core.ValidationError when reading pre-existing models from
the database that had SVG data URIs stored as their profile images.

This ValidationError propagated unhandled through _to_model_model and
get_all_models, crashing the entire /api/models endpoint with HTTP 500
and leaving users with no models available in the UI.

Fix:
- Wrap validate_profile_image_url() in a try/except ValueError inside
  ModelMeta.check_profile_image_url. Legacy entries are cleared to None
  with a warning log instead of raising — the /model/profile/image API
  endpoint already falls back to /static/favicon.png when the value is
  empty.
- Default ModelMeta.profile_image_url to None instead of hardcoding
  /static/favicon.png, since the serving endpoint handles the fallback.
- Add a per-model try/except in ModelsTable.get_all_models so that any
  future unexpected validation failure on a single record skips that
  model with an error log rather than aborting the entire list.
2026-05-31 14:55:07 -07:00
Mateusz Hajder
b52d142c8d
i18n(pl-PL): add missing polish translations (#25176) 2026-05-31 14:54:23 -07:00
Classic298
4719881105
fix: move bypass_system_prompt off query parameter onto request.state (#25156)
bypass_system_prompt is an internal flag used by utils/middleware.py and utils/chat.py to skip applying the model system prompt on recursive base-model calls, but it was still declared as a positional argument on the openai/ollama chat-completion route handlers, so FastAPI bound it from the query string. Move it to request.state so external clients cannot set it, matching how bypass_filter is handled.

Drop the argument from both route signatures and read getattr(request.state, 'bypass_system_prompt', False); utils/chat.py sets request.state.bypass_system_prompt alongside bypass_filter and drops the kwarg from the two route-handler calls (the recursive self-calls keep it). Mirrors c0385f60b.

Co-authored-by: anishgirianish <161533316+anishgirianish@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-31 14:53:53 -07:00
Classic298
f2650353da
fix: remove hardcoded WEBUI_SECRET_KEY fallback, require key explicitly (#25218)
The 't0p-s3cr3t' default was dead code on every supported startup path:
start.sh, start_windows.bat and `open-webui serve` all set or
auto-generate WEBUI_SECRET_KEY before the backend imports env.py. It was
only ever reachable by invoking uvicorn directly, which is unsupported
and unsafe (the app would then sign tokens/cookies with a public,
hardcoded key). It also keeps getting reported as a vulnerability because
it looks dangerous, even though it is unreachable in practice.

Drop the fallback (default to '') so an unset key is caught by the
existing WEBUI_AUTH guard, and replace the vague error with a clear,
actionable message explaining that the key is a hard requirement and how
the supported start methods provide it. Exit cleanly via SystemExit
instead of raising a ValueError traceback.

WEBUI_AUTH=False keeps working unchanged (key defaults to '').

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 14:53:25 -07:00
Sakıp Han Dursun
76b0e65818
i18n: complete Turkish (tr-TR) translation (#25210) 2026-05-31 14:52:51 -07:00
G30
9c9e4f1288
fix(types): add missing markdown rendering settings to Settings type (#25198) 2026-05-31 14:52:35 -07:00
G30
26b1529a45
fix(ui): add voice mode mute shortcut to keyboard shortcuts modal (#25193) 2026-05-31 14:52:20 -07:00
G30
c665d4a7c6
fix(ui): prevent long usernames from overflowing Edit User modal, User Preview modal, and sidebar (#25185)
Long usernames overflow the Edit User modal, User Preview modal header,
and the sidebar user area because the flex containers lack width
constraints.

- EditUserModal: add min-w-0 to the flex-1 container so the existing
  truncate class takes effect
- UserPreviewModal: add min-w-0 and truncate to the title container,
  flex-shrink-0 to the close button so it stays visible
- Sidebar: add truncate to the username display and flex-shrink-0 to
  the avatar container to prevent it from being squeezed
2026-05-31 14:51:56 -07:00
G30
c428ad0c1a
fix(ui): include reasoning_tags in user settings advanced params save handler (#25204) 2026-05-31 14:50:41 -07:00
Algorithm5838
4b33d7ebc1
fix: preserve parent_id on chat_message upsert (#25205) 2026-05-31 14:50:05 -07:00
Classic298
bf6325ff33
fix: sanitize mermaid SVG output to prevent stored XSS in file preview (#25219)
renderMermaidDiagram returned raw mermaid SVG, which FilePreview.svelte injects
via wrapper.innerHTML = svg. Mermaid runs with securityLevel: 'loose', so it
neither sanitizes click hrefs (formatUrl skips sanitizeUrl) nor DOMPurifies its
output; a .md file with a click X href "javascript:..." directive (or an
HTML-label payload) therefore executes script in the app origin when previewed.
The chat path was already safe because SVGPanZoom DOMPurifies before rendering;
file preview was not.

Sanitize at the source: renderMermaidDiagram now returns DOMPurify-cleaned SVG
via a shared sanitizeSvg helper (same policy as SVGPanZoom), so every consumer
including the FilePreview innerHTML sink receives safe output.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 14:49:43 -07:00
maco
690d6e5eb1
fix(i18n): add missing Korean plural _one keys for selected/sources/minutes (#25228) 2026-05-31 14:49:19 -07:00
Classic298
81d4ed79ae
Update main.py (#25271) 2026-05-31 14:48:51 -07:00
Kylapaallikko
4923920bf1
Update fi-FI translation.json (#24963)
Some checks failed
Frontend Build / Unit Tests (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
Python CI / Ruff Format (3.11) (push) Has been cancelled
Python CI / Ruff Format (3.12) (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_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/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 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
Frontend Build / Format & Build (push) Has been cancelled
Create and publish Docker images with specific build args / merge (map[name:cuda suffix:-cuda]) (push) Has been cancelled
Create and publish Docker images with specific build args / merge (map[name:cuda126 suffix:-cuda126]) (push) Has been cancelled
Create and publish Docker images with specific build args / merge (map[name:main suffix:]) (push) Has been cancelled
Create and publish Docker images with specific build args / merge (map[name:ollama suffix:-ollama]) (push) Has been cancelled
Create and publish Docker images with specific build args / merge (map[name:slim suffix:-slim]) (push) Has been cancelled
Create and publish Docker images with specific build args / copy-to-dockerhub (, main) (push) Has been cancelled
Create and publish Docker images with specific build args / copy-to-dockerhub (-cuda, cuda) (push) Has been cancelled
Create and publish Docker images with specific build args / copy-to-dockerhub (-cuda126, cuda126) (push) Has been cancelled
Create and publish Docker images with specific build args / copy-to-dockerhub (-ollama, ollama) (push) Has been cancelled
Create and publish Docker images with specific build args / copy-to-dockerhub (-slim, slim) (push) Has been cancelled
Added missing translations and improved existing ones.
2026-05-28 17:48:06 -05:00
Classic298
3f1c52e018
fix: gate chat-file links by caller access + repair insert_chat_files db arg (#25054)
insert_chat_files() stored any caller-supplied file_id with no ownership
check, so a user could attach another user's file to their own chat and
then read it through the shared-chat access path in has_access_to_file().
Filter file_ids to those the caller owns, is admin for, or can read.

Also repairs an UnboundLocalError introduced in 260ead64d: the existing
duplicate-check referenced `session` before it was assigned (db=session),
so the function threw on every call and no chat_file rows were persisted.
2026-05-28 17:42:17 -05:00
Classic298
f5f4b58958
fix: harden model profile image against SVG stored XSS (#25060)
ModelMeta.profile_image_url now runs validate_profile_image_url, rejecting SVG/script data URIs (matching UserUpdateForm and ChannelWebhookForm). The /model/profile/image endpoint enforces the PROFILE_IMAGE_ALLOWED_MIME_TYPES allowlist and sets X-Content-Type-Options: nosniff, so an SVG data URI can no longer be served inline on-origin. Closes the fourth profile-image XSS sink missed by the user and webhook fixes.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 17:41:55 -05:00
Timothy Jaeryang Baek
78b1637a03 refac 2026-05-28 17:29:01 -05:00
Timothy Jaeryang Baek
84659035f0 refac 2026-05-28 17:28:14 -05:00
Timothy Jaeryang Baek
91810f1c4e refac 2026-05-28 17:26:31 -05:00
Timothy Jaeryang Baek
591e0aafa1 refac 2026-05-28 17:24:33 -05:00
Shirasawa
e61bcc8500
I18n/improve chinese translation (#25114)
* i18n: improve zh-CN translation

* i18n: improve zh-TW translation
2026-05-28 16:44:55 -05:00
G30
66126f3861
fix(auth): use request.scope["path"] to prevent CVE-2026-48710 (BadHost) (#25123)
Starlette reconstructs request.url.path from the HTTP Host header without
validation. An attacker can inject a path into the Host header to make
request.url.path return a different value than the path Starlette routes on.

The API key endpoint restriction check was using request.url.path to decide
whether to allow or deny access — making it bypassable via a crafted Host
header on any Starlette version prior to 1.0.1.

Fix: replace request.url.path with request.scope["path"], which reads the
raw ASGI scope path that Starlette uses for routing. This value is set by
the ASGI server from the actual request path and cannot be injected via
HTTP headers, making it safe regardless of Starlette version.

Affected code path:
  get_current_user_by_api_key() in backend/open_webui/utils/auth.py
  (only triggered when ENABLE_API_KEYS_ENDPOINT_RESTRICTIONS is enabled)

References:
  CVE-2026-48710 / BadHost
  https://arstechnica.com/information-technology/2026/05/millions-of-ai-agents-imperiled-by-critical-vulnerability-in-open-source-package/
2026-05-28 16:41:56 -05:00
Timothy Jaeryang Baek
79bf3d28d8 refac 2026-05-28 16:33:48 -05:00
Classic298
9f20687680
fix: add knowledge_id access check in search_knowledge_files (BOLA) (#25113)
When called without attached model knowledge and given a caller-supplied knowledge_id, search_knowledge_files passed it straight to Knowledges.search_files_by_id, which does not enforce ownership on knowledge_id. An authenticated user who happened to know a target UUID could enumerate file metadata (filename, file id, KB id, KB name, updated_at) from any knowledge base, bypassing the AccessGrants permission model.

Mirror the same admin/owner/AccessGrants check the attached-KB branch already uses, matching the sibling query_knowledge_files function.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 16:23:49 -05:00
G30
48ea043f82
fix(prompts): resolve undefined session variable in _get_access_grants and _to_prompt_model (#25129)
Both _get_access_grants and _to_prompt_model referenced an undefined
local variable 'session' instead of the 'db' parameter passed to each
method. Because these helpers are called outside of any
'async with get_async_db_context()' block, 'session' did not exist in
their scope, causing a NameError on every prompt fetch.

The NameError was silently swallowed by the broad 'except Exception'
clause in get_prompt_by_id, which returned None — causing the frontend
[id]/+page.svelte to immediately redirect back to /workspace/prompts
rather than rendering the prompt editor.

Also adds the missing 'logging' import and module-level 'log' logger,
which was referenced (but never imported) in insert_new_prompt,
update_prompt_version, and delete_prompt_by_id.
2026-05-28 16:23:25 -05:00
Timothy Jaeryang Baek
a4d1b3e937 refac 2026-05-28 16:19:35 -05:00
Timothy Jaeryang Baek
5d9a09a88a refac
Some checks failed
Python CI / Ruff Format (3.11) (push) Has been cancelled
Python CI / Ruff Format (3.12) (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_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/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
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 / merge (map[name:main suffix:]) (push) Has been cancelled
Create and publish Docker images with specific build args / merge (map[name:ollama suffix:-ollama]) (push) Has been cancelled
Create and publish Docker images with specific build args / merge (map[name:cuda suffix:-cuda]) (push) Has been cancelled
Create and publish Docker images with specific build args / merge (map[name:cuda126 suffix:-cuda126]) (push) Has been cancelled
Create and publish Docker images with specific build args / merge (map[name:slim suffix:-slim]) (push) Has been cancelled
Create and publish Docker images with specific build args / copy-to-dockerhub (, main) (push) Has been cancelled
Create and publish Docker images with specific build args / copy-to-dockerhub (-cuda, cuda) (push) Has been cancelled
Create and publish Docker images with specific build args / copy-to-dockerhub (-cuda126, cuda126) (push) Has been cancelled
Create and publish Docker images with specific build args / copy-to-dockerhub (-ollama, ollama) (push) Has been cancelled
Create and publish Docker images with specific build args / copy-to-dockerhub (-slim, slim) (push) Has been cancelled
2026-05-25 20:15:03 +04:00
Classic298
9a347b0376
Update knowledge.py (#25053) 2026-05-25 20:13:11 +04:00
Timothy Jaeryang Baek
42783881e9 refac
Some checks failed
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
Python CI / Ruff Format (3.11) (push) Has been cancelled
Python CI / Ruff Format (3.12) (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_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/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 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 / merge (map[name:cuda126 suffix:-cuda126]) (push) Has been cancelled
Create and publish Docker images with specific build args / merge (map[name:cuda suffix:-cuda]) (push) Has been cancelled
Create and publish Docker images with specific build args / merge (map[name:main suffix:]) (push) Has been cancelled
Create and publish Docker images with specific build args / merge (map[name:ollama suffix:-ollama]) (push) Has been cancelled
Create and publish Docker images with specific build args / merge (map[name:slim suffix:-slim]) (push) Has been cancelled
Create and publish Docker images with specific build args / copy-to-dockerhub (, main) (push) Has been cancelled
Create and publish Docker images with specific build args / copy-to-dockerhub (-cuda, cuda) (push) Has been cancelled
Create and publish Docker images with specific build args / copy-to-dockerhub (-cuda126, cuda126) (push) Has been cancelled
Create and publish Docker images with specific build args / copy-to-dockerhub (-ollama, ollama) (push) Has been cancelled
Create and publish Docker images with specific build args / copy-to-dockerhub (-slim, slim) (push) Has been cancelled
2026-05-21 18:43:45 +04:00
Timothy Jaeryang Baek
fb16e28d28 refac 2026-05-21 17:48:28 +04:00
Timothy Jaeryang Baek
470a074cd1 refac 2026-05-21 16:56:56 +04:00
Timothy Jaeryang Baek
b94245d2ee refac 2026-05-21 16:44:36 +04:00
Timothy Jaeryang Baek
1acfbb6755 refac 2026-05-21 16:25:25 +04:00
Timothy Jaeryang Baek
d8b5b9fa79 refac 2026-05-21 15:29:49 +04:00
Timothy Jaeryang Baek
260ead64da refac 2026-05-21 14:01:57 +04:00
Timothy Jaeryang Baek
cac4c6da2e fix: resolve NameError for redis_sentinels in session_cleanup_lock
The variable was renamed to ws_sentinels but session_cleanup_lock
still referenced the old name, causing a startup crash.
2026-05-21 13:41:21 +04:00
Timothy Jaeryang Baek
154679200f refac: clean up Redis sentinel utilities and import grouping 2026-05-21 11:47:25 +04:00
Timothy Jaeryang Baek
1527eb6e01 refac
Some checks failed
Python CI / Ruff Format (3.11) (push) Has been cancelled
Python CI / Ruff Format (3.12) (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_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/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
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
Frontend Build / Format & Build (push) Has been cancelled
Frontend Build / Unit Tests (push) Has been cancelled
Create and publish Docker images with specific build args / merge (map[name:slim suffix:-slim]) (push) Has been cancelled
Create and publish Docker images with specific build args / merge (map[name:main suffix:]) (push) Has been cancelled
Create and publish Docker images with specific build args / merge (map[name:ollama suffix:-ollama]) (push) Has been cancelled
Create and publish Docker images with specific build args / merge (map[name:cuda suffix:-cuda]) (push) Has been cancelled
Create and publish Docker images with specific build args / merge (map[name:cuda126 suffix:-cuda126]) (push) Has been cancelled
Create and publish Docker images with specific build args / copy-to-dockerhub (, main) (push) Has been cancelled
Create and publish Docker images with specific build args / copy-to-dockerhub (-cuda, cuda) (push) Has been cancelled
Create and publish Docker images with specific build args / copy-to-dockerhub (-cuda126, cuda126) (push) Has been cancelled
Create and publish Docker images with specific build args / copy-to-dockerhub (-ollama, ollama) (push) Has been cancelled
Create and publish Docker images with specific build args / copy-to-dockerhub (-slim, slim) (push) Has been cancelled
2026-05-20 01:46:59 +04:00
Timothy Jaeryang Baek
8f2d346e10 refac 2026-05-20 01:39:22 +04:00
Timothy Jaeryang Baek
97252fa609 refac 2026-05-20 01:34:07 +04:00
Timothy Jaeryang Baek
9835b3f1dd refac 2026-05-20 01:32:25 +04:00
Timothy Jaeryang Baek
73bdf86766 refac 2026-05-20 01:30:26 +04:00
Timothy Jaeryang Baek
60c9db1cb8 refac: kb sync 2026-05-20 01:13:52 +04:00
Timothy Jaeryang Baek
d0b17f0569 refac 2026-05-20 00:37:04 +04:00
Classic298
b9911c8bc6
refactor: remove unused GET /prompts/command/{command} endpoint (#24782)
The lookup-prompt-by-command endpoint's only frontend wrapper,
getPromptByCommand, is dead: nothing imports or calls it, the path is
referenced nowhere else, and the route handler has no internal caller
(slash-command resolution happens client-side from the loaded prompt
list). Removes the route handler, its section header, and the dead
wrapper.

The shared Prompts.get_prompt_by_command data-layer method is kept: it
is still used by create/update prompt validation (prompts.py:175, 275,
340). PromptAccessResponse and AccessGrants are untouched.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 00:28:07 +04:00
Timothy Jaeryang Baek
c8f851bd2d refac
Co-Authored-By: Sergey Zinchenko <sergey.zinchenko.rnd@gmail.com>
2026-05-20 00:26:22 +04:00
Dara Adib
8a104a7ab1
Run transcode_audio_to_mp3 in a thread to avoid blocking (#24876)
This incorporates the transcoding implementation in #24145.
2026-05-20 00:25:34 +04:00
Timothy Jaeryang Baek
2b99945d27 refac
Co-Authored-By: Classic298 <27028174+Classic298@users.noreply.github.com>
2026-05-20 00:22:27 +04:00
Classic298
d07fd7d6d8
fix: disable redirect following in OAuth picture fetch (SSRF) (#24809)
_process_picture_url validated the initial picture URL with validate_url()
but then aiohttp followed 3xx redirects without re-validating the target,
so a validate_url-passing public URL could 302 to an internal address and
the body was base64-stored in the user's profile_image_url. This is the
sixth call site of the CVE-2026-45401 redirect-bypass cohort; the other
five already pass allow_redirects=AIOHTTP_CLIENT_ALLOW_REDIRECTS. Apply
the same.
2026-05-19 23:57:38 +04:00
Classic298
854440f703
fix: mitigate DNS rebinding in web loader fetch paths (#24759)
validate_url() resolves DNS to check IPs but discards the result; the
HTTP client resolves again independently.  Between those two lookups an
attacker can swap the DNS record from a public IP to an internal one
(DNS rebinding).

Push the IP-is-global check into the actual connection layer so the
validated resolution is the one used for the TCP connect:

- aiohttp (_fetch): _SSRFSafeResolver wraps DefaultResolver and rejects
  non-global IPs at resolve time (zero TOCTOU window).
- requests (_scrape): _SSRFSafeAdapter mounts custom urllib3 connection
  classes whose _new_conn resolves, validates, and connects to the
  validated IP in one shot (zero TOCTOU window).

Both paths respect ENABLE_RAG_LOCAL_WEB_FETCH (skip validation when on).

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-19 23:57:12 +04:00
Timothy Jaeryang Baek
cfa6908d57 refac 2026-05-19 22:25:39 +04:00
Timothy Jaeryang Baek
eb3076c1b0 refac 2026-05-19 22:14:46 +04:00
Classic298
d169f086da
fix: respect access_type in shared-chat file authorization branch (#24755)
has_access_to_file granted access whenever the file was attached to a
shared chat the user could read, ignoring the requested access_type. A
read-only shared-chat recipient therefore satisfied write and delete
checks and could delete or mutate the chat owner's attached file. Gate
the shared-chat branch on read access, matching the channels branch
directly above it.

Co-authored-by: oxsignal <oxsignal@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 22:09:56 +04:00
Algorithm5838
5d104abd08
fix: emit [DONE] for AsyncGenerator pipe returns (#24763) 2026-05-19 22:07:56 +04:00
Classic298
a803372805
fix: log expected fetch/transcript/tool-server failures as warnings (#24903) 2026-05-19 21:55:40 +04:00
Timothy Jaeryang Baek
56c0d00e13 enh: linkup 2026-05-19 21:54:38 +04:00
Timothy Jaeryang Baek
3c5e7968f0 refac 2026-05-19 21:48:44 +04:00
Timothy Jaeryang Baek
94b66b1797 refac 2026-05-19 21:35:12 +04:00
Timothy Jaeryang Baek
ed73ef3d8d refac 2026-05-19 21:35:04 +04:00
Classic298
f02aeea0bb
fix: validate Playwright navigations and gate redirects in web loader (#24756)
SafePlaywrightURLLoader validated only the initially submitted URL and
then let the browser follow HTTP redirects and client-side navigations
without re-checking them, so a public URL could redirect into the
internal network (cloud metadata, RFC1918, loopback). Intercept
document-type requests, re-run validate_url on each, and apply the same
redirect policy as the requests loader (blocked unless
AIOHTTP_CLIENT_ALLOW_REDIRECTS). Sub-resource requests pass through
unchanged so page rendering performance is unaffected.

Co-authored-by: POV9en <POV9en@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 21:27:43 +04:00
Classic298
5401e8560b
refactor: remove dead generateFollowUps frontend wrapper (#24794)
generateFollowUps in src/lib/apis/index.ts is dead: it appears only at
its own definition, nothing imports or calls it, and it targets a
non-existent path (/api/v1/tasks/follow_ups/completions, plural) while
the real route is /tasks/follow_up/completions (singular). Follow-up
suggestions are generated server-side in the chat-completion middleware
and delivered over the chat:message:follow_ups websocket event, so this
wrapper was never on the live path.

Removes only the dead wrapper. The backend POST /tasks/follow_up/completions
endpoint is intentionally kept: it is a member of the actively-used
/tasks/*/completions family (title, tags, emoji, queries, moa) and its
handler delegates to the core generate_follow_ups function.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 21:27:19 +04:00
Classic298
ea058841c9
fix: check destination calendar write access on event update (#24764)
update_event only verified write access on the event's source calendar.
CalendarEventUpdateForm accepts a new calendar_id which the model layer
applies unconditionally, so a user with write access to their own calendar
could move (inject) an event into any other user's calendar. Mirror the
destination check create_event already performs.
2026-05-19 21:26:58 +04:00
Classic298
c48ac5163c
fix: pass subscription_key and endpoint in bing.py CLI search_bing() call (#24768)
The __main__ block called search_bing() with 4 positional arguments, but
the function requires 5 (subscription_key, endpoint, locale, query,
count). Running `python -m open_webui.retrieval.web.bing` raised a
TypeError and, before failing, silently misrouted every argument. Read
the key/endpoint from environment variables, matching config.py defaults.

Closes #24765

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 21:26:10 +04:00
Timothy Jaeryang Baek
2ca91ceeec refac: audio 2026-05-19 21:24:58 +04:00
Classic298
c306a7e16e
refactor: remove unused DELETE /chats/{id}/tags/all endpoint (#24785)
The bulk-clear-chat-tags endpoint's only frontend wrapper,
deleteTagsById in src/lib/apis/chats/index.ts, is dead: nothing imports
or calls it, the path is referenced nowhere else, and the route handler
has no internal caller. Removes the route handler, the dead wrapper, and
the now-orphaned Chats.delete_all_tags_by_id_and_user_id model method
(its sole caller was this route). The shared
Chats.delete_orphan_tags_for_user method is untouched.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 21:03:58 +04:00
Timothy Jaeryang Baek
cc94a90b4d refac
Co-Authored-By: Algorithm5838 <108630393+Algorithm5838@users.noreply.github.com>
2026-05-19 21:03:23 +04:00
Timothy Jaeryang Baek
1fa3050f06 refac 2026-05-19 20:57:56 +04:00
Timothy Jaeryang Baek
1e36a20600 refac 2026-05-19 20:55:04 +04:00
Timothy Jaeryang Baek
0adc090dcb refac 2026-05-19 20:51:53 +04:00
Classic298
f216bfab63
refactor: remove unused POST /api/v1/utils/markdown endpoint (#24779)
POST /utils/markdown rendered a markdown string to HTML server-side. Its
only frontend wrapper, getHTMLFromMarkdown in src/lib/apis/utils/index.ts,
is dead: nothing imports or calls it, the route is hit by no other code
path, and the path string appears nowhere else in the repo (no direct
fetch, no test, no docs). Markdown is rendered client-side in the UI, so
this endpoint was redundant.

Fully self-contained removal: the endpoint, its MarkdownForm model, the
now-orphaned 'import markdown' in the utils router (used only here), and
the dead getHTMLFromMarkdown wrapper. Nothing else depends on any of them.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 20:50:07 +04:00
Classic298
92e727b1fc
refactor: remove unused GET /evaluations/feedbacks/all endpoint (#24778)
GET /evaluations/feedbacks/all returned the entire feedback table in a
single response (flagged as a Medium OOM risk for admins in
open-webui#22206). Its only frontend wrapper, getAllFeedbacks in
src/lib/apis/evaluations/index.ts, is dead: nothing imports or calls it
anywhere in the codebase. The endpoint is a redundant view-only twin of
GET /evaluations/feedbacks/all/export, which is what the admin Feedbacks
UI actually uses.

Removes the endpoint, the now-unused FeedbackResponse import in the
evaluations router, and the dead getAllFeedbacks frontend wrapper. The
shared Feedbacks.get_all_feedbacks data-layer method is kept, since the
live /feedbacks/all/export endpoint still uses it.

Ref: open-webui#22206

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 20:49:22 +04:00
Classic298
557df60c3b
refactor: remove dead frontend API wrappers with no backend route (#24792)
These 19 exported wrappers are dead: each appears exactly once in the
codebase (its own definition), nothing imports or calls any of them, and
none has a corresponding backend route. They are leftovers from settings
that were consolidated server-side into /auths/admin/config,
/openai/config, /ollama/config and /api/config:

- index.ts: getModelFilterConfig, updateModelFilterConfig,
  getCommunitySharingEnabledStatus, toggleCommunitySharingEnabledStatus,
  getModelConfig, updateModelConfig (+ orphaned GlobalModelConfig type)
- auths: getSignUpEnabledStatus, toggleSignUpEnabledStatus,
  getDefaultUserRole, updateDefaultUserRole, getJWTExpiresDuration,
  updateJWTExpiresDuration
- openai: getOpenAIUrls, updateOpenAIUrls, getOpenAIKeys, updateOpenAIKeys
- ollama: updateOllamaUrls
- prompts: restorePromptFromHistory
- folders: updateFolderItemsById (+ orphaned FolderItems type)

Shared types (ModelConfig/ModelMeta/ModelParams) and all live wrappers
are untouched. Removal is import-safe: nothing referenced these.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 20:48:02 +04:00
Timothy Jaeryang Baek
7e9d41d664 refac 2026-05-19 20:44:38 +04:00
Aindriú Mac Giolla Eoin
4f89617192
i18n: update Irish translation (#24883) 2026-05-19 20:41:19 +04:00
Timothy Jaeryang Baek
aa06200f78 refac 2026-05-19 20:37:53 +04:00
Timothy Jaeryang Baek
c75fe8e74b fix: get_image_base64_from_file_id 2026-05-19 20:33:46 +04:00
Timothy Jaeryang Baek
0367ad54c5 chore: pyodide 2026-05-19 20:21:01 +04:00
Timothy Jaeryang Baek
ef975649b2 refac 2026-05-19 20:19:23 +04:00
Timothy Jaeryang Baek
d310a0777c refac 2026-05-19 20:12:00 +04:00
Classic298
359590ca9d
fix: wire workspace.skills into the sidebar + workspace-index gates (#24729)
Some checks failed
Python CI / Ruff Format (3.11) (push) Has been cancelled
Python CI / Ruff Format (3.12) (push) Has been cancelled
Frontend Build / Format & Build (push) Has been cancelled
Frontend Build / Unit Tests (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_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/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
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 / merge (map[name:main suffix:]) (push) Has been cancelled
Create and publish Docker images with specific build args / merge (map[name:ollama suffix:-ollama]) (push) Has been cancelled
Create and publish Docker images with specific build args / merge (map[name:slim suffix:-slim]) (push) Has been cancelled
Create and publish Docker images with specific build args / merge (map[name:cuda suffix:-cuda]) (push) Has been cancelled
Create and publish Docker images with specific build args / merge (map[name:cuda126 suffix:-cuda126]) (push) Has been cancelled
Create and publish Docker images with specific build args / copy-to-dockerhub (, main) (push) Has been cancelled
Create and publish Docker images with specific build args / copy-to-dockerhub (-cuda, cuda) (push) Has been cancelled
Create and publish Docker images with specific build args / copy-to-dockerhub (-cuda126, cuda126) (push) Has been cancelled
Create and publish Docker images with specific build args / copy-to-dockerhub (-ollama, ollama) (push) Has been cancelled
Create and publish Docker images with specific build args / copy-to-dockerhub (-slim, slim) (push) Has been cancelled
Reported by bwgabrielsusai on #24719: granting a user only
`workspace.skills` doesn't show the Workspace menu, and visiting
`/workspace` directly bounces them to `/`.

The per-route guard in `/workspace/+layout.svelte` already covered
skills, but two earlier gates in the chain didn't:

* `Sidebar.svelte` case 'workspace' OR'd models/knowledge/prompts/tools
  to decide menu visibility — skills was missing, so the entry never
  rendered for skills-only users.
* `/workspace/+page.svelte` redirect chain picked the first available
  section — skills was missing, so the fallback `goto('/')` fired.

Adding skills to both.
2026-05-15 09:34:23 +09:00
G30
eba1540a95
feat(ui): add emoji picker to rich text formatting toolbar (#24704) 2026-05-15 09:32:56 +09:00
Algorithm5838
ae48838b04
fix: tag composite pk in migration (#24722) 2026-05-15 09:31:27 +09:00
Classic298
6360af36d8
Update SECURITY.md (#24726) 2026-05-15 09:30:48 +09:00
mayamsin
1ded5ce969
i18n: update Russian translations (#24728) 2026-05-15 09:30:10 +09:00
Classic298
0cd1529008
fix: default optional env vars used with bash ,, in start.sh (#24683)
start.sh runs with `set -euo pipefail`, but three call sites added in
070ab2650 (refac: reorganize scripts and ci workflows) reference
optional env vars via bash's `,,` lowercase expansion without any
default. Containers that don't set these vars — the default for every
deployment that isn't explicitly opting into Playwright / bundled
Ollama / CUDA — crash on startup with:

    start.sh: line 15: WEB_LOADER_ENGINE: unbound variable

(and the same for USE_OLLAMA_DOCKER, USE_CUDA_DOCKER once the first
were set in turn.) Reported in open-webui#24560 by urbenlegend.

The same refactor correctly defaulted every other optional env var
with `${VAR:-…}`. The three `,,` references slipped through because
bash can't combine `:-default` with `,,` in a single substitution —
`${VAR:-default,,}` makes the default literal `,,`, not what's wanted.

Fix: normalise the three vars in a one-line preamble with `${VAR:=}`,
which assigns an empty default if unset. The downstream `${VAR,,}`
expressions stay exactly as Tim wrote them, preserving the file's
visual style and matching the existing `${VAR:-…}` idiom for "this
variable is optional".
2026-05-15 09:29:25 +09:00
Timothy Jaeryang Baek
9263b7568e refac
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: 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 / merge (map[name:main suffix:]) (push) Blocked by required conditions
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 / merge (map[name:ollama suffix:-ollama]) (push) Blocked by required conditions
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 / 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:slim suffix:-slim]) (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 (-ollama, ollama) (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 / Format & Build (push) Waiting to run
Frontend Build / Unit Tests (push) Waiting to run
Co-Authored-By: Classic298 <27028174+Classic298@users.noreply.github.com>
2026-05-14 14:12:42 +09:00
Timothy Jaeryang Baek
73d2065227 fix: legacy peewee tables fk 2026-05-14 14:06:46 +09:00
Timothy Jaeryang Baek
d7cfc1e46a refac 2026-05-14 14:06:32 +09:00
Timothy Jaeryang Baek
9717ada92f refac 2026-05-14 14:05:28 +09:00
Timothy Jaeryang Baek
9a8969ca93 refac 2026-05-14 13:49:50 +09:00
Timothy Jaeryang Baek
2e1b671e8d refac 2026-05-14 13:49:15 +09:00
Timothy Jaeryang Baek
db2b3d7fd8 refac 2026-05-14 13:46:54 +09:00
Timothy Jaeryang Baek
1004dad274 refac 2026-05-14 13:46:36 +09:00
Timothy Jaeryang Baek
ee3b14233a refac 2026-05-14 13:46:23 +09:00
Timothy Jaeryang Baek
dc0f8ae6f2 refac 2026-05-14 13:46:08 +09:00
Timothy Jaeryang Baek
1b9d22e324 refac 2026-05-14 13:45:59 +09:00
Timothy Jaeryang Baek
98d3b23085 refac 2026-05-14 13:45:39 +09:00
Timothy Jaeryang Baek
6b1df94bf9 refac 2026-05-14 13:45:31 +09:00
Timothy Jaeryang Baek
95840e307a refac 2026-05-14 13:45:21 +09:00
Timothy Jaeryang Baek
6df09a4039 refac 2026-05-14 13:45:12 +09:00
Timothy Jaeryang Baek
459b1c3fda refac 2026-05-14 13:45:05 +09:00
Timothy Jaeryang Baek
bd9f82d5a6 refac 2026-05-14 13:44:50 +09:00
Timothy Jaeryang Baek
e5c8f8110a refac 2026-05-14 13:19:00 +09:00
Timothy Jaeryang Baek
9c14740ffb refac 2026-05-14 13:12:59 +09:00
Timothy Jaeryang Baek
74ae177d79 refac 2026-05-14 13:10:37 +09:00
Timothy Jaeryang Baek
1ea54c3217 refac 2026-05-14 13:10:22 +09:00
Timothy Jaeryang Baek
f0e88dadc8 refac 2026-05-14 13:08:53 +09:00
Classic298
5cc1eb5170
fix: enforce features.direct_tool_servers on chat-completion tool_servers (#24693)
* fix: enforce features.direct_tool_servers on chat-completion tool_servers

The features.direct_tool_servers per-user permission was correctly
enforced on the storage path (routers/users.py user/settings/update,
which strips toolServers from saved settings when the caller lacks the
permission), but the inference path (/api/chat/completions) popped
tool_servers straight from the request body into metadata with no
permission check. The middleware (utils/middleware.py:2799) then
consumed direct_tool_servers to inject system_prompt into the message
array and register external tool specs that get invoked during the
completion. End result: any authenticated user could bypass the
admin-set per-user feature toggle and use inline tool_servers in their
chat-completion requests, even when admin had explicitly denied the
permission.

Default for USER_PERMISSIONS_FEATURES_DIRECT_TOOL_SERVERS is False
(config.py:2750), so under default config no regular user is supposed
to be able to use direct tool servers — making this a real boundary
bypass on out-of-the-box deployments rather than a corner case.

Mirror the storage-side behaviour at the inference entry point: pop
tool_servers from the request body, then silently drop the value if
the caller is non-admin and lacks features.direct_tool_servers. Admins
always pass; users with the explicit grant always pass; everyone else
gets None propagated into metadata, which the middleware already
handles as the no-tool-servers case.

Reported by berkant-koc in GHSA-f582-c373-jjf6.

Co-authored-by: berkant-koc <berkant-koc@users.noreply.github.com>

* chore: trim verbose comment on tool_servers permission check

---------

Co-authored-by: berkant-koc <berkant-koc@users.noreply.github.com>
2026-05-14 13:08:33 +09:00
Timothy Jaeryang Baek
ae06e199d5 refac 2026-05-14 03:25:54 +09:00
Timothy Jaeryang Baek
cc16e06c32 refac 2026-05-14 03:10:48 +09:00
Timothy Jaeryang Baek
81f611fb73 refac 2026-05-14 03:06:37 +09:00
Timothy Jaeryang Baek
245e0ee029 refac 2026-05-14 03:01:48 +09:00
Classic298
ba78ebea27
fix: validate folder_id ownership on chat create + folder-update endpoints (#24588)
POST /api/v1/chats/new and POST /api/v1/chats/{id}/folder accepted a
caller-supplied folder_id with no validation — neither ownership, nor
existence, nor UUID format. The row was persisted with the supplied
value verbatim, so the DB ended up with chat rows whose folder_id
referenced another user's folder, a non-existent UUID, or even a
non-UUID string.

No read path surfaces this across users — every chat-folder read is
user_id-filtered on both sides — so this is referential-integrity
hardening rather than a security boundary fix. But there's no reason
to accept dangling references either, and the downstream consumers
shouldn't have to assume the column is clean.

Add a Folders.get_folder_by_id_and_user_id() lookup at both writers:
if a folder_id is supplied, it must match a folder owned by the
caller. None remains allowed (chat-without-folder is the default).
Non-existent and non-UUID values fall through to 404.

Reported by ShigekiTsuchiyama in GHSA-4vrg-2vcq-q7jc.

Co-authored-by: ShigekiTsuchiyama <ShigekiTsuchiyama@users.noreply.github.com>
2026-05-14 02:57:57 +09:00
Timothy Jaeryang Baek
f607337582 refac 2026-05-14 02:56:44 +09:00
Aleix Dorca
2a7524056a
i18n: Update catalan translation.json (#24569) 2026-05-14 02:49:32 +09:00
Timothy Jaeryang Baek
0214c8db24 fix: korean i18n 2026-05-14 02:41:54 +09:00
Timothy Jaeryang Baek
8aa2a42dc7 refac 2026-05-14 02:40:20 +09:00
Asbjørn Dyhrberg Thegler
f6d8e1e38d
Fix translation for 'Authentication' in Danish (#24645) 2026-05-14 02:36:43 +09:00
Timothy Jaeryang Baek
7dcd932ad7 refac 2026-05-14 02:34:19 +09:00
Daniel Nylander
be301c1c27
i18n: Update Swedish (sv-SE) translation — merge with latest dev (#24665)
Co-authored-by: Daniel Nylander <daniel@danielnylander.se>
2026-05-14 02:30:10 +09:00
Taey
5ab32a5086
fix: German i18n translations (#24668) 2026-05-14 02:29:33 +09:00
Timothy Jaeryang Baek
42c86e7283 refac 2026-05-14 02:25:16 +09:00
Timothy Jaeryang Baek
74f95a9b0d refac 2026-05-14 02:08:35 +09:00
Timothy Jaeryang Baek
0b81520e07 refac 2026-05-14 02:08:01 +09:00
Timothy Jaeryang Baek
4e78b355ef refac 2026-05-14 02:00:23 +09:00
Timothy Jaeryang Baek
d74ee34d91 refac 2026-05-14 01:49:34 +09:00
Timothy Jaeryang Baek
3127f1b462 refac 2026-05-14 01:43:33 +09:00
Timothy Jaeryang Baek
ab0ee858b7 refac 2026-05-14 00:07:55 +09:00
Timothy Jaeryang Baek
32a417bbf6 refac 2026-05-13 22:52:09 +09:00
Timothy Jaeryang Baek
e7d2ddbb1d refac 2026-05-13 22:51:23 +09:00
Timothy Jaeryang Baek
171150c1e1 refac 2026-05-13 22:46:00 +09:00
Timothy Jaeryang Baek
2ad327a4dc refac 2026-05-13 22:44:17 +09:00
Timothy Jaeryang Baek
d6dae36da0 refac 2026-05-13 22:39:51 +09:00
Timothy Jaeryang Baek
c2cbc47ca7 feat: knowledge directory 2026-05-13 22:37:53 +09:00
_00_
8ace73da84
upd:i18n: es-ES Translation update v0.9.5 (#24651)
es-ES Translation. Update v0.9.5

Added translation of new strings.
2026-05-13 21:45:44 +09:00
Timothy Jaeryang Baek
3b00e5721a refac 2026-05-13 16:02:11 +09:00
Timothy Jaeryang Baek
2f642754ac refac 2026-05-13 16:00:56 +09:00
Timothy Jaeryang Baek
9ef579ce4b refac 2026-05-13 15:58:52 +09:00
Timothy Jaeryang Baek
ecec86dd32 refac 2026-05-13 15:55:21 +09:00
Timothy Jaeryang Baek
5b125c24d4 feat: kb_exec 2026-05-13 15:41:30 +09:00
Timothy Jaeryang Baek
bc244fdc90 refac
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: 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
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 / 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 (-ollama, ollama) (push) Blocked by required conditions
Create and publish Docker images with specific build args / copy-to-dockerhub (-slim, slim) (push) Blocked by required conditions
2026-05-13 12:44:12 +09:00
Timothy Jaeryang Baek
6d0295588e refac: modernize type annotations (PEP 604 / PEP 585) 2026-05-12 17:10:15 +09:00
Timothy Jaeryang Baek
a59c967d7e refac: modernize imports, standardize type hints and docstrings 2026-05-12 06:30:38 +09:00
Timothy Jaeryang Baek
998c86a52b style: standardize os.environ.get to os.getenv
Some checks failed
Frontend Build / Format & Build (push) Has been cancelled
Frontend Build / Unit Tests (push) Has been cancelled
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: 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
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 / 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 (-ollama, ollama) (push) Blocked by required conditions
Create and publish Docker images with specific build args / copy-to-dockerhub (-slim, slim) (push) Blocked by required conditions
2026-05-12 06:12:16 +09:00
Timothy Jaeryang Baek
83736e8dbd style: ruff format 2026-05-12 06:12:06 +09:00
Timothy Jaeryang Baek
4cee7c29a5 refac 2026-05-12 06:01:12 +09:00
Timothy Jaeryang Baek
1413ce4a52 refac 2026-05-12 05:30:25 +09:00
Timothy Jaeryang Baek
c79ff81f55 refac: remove dead peewee connection wrappers 2026-05-12 04:14:19 +09:00
Timothy Jaeryang Baek
2c2d06c31b refac: deprecate peewee migration layer
The Alembic init migration (7e5b5dc7342b) already creates the
equivalent schema. Peewee migrations are no longer needed for
any version >= 0.3.6.
2026-05-12 03:45:59 +09:00
Timothy Jaeryang Baek
4527c747b4 chore: remove unused legacy test suite 2026-05-12 03:35:51 +09:00
Timothy Jaeryang Baek
9c18e707ce chore: remove unused cypress tests 2026-05-12 03:31:57 +09:00
Timothy Jaeryang Baek
070ab26501 refac: reorganize scripts and ci workflows 2026-05-12 03:26:18 +09:00
Timothy Jaeryang Baek
90a02e623f refac 2026-05-12 03:04:35 +09:00
Timothy Jaeryang Baek
18ff358c15 refac
Some checks failed
Create and publish Docker images with specific build args / build-main-image (linux/amd64, ubuntu-latest) (push) Has been cancelled
Create and publish Docker images with specific build args / build-main-image (linux/arm64, ubuntu-24.04-arm) (push) Has been cancelled
Create and publish Docker images with specific build args / build-cuda-image (linux/amd64, ubuntu-latest) (push) Has been cancelled
Create and publish Docker images with specific build args / build-cuda-image (linux/arm64, ubuntu-24.04-arm) (push) Has been cancelled
Create and publish Docker images with specific build args / build-cuda126-image (linux/amd64, ubuntu-latest) (push) Has been cancelled
Create and publish Docker images with specific build args / build-cuda126-image (linux/arm64, ubuntu-24.04-arm) (push) Has been cancelled
Create and publish Docker images with specific build args / build-ollama-image (linux/amd64, ubuntu-latest) (push) Has been cancelled
Create and publish Docker images with specific build args / build-ollama-image (linux/arm64, ubuntu-24.04-arm) (push) Has been cancelled
Create and publish Docker images with specific build args / build-slim-image (linux/amd64, ubuntu-latest) (push) Has been cancelled
Create and publish Docker images with specific build args / build-slim-image (linux/arm64, ubuntu-24.04-arm) (push) Has been cancelled
Python CI / Format Backend (push) Has been cancelled
Frontend Build / Format & Build Frontend (push) Has been cancelled
Frontend Build / Frontend Unit Tests (push) Has been cancelled
Create and publish Docker images with specific build args / merge-main-images (push) Has been cancelled
Create and publish Docker images with specific build args / merge-cuda-images (push) Has been cancelled
Create and publish Docker images with specific build args / merge-cuda126-images (push) Has been cancelled
Create and publish Docker images with specific build args / merge-ollama-images (push) Has been cancelled
Create and publish Docker images with specific build args / merge-slim-images (push) Has been cancelled
Create and publish Docker images with specific build args / copy-to-dockerhub (, main) (push) Has been cancelled
Create and publish Docker images with specific build args / copy-to-dockerhub (-cuda, cuda) (push) Has been cancelled
Create and publish Docker images with specific build args / copy-to-dockerhub (-cuda126, cuda126) (push) Has been cancelled
Create and publish Docker images with specific build args / copy-to-dockerhub (-ollama, ollama) (push) Has been cancelled
Create and publish Docker images with specific build args / copy-to-dockerhub (-slim, slim) (push) Has been cancelled
2026-05-11 03:30:12 +09:00
Tim Baek
3660bc00fd
Merge pull request #24492 from open-webui/dev
Some checks failed
Release / release (push) Has been cancelled
Python CI / Format Backend (push) Has been cancelled
Create and publish Docker images with specific build args / build-main-image (linux/amd64, ubuntu-latest) (push) Has been cancelled
Create and publish Docker images with specific build args / build-main-image (linux/arm64, ubuntu-24.04-arm) (push) Has been cancelled
Create and publish Docker images with specific build args / build-cuda-image (linux/amd64, ubuntu-latest) (push) Has been cancelled
Create and publish Docker images with specific build args / build-cuda-image (linux/arm64, ubuntu-24.04-arm) (push) Has been cancelled
Create and publish Docker images with specific build args / build-cuda126-image (linux/amd64, ubuntu-latest) (push) Has been cancelled
Create and publish Docker images with specific build args / build-cuda126-image (linux/arm64, ubuntu-24.04-arm) (push) Has been cancelled
Create and publish Docker images with specific build args / build-ollama-image (linux/amd64, ubuntu-latest) (push) Has been cancelled
Create and publish Docker images with specific build args / build-ollama-image (linux/arm64, ubuntu-24.04-arm) (push) Has been cancelled
Create and publish Docker images with specific build args / build-slim-image (linux/amd64, ubuntu-latest) (push) Has been cancelled
Create and publish Docker images with specific build args / build-slim-image (linux/arm64, ubuntu-24.04-arm) (push) Has been cancelled
Frontend Build / Format & Build Frontend (push) Has been cancelled
Frontend Build / Frontend Unit Tests (push) Has been cancelled
Release to PyPI / release (push) Has been cancelled
Create and publish Docker images with specific build args / merge-main-images (push) Has been cancelled
Create and publish Docker images with specific build args / merge-cuda-images (push) Has been cancelled
Create and publish Docker images with specific build args / merge-cuda126-images (push) Has been cancelled
Create and publish Docker images with specific build args / merge-ollama-images (push) Has been cancelled
Create and publish Docker images with specific build args / merge-slim-images (push) Has been cancelled
Create and publish Docker images with specific build args / copy-to-dockerhub (, main) (push) Has been cancelled
Create and publish Docker images with specific build args / copy-to-dockerhub (-cuda, cuda) (push) Has been cancelled
Create and publish Docker images with specific build args / copy-to-dockerhub (-cuda126, cuda126) (push) Has been cancelled
Create and publish Docker images with specific build args / copy-to-dockerhub (-ollama, ollama) (push) Has been cancelled
Create and publish Docker images with specific build args / copy-to-dockerhub (-slim, slim) (push) Has been cancelled
0.9.5
2026-05-10 14:13:55 -04:00
Timothy Jaeryang Baek
41b48b54aa doc: changelog 2026-05-11 02:54:22 +09:00
Timothy Jaeryang Baek
4856ce48be chore: format 2026-05-11 02:51:59 +09:00
Timothy Jaeryang Baek
0037baeb26 enh: channels streaming agent 2026-05-11 02:50:30 +09:00
Timothy Jaeryang Baek
c951b4f262 chore: format 2026-05-11 02:29:13 +09:00
Timothy Jaeryang Baek
39777e35d8 doc: changelog 2026-05-11 02:28:38 +09:00
Timothy Jaeryang Baek
15e696691c refac 2026-05-11 02:25:11 +09:00
Timothy Jaeryang Baek
3a21b334cc refac 2026-05-11 02:15:46 +09:00
Timothy Jaeryang Baek
2dbf7b6764 refac 2026-05-11 02:12:38 +09:00
Timothy Jaeryang Baek
4a1064cefd refac 2026-05-11 02:04:09 +09:00
Timothy Jaeryang Baek
315566064a refac 2026-05-11 01:57:50 +09:00
Timothy Jaeryang Baek
3bba1c2270 feat: add IFRAME_CSP env var for srcdoc iframe content security policy
Adds an IFRAME_CSP environment variable that injects a Content-Security-Policy
<meta> tag into all srcdoc iframes rendering untrusted content:
- Artifacts (LLM-generated HTML previews)
- FullHeightIframe (tool/embed output)
- FilePreview (user-uploaded HTML files)
- CitationModal (RAG document HTML)

Shared utility in src/lib/utils/csp.ts handles injection with HTML-safe
attribute escaping. URL-based iframes (src=) are correctly excluded.

Env-var only — no PersistentConfig, no admin UI, no DB. Set once at deploy
time, requires restart. Empty string (default) means no CSP restriction.
2026-05-11 01:56:02 +09:00
Timothy Jaeryang Baek
1388f4568b refac 2026-05-11 01:46:33 +09:00
Timothy Jaeryang Baek
d1ef538237 refac 2026-05-11 01:31:46 +09:00
Classic298
fc94118b2d
fix: prevent mass-assignment user_id spoofing in POST /api/v1/evaluations/feedback (#24508)
* fix: prevent mass-assignment user_id spoofing in POST /api/v1/evaluations/feedback

Two independent gaps in backend/open_webui/models/feedbacks.py let an
authenticated caller forge the `user_id` (and `id`, `version`) on a new
feedback record submitted to POST /api/v1/evaluations/feedback:

1. `FeedbackForm` declared `model_config = ConfigDict(extra='allow')`,
   so Pydantic preserved any extra fields supplied in the request body —
   including `user_id`, `id`, `version`. The form is the public input
   boundary for the endpoint and should not accept unknown fields.

2. In `insert_new_feedback`, the dict literal placed
   `**form_data.model_dump()` AFTER `'id': id`, `'user_id': user_id`,
   `'version': 0`. Python dict-literal duplicate-key resolution is
   last-wins, so any of those fields present in `form_data` overwrote
   the server-derived values.

Combined effect: a regular user could POST a feedback record with an
arbitrary `user_id`, attributing the rating to any other user. The Elo
leaderboard at backend/open_webui/routers/evaluations.py computes model
rankings from these records, and the admin export
(GET /api/v1/evaluations/feedbacks/export) and admin list
(GET /api/v1/evaluations/feedbacks/all) display the spoofed attribution.

Two fixes, defense-in-depth:

- FeedbackForm: switch `extra='allow'` to `extra='ignore'` so Pydantic
  drops unknown fields at parse time. Sub-models (RatingData / MetaData /
  SnapshotData) intentionally keep `extra='allow'` because their contents
  are deliberately schema-flexible — the spoofing surface was the form,
  not the sub-payloads.

- insert_new_feedback: spread `form_data.model_dump()` first, then
  overlay server-controlled fields (`id`, `user_id`, `version`,
  `created_at`, `updated_at`) so the explicit keys win on duplicate-key
  resolution regardless of what reaches the function. Matches the secure
  pattern already used in backend/open_webui/models/functions.py:120.

Reported by yantongggg in GHSA-rjmp-vjf2-qf4g. Same root-cause class as
the prior published GHSA-hr43-rjmr-7wmm (folder mass-assignment, fixed
in v0.9.0); that fix did not generalize across the codebase, this fix
closes the feedback variant.

Co-authored-by: yantongggg <yantongggg@users.noreply.github.com>

* chore: trim comments

---------

Co-authored-by: yantongggg <yantongggg@users.noreply.github.com>
2026-05-11 01:16:17 +09:00
Timothy Jaeryang Baek
8d3133fe28 refac 2026-05-11 01:15:34 +09:00
Classic298
d11e06f1b7
fix: prevent redirect-based SSRF and enforce collecton write access (#24524)
* fix: prevent redirect-based SSRF in get_image_base64_from_url

Cohort follow-up to PR #24491. That PR patched three call sites
(SafeWebBaseLoader._scrape, get_content_from_url, load_url_image) to
pass allow_redirects=False on the underlying HTTP client; this fourth
call site in utils/files.py was missed.

get_image_base64_from_url() is invoked from convert_url_images_to_base64
in utils/middleware.py on every /api/chat/completions request whose
message content includes an image_url part. validate_url() is called on
the originally-submitted URL only; the aiohttp session.get() call had
no allow_redirects argument and the shared session pool does not
override the aiohttp default (allow_redirects=True). An authenticated
user sending a chat message with image_url pointing at an attacker host
that 302-redirects to 169.254.169.254 / 127.0.0.1 / RFC1918 reached the
internal target. This is the most reachable variant in the redirect
cluster: no special endpoint, no admin permission, no feature flag.

Apply the same one-line fix as the other three call sites: pass
allow_redirects=AIOHTTP_CLIENT_ALLOW_REDIRECTS (defaults to False).

Reported by nayakchinmohan in GHSA-88jq-grjp-jx6f; consolidated under
GHSA-rh5x-h6pp-cjj6.

Co-authored-by: nayakchinmohan <nayakchinmohan@users.noreply.github.com>

* fix: enforce collection write access on process_file endpoint

Cohort follow-up to ba83613ff. That commit added _validate_collection_access
to process_text and process_web (the user-supplied collection_name path)
but missed process_file in the same router.

process_file accepts a user-supplied collection_name and writes the file's
embedded content into that collection via save_docs_to_vector_db. The
file_id is gated by file ownership (line 1562) but collection_name was
unchecked, so an authenticated user could append content from a file they
own into another user's knowledge-base collection by passing the victim's
KB UUID as collection_name. Identical pattern to the process_text and
process_web gaps that ba83613ff closed.

Apply the same one-line gate as the sibling endpoints: when
collection_name is user-supplied (not the default file-{file.id} fallback),
require write access via _validate_collection_access. The shared validator
delegates to filter_accessible_collections, which already correctly
handles file-* prefixes (via has_access_to_file) and KB UUIDs
(via Knowledges.check_access_by_user_id) — admins bypass.

Reported by tenbbughunters (Tenable) in GHSA-4g37-7p2c-38r9 (the
comprehensive write-path filing covering process_text / process_file /
process_web / process_youtube and the _validate_collection_access UUID
root cause), and independently re-identified for the missed process_file
call site by kodareef5 in GHSA-4m74-3cmc-293g.

Co-authored-by: tenbbughunters <tenbbughunters@users.noreply.github.com>
Co-authored-by: kodareef5 <kodareef5@users.noreply.github.com>

* fix: enforce collection write access on process_files_batch endpoint

Cohort follow-up to ba83613ff and the prior process_file fix on this
branch. process_files_batch (line 2604) is the third write endpoint in
the same router that accepts a user-supplied collection_name; it was
covered in the same Tenable filing as process_file and was missed by
the same cohort fix. The endpoint validates per-file ownership at line
2642 but does not check whether the caller has write access to the
target collection_name before save_docs_to_vector_db writes into it
at line 2683-2690 with add=True.

Apply the same one-line gate as the sibling endpoints. Validate only
when collection_name is user-supplied (truthy) so the existing fall
through behavior for the None case is unchanged.

Same Tenable / kodareef5 cohort as the previous commit.

Co-authored-by: tenbbughunters <tenbbughunters@users.noreply.github.com>
Co-authored-by: kodareef5 <kodareef5@users.noreply.github.com>

---------

Co-authored-by: nayakchinmohan <nayakchinmohan@users.noreply.github.com>
Co-authored-by: tenbbughunters <tenbbughunters@users.noreply.github.com>
Co-authored-by: kodareef5 <kodareef5@users.noreply.github.com>
2026-05-11 01:09:15 +09:00
Classic298
841c9045d7
fix: gate tool content updates behind workspace.tools to match create endpoint (#24513)
* fix: gate tool content updates behind workspace.tools to match create endpoint

`update_tools_by_id` (routers/tools.py:452) authorizes a caller as long as
they are the tool's owner, hold a `write` access grant on the tool, or are
an admin. This means a verified user who has been given a write grant on
a tool — typically as part of a metadata-collaboration workflow (edit
description, adjust valves, manage access grants) — can also overwrite
the tool's Python source. Because `load_tool_module_by_id` further down
calls `exec(content, module.__dict__)` at module-import time, anything
the new content puts outside the `class Tools:` body executes immediately
on the server with the worker's privileges (root in the default Docker
deployment).

The `create_new_tools` endpoint already requires
`workspace.tools` (or `workspace.tools_import`) precisely because creating
a tool means submitting executable code. The update endpoint did not
mirror that check, producing an asymmetric authorization surface in which
a write-grantee with no workspace permission can still reach the same
exec sink as a workspace.tools-trusted creator. SECURITY.md frames
`workspace.tools` as the trust signal an admin uses to delegate
code-execution capability; the previous behavior let that signal be
bypassed by a per-resource share.

Fix: after the existing ownership / write-grant / admin gate, add a
content-change check. If `form_data.content != tools.content`, require
`workspace.tools` or `workspace.tools_import` (or admin role). Metadata
edits — `name`, `description`, valves config, access grants — continue
to flow through the existing gate, so the legitimate share-for-
collaboration workflow is unaffected.

Reported by KadirArslan in GHSA-p4fx-23fq-jfg6 with a working three-user
PoC (Alice trusted with workspace.tools creates a tool and shares write
to Bob; Bob updates content and the new code runs as root inside the
container, with Burp Collaborator confirming outbound exfiltration).

Co-authored-by: KadirArslan <KadirArslan@users.noreply.github.com>

* chore: trim comment

---------

Co-authored-by: KadirArslan <KadirArslan@users.noreply.github.com>
2026-05-11 01:08:12 +09:00
Classic298
f5e110fbee
fix: enforce message ownership in group/DM channel update + delete endpoints (#24506)
* fix: enforce message ownership in group/DM channel update + delete endpoints

`update_message_by_id` (channels.py:1348) and `delete_message_by_id`
(channels.py:1550) branch on `channel.type`. The `else` branch (standard
channels) correctly enforces `message.user_id != user.id` ownership before
mutating, but the `if channel.type in ['group', 'dm']` branch only checked
`is_user_channel_member` — channel membership alone, with no message
ownership verification.

Effect on group/DM channels: any verified member of the conversation could:

- overwrite another member's message content while the server preserved
  `user_id=victim`, producing tampered content that renders to other
  members as the original author's authentic post (integrity + authenticity);
- silently delete another member's messages, removing them from
  conversation history without trace (integrity).

Reproduced end-to-end against v0.9.4 with three users (attacker, victim,
viewer) sharing a group channel: attacker overwrites victim's message and
deletes another, viewer reads the tampered content as victim-authored.

Two patches, identical shape, mirror the `else` branch's existing
ownership semantics:

- `update_message_by_id` group/DM branch: add
  `if user.role != 'admin' and message.user_id != user.id: raise 403`
  immediately after the `is_user_channel_member` check.
- `delete_message_by_id` group/DM branch: same.

The standard-channel branch is unchanged (it already enforced ownership).
Admins remain able to moderate any message, matching the existing semantic
in the standard-channel branch.

Reports consolidated under GHSA-wwhq-cx22-f7vv (earliest live filing of the
group/DM-specific variant). Same gap previously surfaced and partially
fixed under GHSA-jxwr-g6r6-j3fx (which addressed the standard-channel
branch only) — this completes the cohort.

* chore: trim comments
2026-05-11 01:03:39 +09:00
Timothy Jaeryang Baek
e8e9141061 refac 2026-05-11 01:02:45 +09:00
Classic298
d3737176bc
fix: require write permission for pin_channel_message on standard channels (#24521)
`pin_channel_message` (channels.py:1242) checked `permission='read'` on
the standard-channel branch before mutating `is_pinned` / `pinned_by` /
`pinned_at` via `Messages.update_is_pinned_by_id`. Pin/unpin is a write
operation; gating it on read access let any user with read-only channel
access pin or unpin any message in the channel, including admin posts.

One-character fix: change `permission='read'` to `permission='write'`.

Reported by kikayli in GHSA-5gc6-xhv4-2wg6.

Co-authored-by: kikayli <kikayli@users.noreply.github.com>
2026-05-11 00:59:50 +09:00
Classic298
2d9939ed49
chore: add validate_url() to get_image_data() for cohort consistency hardening (#24518)
* chore: add validate_url() to get_image_data() for cohort consistency hardening

`get_image_data()` in `backend/open_webui/routers/images.py` fetches the
URL returned by the configured image generation API directly via
`session.get(data)` without first calling `validate_url()`. The sibling
`load_url_image()` in the same file (called from /images/edit) calls
`validate_url(data)` first — that gate was added under
GHSA-jgx9-jr5x-mvpv. The two functions handle structurally identical
input (an attacker-or-server-supplied URL string) and should enforce the
same SSRF gate as a matter of code hygiene.

In the current call graph, the URL passed to `get_image_data()` comes
from the admin-configured image generation API's response, so an
exploitable SSRF chain additionally requires admin-side trust delegation
(misconfigured/untrusted upstream image API, or a custom
OpenAI-compatible server that reflects user input into response URLs).
That makes the missing call a defense-in-depth gap rather than a
vulnerability per SECURITY.md Rule 9 — a position the GHSA-h7cc-wwjp-5xqh
advisory is being closed under. This change is hardening: it brings the
two image-fetch helpers into alignment so any future caller that begins
passing user-influenced URLs into `get_image_data()` is gated by the
same private-IP / loopback / metadata-IP filter the rest of the
codebase enforces.

Surface raised by brodmart in GHSA-h7cc-wwjp-5xqh.

Co-authored-by: brodmart <brodmart@users.noreply.github.com>

* chore: trim comment

---------

Co-authored-by: brodmart <brodmart@users.noreply.github.com>
2026-05-11 00:59:32 +09:00
Classic298
c66c273f62
fix: strip model params for read-only callers on per-id endpoint (#24525)
GET /api/v1/models/model?id=<model_id> at routers/models.py:412
returned the full model.model_dump() to any caller with read access,
including the params dict that holds the admin-curated system prompt
and other behavior config. The user-facing /api/models endpoint
already strips this via utils/models.py:170,210 with the comment
"Remove params to avoid exposing sensitive info", and /api/v1/models/list
gates by write permission so non-curators don't see the model in their
workspace listing at all. The per-id endpoint missed the same gate, so
a user with read-only access (e.g. granted access to use the model in
chat) could open /workspace/models/edit?id=<not-mine> in the browser
and read the system prompt verbatim from the network response, even
though saving was correctly blocked.

Compute write_access once at the top of the handler so it can serve
both the response-shape decision and the response field. When the
caller lacks write access, replace params with an empty dict in the
serialised response. Owners, admins under BYPASS_ADMIN_ACCESS_CONTROL,
and explicit write-grant holders still get the full payload so the
workspace edit UI keeps working for users who legitimately curate the
model.

Read-permission users continue to receive everything else they need to
chat with the model — the chat path resolves prompt/params server-side
from the stored ModelModel and never echoes them back through this
endpoint.

Reported by destination-one in GHSA-h2cw-7qw9-56xr.

Co-authored-by: destination-one <destination-one@users.noreply.github.com>
2026-05-11 00:59:08 +09:00
Classic298
e7ba8978c6
fix: reject parser-confusing chars in validate_url to close SSRF bypass (#24534)
urllib.parse.urlparse and requests/aiohttp disagree on how to split URLs
containing backslash, tab, CR, or LF in or around the netloc. urlparse
treats backslash as part of userinfo and uses what follows '@' as the
host; requests treats backslash as the start of the path and connects
to whatever precedes it. The same URL therefore passes the private-IP
filter (urlparse sees a public host) but reaches an internal target
(requests connects to e.g. 127.0.0.1). End result is an SSRF that the
existing IP block list cannot catch because it's evaluating the wrong
host.

PoC: http://127.0.0.1:6666\@1.1.1.1 — urlparse hostname is 1.1.1.1
(global, passes), requests reaches 127.0.0.1 (loopback).

Reject up front any URL containing one of the four documented parser-
confusing characters before either parser gets a chance to interpret
it. None of these characters is valid in an unencoded URL (\ should
always be %5C, whitespace should be %09 / %0A / %0D), so this is a
pure defensive rejection with no legitimate-input false positives.

Reported by Fushuling and RacerZ-fighting in GHSA-8w7q-q5jp-jvgx.

Co-authored-by: Fushuling <Fushuling@users.noreply.github.com>
Co-authored-by: RacerZ-fighting <RacerZ-fighting@users.noreply.github.com>
2026-05-11 00:57:48 +09:00
joaoback
5b13e3e3f0
i18n: add pt-BR translations for newly added UI items and consistency pass (#24503)
New **pt-BR** translations for items introduced in the latest releases, plus a consistency/quality pass across existing strings (grammar, tone, capitalization, pluralization). Placeholders and hotkeys preserved. No logic changes.
2026-05-11 00:57:18 +09:00
Timothy Jaeryang Baek
b0a56375d2 chore
Some checks are pending
Create and publish Docker images with specific build args / build-main-image (linux/amd64, ubuntu-latest) (push) Waiting to run
Create and publish Docker images with specific build args / build-main-image (linux/arm64, ubuntu-24.04-arm) (push) Waiting to run
Create and publish Docker images with specific build args / build-cuda-image (linux/amd64, ubuntu-latest) (push) Waiting to run
Create and publish Docker images with specific build args / build-cuda-image (linux/arm64, ubuntu-24.04-arm) (push) Waiting to run
Create and publish Docker images with specific build args / build-cuda126-image (linux/amd64, ubuntu-latest) (push) Waiting to run
Create and publish Docker images with specific build args / build-cuda126-image (linux/arm64, ubuntu-24.04-arm) (push) Waiting to run
Create and publish Docker images with specific build args / build-ollama-image (linux/amd64, ubuntu-latest) (push) Waiting to run
Create and publish Docker images with specific build args / build-ollama-image (linux/arm64, ubuntu-24.04-arm) (push) Waiting to run
Create and publish Docker images with specific build args / build-slim-image (linux/amd64, ubuntu-latest) (push) Waiting to run
Create and publish Docker images with specific build args / build-slim-image (linux/arm64, ubuntu-24.04-arm) (push) Waiting to run
Create and publish Docker images with specific build args / merge-main-images (push) Blocked by required conditions
Create and publish Docker images with specific build args / merge-cuda-images (push) Blocked by required conditions
Create and publish Docker images with specific build args / merge-cuda126-images (push) Blocked by required conditions
Create and publish Docker images with specific build args / merge-ollama-images (push) Blocked by required conditions
Create and publish Docker images with specific build args / merge-slim-images (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 (-ollama, ollama) (push) Blocked by required conditions
Create and publish Docker images with specific build args / copy-to-dockerhub (-slim, slim) (push) Blocked by required conditions
Python CI / Format Backend (push) Waiting to run
Frontend Build / Frontend Unit Tests (push) Waiting to run
Frontend Build / Format & Build Frontend (push) Waiting to run
2026-05-09 23:38:32 +09:00
Timothy Jaeryang Baek
8689f7090f chore: format 2026-05-09 23:22:05 +09:00
Timothy Jaeryang Baek
55535a8965 doc: changelog 2026-05-09 23:21:08 +09:00
Classic298
203ec29baf
chore: remove unauthenticated dead-code GET /api/v1/retrieval/ status endpoint (#24497)
The `get_status()` handler at retrieval.py:263 (`@router.get('/')`) returned
the live RAG pipeline configuration (CHUNK_SIZE, CHUNK_OVERLAP, RAG_TEMPLATE,
RAG_EMBEDDING_ENGINE, RAG_EMBEDDING_MODEL, RAG_RERANKING_MODEL, etc.) without
any authentication dependency, while every adjacent endpoint on the same
router (/embedding, /embedding/update, /config, /config/update) requires
get_admin_user.

Exhaustive search of the repository confirms the endpoint has no callers:

- Frontend (src/): no `RETRIEVAL_API_BASE_URL}/'`-style fetch; the existing
  `getRAGConfig()` in src/lib/apis/retrieval/index.ts targets `/config`,
  not the root, and is the only consumer of admin-level retrieval state.
- Backend self-references: none.
- Cypress e2e (chat, documents, registration, settings): none.
- Backend tests (backend/open_webui/test/): none.
- Build/CI scripts (scripts/): none.
- Direct symbol import of `get_status` from this router: none.

The endpoint is dead code, almost certainly a relic from before the
/config GET split. Removing it has zero UX impact and eliminates the
unauthenticated-config-disclosure surface raised in advisory triage on
GHSA-65pg-qhhw-mxwg. External monitoring scripts that may have hit the
bare root will receive a 404 and can switch to the existing /config
endpoint, which returns the same fields plus the rest of the RAG config
under admin auth.

Surface raised by 0xRyuzak1 in GHSA-65pg-qhhw-mxwg. The advisory was closed
as not-a-vulnerability per SECURITY.md Rule 1 (no security boundary
crossed in default config — RAG_TEMPLATE default is a citation-format
instruction, not a system prompt; no integrity/availability impact); this
removal is independent code-hygiene that aligns the router cohort.

Reported-by: 0xRyuzak1 <https://github.com/0xRyuzak1>
2026-05-09 23:19:14 +09:00
Classic298
9918ab6265
fix: gate public sharing of skills behind sharing.public_skills on create/update (#24494)
The /create (L155-193) and /id/{id}/update (L248-297) endpoints in
routers/skills.py persisted form_data.access_grants directly to
AccessGrants.set_access_grants without filter_allowed_access_grants,
while every other shareable resource in the codebase (channels, knowledge,
models, notes, prompts, tools, calendars) and the dedicated
/id/{id}/access/update endpoint on this same router (L309-348) all do call
the filter. A user with workspace.skills permission (default False, but
admins can grant it to skill-creating users) could therefore attach
{"principal_type":"user","principal_id":"*","permission":"read"|"write"}
to the create or update payload and have it persisted unfiltered, bypassing
the sharing.public_skills gate that the rest of the cohort enforces.

Two changes:

- create_new_skill: call filter_allowed_access_grants with
  'sharing.public_skills' immediately before insert, after the existing
  permission check and ID-taken check.
- update_skill_by_id: call filter_allowed_access_grants with the same key
  after the access check, before form_data.model_dump() flows into
  Skills.update_skill_by_id. The pre-existing access check at L263-277 only
  restricts WHO may modify the skill; the new filter restricts WHICH grants
  they may set.

All supporting plumbing was already in place from prior PRs:
filter_allowed_access_grants is already imported at L22, the
USER_PERMISSIONS_WORKSPACE_SKILLS_ALLOW_PUBLIC_SHARING constant exists,
DEFAULT_USER_PERMISSIONS['sharing']['public_skills'] is wired up,
SharingPermissions.public_skills is in the Pydantic, and the admin UI
already renders the toggle. This is a pure 2-line router fix that closes
the cohort-consistency gap.

Same shape as the calendar fix in #24493, reported by Matteo Panzeri while
auditing the resource-cohort cohort during follow-up on #24493.

Co-authored-by: Matteo Panzeri <28739806+matte1782@users.noreply.github.com>
2026-05-09 23:19:03 +09:00
Classic298
8a0018cf96
fix: gate public sharing of calendars behind sharing.public_calendars permission (#24493)
* fix: gate public sharing of calendars behind sharing.public_calendars permission

The calendar router did not call filter_allowed_access_grants on either the
create or update endpoint, while every other shareable resource in the
codebase (channels, knowledge, models, notes, prompts, skills, tools) does.
A verified non-admin owner could therefore attach
`{"principal_type":"user","principal_id":"*","permission":"read"|"write"}`
to their own calendar in the create or update payload and have it persisted
unfiltered. Any other verified user with the (default-on) features.calendar
permission could then read or, for write grants, write events on it via the
existing /events* endpoints, bypassing the per-user sharing.public_<X>
permission gate the rest of the resource cohort enforces.

Three changes:

- config.py: add USER_PERMISSIONS_CALENDAR_ALLOW_PUBLIC_SHARING (default
  False, env-overridable) and surface it in DEFAULT_USER_PERMISSIONS
  ['sharing']['public_calendars'] so admins can grant it per group via the
  same UI used for notes/models/etc.
- routers/calendar.py: import filter_allowed_access_grants and call it in
  create_calendar with the new sharing.public_calendars key, identical to
  the channel router's pattern.
- routers/calendar.py: call filter_allowed_access_grants in update_calendar
  too. The pre-existing owner-only gate at L350 only restricts WHO may
  change grants; the new filter restricts WHICH grants they may set, so a
  non-admin owner cannot make their own calendar publicly readable or
  writable without the corresponding sharing permission.

Same shape as GHSA-7rjh-px4v-5w55 (channels). Reported by Matteo Panzeri.

Co-authored-by: Matteo Panzeri <28739806+matte1782@users.noreply.github.com>

* fix: expose public_calendars + features.calendar through admin permissions surface

The earlier commit added DEFAULT_USER_PERMISSIONS['sharing']['public_calendars']
and the runtime filter call, but the new key was not yet plumbed through the
admin /users/default/permissions endpoint. Without these changes the toggle
would round-trip as silently dropped:

- routers/users.py SharingPermissions: any payload POSTed to
  /default/permissions ran through `form_data.model_dump()`, and Pydantic
  drops fields not declared on the model. The new public_calendars key
  would have been stripped on every save, leaving admins unable to grant
  the permission via the UI even though the runtime filter would honor it.
- src/lib/constants/permissions.ts: the frontend's DEFAULT_PERMISSIONS dict
  is the seed shape used by the admin Groups Permissions panel; without
  the new key it could not bind a Switch component to it.
- Permissions.svelte: add a Calendars Public Sharing toggle alongside the
  Notes/Chats Public Sharing toggles, gated on the existing
  features.calendar flag (matches the pattern used for notes/chats).

Also closes a pre-existing parity gap on features.calendar: DEFAULT_USER_
PERMISSIONS['features']['calendar'] has existed since the calendar feature
shipped, and Permissions.svelte already renders a Calendar feature toggle,
but FeaturesPermissions Pydantic and the frontend defaults never knew
about it. Adding it everywhere completes the round-trip so admin saves no
longer silently drop the calendar feature flag either.

---------

Co-authored-by: Matteo Panzeri <28739806+matte1782@users.noreply.github.com>
2026-05-09 23:18:51 +09:00
Timothy Jaeryang Baek
69270e1c9e doc: changelog 2026-05-09 21:08:07 +09:00
Timothy Jaeryang Baek
2e71b3fbb8 chore: format 2026-05-09 21:07:08 +09:00
Timothy Jaeryang Baek
df42d96c95 refac 2026-05-09 21:05:49 +09:00
Timothy Jaeryang Baek
2fa3b84241 chore: bump 2026-05-09 21:04:52 +09:00
Classic298
8854541508
fix: prevent redirect-based SSRF in web-fetch and image-load call sites (#24491)
validate_url() in retrieval/web/utils.py only validates the initial URL.
The HTTP clients used downstream (sync requests, sync requests via the
parent WebBaseLoader._scrape, aiohttp via load_url_image) followed 3xx
redirects by default and did not re-validate the redirect target against
the private-IP / metadata-IP block list. An authenticated user could
submit a public URL that 302-redirected to an internal address (RFC1918,
127.0.0.1, 169.254.169.254, etc.) and the redirected response was returned
to them, enabling SSRF reads of internal services and cloud metadata.

Three call sites needed allow_redirects=False to match the policy already
enforced on the async _fetch() path:

- SafeWebBaseLoader: override requests_kwargs in __init__ so that the
  inherited synchronous _scrape() path passes allow_redirects=False to
  self.session.get() (the parent WebBaseLoader uses requests' default
  allow_redirects=True).
- get_content_from_url (retrieval/utils.py): pass allow_redirects=False
  on the streamed requests.get(...) call.
- load_url_image (routers/images.py, image-edits endpoint): pass
  allow_redirects=False on the aiohttp session.get(...) call.

Reports consolidated under GHSA-rh5x-h6pp-cjj6:
- GHSA-rh5x-h6pp-cjj6 (tenbbughunters / Tenable) - sync _scrape
- GHSA-5vxg-6gmv-m2qr (YLChen-007) - load_url_image
- GHSA-hf76-c83f-63w2 (tempcollab) - aiohttp _fetch (already fixed)
- GHSA-h55f-h5fh-mvm4 (sneaXOR) - get_content_from_url
2026-05-09 21:01:45 +09:00
Timothy Jaeryang Baek
793e628ac3 refac 2026-05-09 20:59:29 +09:00
Classic298
a0268e51fc
Merge pull request #24486 from Classic298/fix/notes-is-pinned-typeerror
fix: notes is_pinned TypeError on create/get
2026-05-09 20:56:06 +09:00
Tim Baek
f51d2b026f
Merge pull request #24483 from open-webui/dev
Some checks are pending
Release / release (push) Waiting to run
Create and publish Docker images with specific build args / build-main-image (linux/amd64, ubuntu-latest) (push) Waiting to run
Create and publish Docker images with specific build args / build-main-image (linux/arm64, ubuntu-24.04-arm) (push) Waiting to run
Create and publish Docker images with specific build args / build-cuda-image (linux/amd64, ubuntu-latest) (push) Waiting to run
Create and publish Docker images with specific build args / build-ollama-image (linux/arm64, ubuntu-24.04-arm) (push) Waiting to run
Create and publish Docker images with specific build args / build-cuda-image (linux/arm64, ubuntu-24.04-arm) (push) Waiting to run
Create and publish Docker images with specific build args / build-cuda126-image (linux/amd64, ubuntu-latest) (push) Waiting to run
Create and publish Docker images with specific build args / build-cuda126-image (linux/arm64, ubuntu-24.04-arm) (push) Waiting to run
Create and publish Docker images with specific build args / build-ollama-image (linux/amd64, ubuntu-latest) (push) Waiting to run
Create and publish Docker images with specific build args / build-slim-image (linux/amd64, ubuntu-latest) (push) Waiting to run
Create and publish Docker images with specific build args / build-slim-image (linux/arm64, ubuntu-24.04-arm) (push) Waiting to run
Create and publish Docker images with specific build args / merge-main-images (push) Blocked by required conditions
Create and publish Docker images with specific build args / merge-cuda-images (push) Blocked by required conditions
Create and publish Docker images with specific build args / merge-cuda126-images (push) Blocked by required conditions
Create and publish Docker images with specific build args / merge-ollama-images (push) Blocked by required conditions
Create and publish Docker images with specific build args / merge-slim-images (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 (-ollama, ollama) (push) Blocked by required conditions
Create and publish Docker images with specific build args / copy-to-dockerhub (-slim, slim) (push) Blocked by required conditions
Python CI / Format Backend (push) Waiting to run
Frontend Build / Frontend Unit Tests (push) Waiting to run
Frontend Build / Format & Build Frontend (push) Waiting to run
Release to PyPI / release (push) Waiting to run
0.9.4
2026-05-09 03:50:05 -04:00
Timothy Jaeryang Baek
0f07af1bb8 fix: bump to 0.9.4, changelog for scroll fix 2026-05-09 16:41:01 +09:00
Timothy Jaeryang Baek
7d3efb8513 refac 2026-05-09 16:37:19 +09:00
941 changed files with 215697 additions and 109529 deletions

View file

@ -13,10 +13,31 @@ OPENAI_API_KEY=''
# CORS_ALLOW_ORIGIN='http://localhost:5173;http://localhost:8080'
CORS_ALLOW_ORIGIN='*'
# Set to false to keep memory tools enabled without adding memory context to the system context.
ENABLE_MEMORY_SYSTEM_CONTEXT=true
# Set to true to add compact row/column stats to parsed CSV retrieval context.
ENABLE_RAG_CSV_SUMMARY=false
# Set to true to preserve backing file records, storage blobs, and per-file vectors when files are removed from knowledge bases.
ENABLE_KNOWLEDGE_FILE_RETENTION=false
# Set to false to disable workspace Tools and Functions.
ENABLE_PLUGINS=true
# For production you should set this to match the proxy configuration (127.0.0.1)
FORWARDED_ALLOW_IPS='*'
# DO NOT TRACK
SCARF_NO_ANALYTICS=true
DO_NOT_TRACK=true
ANONYMIZED_TELEMETRY=false
ANONYMIZED_TELEMETRY=false
# Valkey Vector Store (requires VECTOR_DB=valkey)
# VALKEY_URL='valkey://localhost:6379'
# VALKEY_COLLECTION_PREFIX='open_webui'
# VALKEY_INDEX_TYPE='HNSW'
# VALKEY_DISTANCE_METRIC='COSINE'
# VALKEY_HNSW_M='16'
# VALKEY_HNSW_EF_CONSTRUCTION='200'
# VALKEY_HNSW_EF_RUNTIME='10'

2
.github/FUNDING.yml vendored
View file

@ -1 +1 @@
github: tjbck
github: open-webui

View file

@ -2,7 +2,6 @@ name: Bug Report
description: Create a detailed bug report to help us improve Open WebUI.
title: 'issue: '
labels: ['bug', 'triage']
assignees: []
body:
- type: markdown
attributes:
@ -35,6 +34,8 @@ body:
required: true
- label: I have also searched in the CLOSED issues AND CLOSED discussions and found no related items (your issue might already be addressed on the development branch!).
required: true
- label: I have checked whether this issue is already fixed on the `dev` branch or in the latest source.
required: true
- label: I am using the latest version of Open WebUI.
required: true
@ -55,7 +56,7 @@ body:
id: open-webui-version
attributes:
label: Open WebUI Version
description: Specify the version (e.g., v0.6.26)
description: Specify the version (e.g., v0.11.0)
validations:
required: true
@ -63,7 +64,7 @@ body:
id: ollama-version
attributes:
label: Ollama Version (if applicable)
description: Specify the version (e.g., v0.2.0, or v0.1.32-rc1)
description: Specify the version (e.g., v0.32.5, or v0.32.6-rc0)
validations:
required: false
@ -71,7 +72,7 @@ body:
id: operating-system
attributes:
label: Operating System
description: Specify the OS (e.g., Windows 10, macOS Sonoma, Ubuntu 22.04, Debian 12)
description: Specify the OS (e.g., Windows 11, macOS Tahoe, Ubuntu 26.04, Debian 13)
validations:
required: true
@ -79,7 +80,7 @@ body:
id: browser
attributes:
label: Browser (if applicable)
description: Specify the browser/version (e.g., Chrome 100.0, Firefox 98.0)
description: Specify the browser/version (e.g., Chrome 151.0, Firefox 153.0.3)
validations:
required: false
@ -138,11 +139,11 @@ body:
placeholder: |
Example (include every detail):
1. Start with a clean Ubuntu 22.04 install.
2. Install Docker v24.0.5 and start the service.
1. Start with a clean Ubuntu 26.04 install.
2. Install Docker v29.7.1 and start the service.
3. Clone the Open WebUI repo (git clone ...).
4. Use the Docker Compose file without modifications.
5. Open browser Chrome 115.0 in incognito mode.
5. Open browser Chrome 151.0 in incognito mode.
6. Go to http://localhost:8080 and log in with user "test@example.com".
7. Set the language to "English" and theme to "Dark".
8. Attempt to connect to Ollama at "http://localhost:11434".

View file

@ -1 +1,5 @@
blank_issues_enabled: false
contact_links:
- name: 🔒 Report a Security Vulnerability
url: https://github.com/open-webui/open-webui/security
about: Do NOT open a public issue for security vulnerabilities, suspected vulnerabilities, or any security-related concern. Please review our Security Policy and report privately via the "Report a vulnerability" button so it can be handled as a private advisory.

View file

@ -1,82 +1,78 @@
name: Feature Request
description: Suggest an idea for this project
description: Suggest a new feature or improvement
title: 'feat: '
labels: ['triage']
body:
- type: markdown
attributes:
value: |
## Important Notes
### Before submitting
Please check the **open AND closed** [Issues](https://github.com/open-webui/open-webui/issues) AND [Discussions](https://github.com/open-webui/open-webui/discussions) to see if a similar request has been posted.
It's likely we're already tracking it! If youre unsure, start a discussion post first.
## Before Submitting
#### Scope
Please check **open AND closed** [Issues](https://github.com/open-webui/open-webui/issues) and [Discussions](https://github.com/open-webui/open-webui/discussions) for similar requests. If you find one, add your input there instead.
If your feature request is likely to take more than a quick coding session to implement, test and verify, then open it in the **Ideas** section of the [Discussions](https://github.com/open-webui/open-webui/discussions) instead.
**We will close and force move your feature request to the Ideas section, if we believe your feature request is not trivial/quick to implement.**
This is to ensure the issues tab is used only for issues, quickly addressable feature requests and tracking tickets by the maintainers.
Other feature requests belong in the **Ideas** section of the [Discussions](https://github.com/open-webui/open-webui/discussions).
If your feature request might impact others in the community, definitely open a discussion instead and evaluate whether and how to implement it.
This will help us efficiently focus on improving the project.
### Collaborate respectfully
We value a **constructive attitude**, so please be mindful of your communication. If negativity is part of your approach, our capacity to engage may be limited. We're here to help if you're **open to learning** and **communicating positively**.
### Scope Guidelines
Remember:
- Open WebUI is a **volunteer-driven project**
- It's managed by a **single maintainer**
- It's supported by contributors who also have **full-time jobs**
Feature requests that require significant implementation effort should be posted in the **Ideas** section of [Discussions](https://github.com/open-webui/open-webui/discussions) instead. We will move oversized feature requests to Discussions to keep the Issues tab focused on actionable items.
We appreciate your time and ask that you **respect ours**.
If your request might impact the broader community, please open a Discussion first so others can weigh in on the design.
### Be Respectful
Open WebUI is a volunteer-driven project maintained by a small team. We value constructive, positive communication. Please be mindful of maintainers' time and energy.
### Contributing
If you encounter an issue, we highly encourage you to submit a pull request or fork the project. We actively work to prevent contributor burnout to maintain the quality and continuity of Open WebUI.
### Bug reproducibility
If a bug cannot be reproduced with a `:main` or `:dev` Docker setup, or a `pip install` with Python 3.11, it may require additional help from the community. In such cases, we will move it to the "[issues](https://github.com/open-webui/open-webui/discussions/categories/issues)" Discussions section due to our limited resources. We encourage the community to assist with these issues. Remember, its not that the issue doesnt exist; we need your help!
If you encounter an issue, we encourage you to submit a pull request or fork the project. We actively work to prevent contributor burnout and maintain project quality.
### Reproducibility
If a bug cannot be reproduced with a `:main` or `:dev` Docker setup, or a `pip install` with Python 3.11, it may be moved to the "Issues" section in Discussions for community assistance.
- type: checkboxes
id: existing-issue
attributes:
label: Check Existing Issues
description: Please confirm that you've checked for existing similar requests
description: Confirm you have searched for similar requests.
options:
- label: I have searched for all existing **open AND closed** issues and discussions for similar requests. I have found none that is comparable to my request.
- label: I have searched all existing **open AND closed** issues and discussions and found none comparable to my request.
required: true
- label: I have checked whether this request is already implemented on the `dev` branch or in the latest source.
required: true
- type: checkboxes
id: feature-scope
attributes:
label: Verify Feature Scope
description: Please confirm the feature's scope is within the described scope
description: Confirm this request belongs in Issues rather than Discussions.
options:
- label: I have read through and understood the scope definition for feature requests in the Issues section. I believe my feature request meets the definition and belongs in the Issues section instead of the Discussions.
- label: I believe this feature request is appropriately scoped for the Issues section as described above.
required: true
- type: textarea
id: problem-description
attributes:
label: Problem Description
description: Is your feature request related to a problem? Please provide a clear and concise description of what the problem is.
placeholder: "Ex. I'm always frustrated when... / Not related to a problem"
description: Is this related to a problem? Describe the pain point clearly.
placeholder: "e.g., I'm frustrated when..."
validations:
required: true
- type: textarea
id: solution-description
attributes:
label: Desired Solution you'd like
description: Clearly describe what you want to happen.
label: Proposed Solution
description: Describe what you would like to happen.
validations:
required: true
- type: textarea
id: alternatives-considered
attributes:
label: Alternatives Considered
description: A clear and concise description of any alternative solutions or features you've considered.
description: Describe any alternative solutions or workarounds you have considered.
- type: textarea
id: additional-context
attributes:
label: Additional Context
description: Add any other context or screenshots about the feature request here.
description: Add any other context, mockups, or screenshots about the feature request.

View file

@ -4,17 +4,22 @@ updates:
directory: '/'
schedule:
interval: monthly
target-branch: 'dev'
target-branch: dev
- package-ecosystem: pip
directory: '/backend'
schedule:
interval: monthly
target-branch: 'dev'
target-branch: dev
- package-ecosystem: 'github-actions'
- package-ecosystem: github-actions
directory: '/'
schedule:
# Check for updates to GitHub Actions every week
interval: monthly
target-branch: 'dev'
target-branch: dev
- package-ecosystem: npm
directory: '/'
schedule:
interval: monthly
target-branch: dev

View file

@ -1,92 +1,114 @@
<!--
⚠️ CRITICAL CHECKS FOR CONTRIBUTORS (READ, DON'T DELETE) ⚠️
1. Target the `dev` branch. PRs targeting `main` will be automatically closed.
2. Do NOT delete the CLA section at the bottom. It is required for the bot to accept your PR.
2. First-time contributors should not open pull requests directly unless the pull request contains only i18n/localization updates.
Do not open a PR as the first step.
For real, reproducible bugs, start with a well-described Issue that explains the problem, why it matters, and what outcome you are looking for.
For feature requests, enhancements, behavior changes, UI/UX changes, architecture changes, suspected fixes, or unconfirmed approaches, start with an active Discussion.
If you want to propose an implementation, include it only as a reference in the Issue or Discussion, such as a local diff, patch, or branch.
Opening an Issue or Discussion does not mean a PR is the right next step. Maintainers will confirm when a PR would be useful.
We ask for this because PRs, especially from first-time contributors, often need broader maintainer context on product direction, scope, architecture, UX, edge cases, compatibility, documentation, and long-term maintenance before implementation.
We may close unsolicited PRs without review.
Contributors with a history of successful merged PRs may be given more latitude.
3. Do NOT delete the CLA section at the bottom. It is required for the bot to accept your PR.
-->
# Pull Request Checklist
### Note to first-time contributors: Please open a discussion post in [Discussions](https://github.com/open-webui/open-webui/discussions) to discuss your idea/fix with the community before creating a pull request, and describe your changes before submitting a pull request.
### Do not open a pull request as the first step.
This is to ensure large feature PRs are discussed with the community first, before starting work on it. If the community does not want this feature or it is not relevant for Open WebUI as a project, it can be identified in the discussion before working on the feature and submitting the PR.
For real, reproducible bugs, start with a well-described [Issue](https://github.com/open-webui/open-webui/issues) that explains the problem, why it matters, and what outcome you are looking for.
For feature requests, enhancements, behavior changes, UI/UX changes, architecture changes, suspected fixes, or unconfirmed approaches, start with an active [Discussion](https://github.com/open-webui/open-webui/discussions). Merely opening a discussion is not enough; it needs to be actively discussed.
If you want to propose an implementation, include it only as a reference in the Issue or Discussion, such as a local diff, patch, or branch.
Opening an Issue or Discussion does not mean a PR is the right next step. Maintainers will confirm when a PR would be useful.
We ask for this because PRs, especially from first-time contributors, often need broader maintainer context on product direction, scope, architecture, UX, edge cases, compatibility, documentation, and long-term maintenance before implementation.
Unsolicited PRs may be closed without review. Contributors with a history of successful merged PRs may be given more latitude.
<!--
### ⚠️ Important: Your PR is a contribution, not a guarantee of merge.
We appreciate thoughtful contributions. Pull requests are for implementation-ready changes that have already been requested, confirmed, or actively discussed in a linked Issue or Discussion. Feature ideas, behavior changes, UI/UX changes, architecture changes, suspected fixes, and unconfirmed approaches should start as an Issue or Discussion instead.
Before opening a PR, make sure the change has a clear linked problem, follows nearby patterns, has been manually tested, and accounts for related or downstream behavior. PRs that are ideas, prototypes, unresolved design questions, unchecked AI-generated code, symptom-only patches, one-off patches, or changes where affected paths have not been checked will usually be closed.
The most impactful way to contribute to Open WebUI is through well-written bug reports, detailed feature discussions, and thoughtful ideas. These directly shape the project. If you do open a pull request, please know that Open WebUI is held to the highest standard of code quality, consistency, and architectural coherence, and every line merged becomes something the core team must own, maintain, and support indefinitely. Submitted code may be refactored, rewritten, or used as inspiration for a different implementation. This is not a reflection of your work's quality. It is how we ensure that a small team can deeply understand and evolve every part of the codebase.
-->
**Before submitting, make sure you've checked the following:**
**Before submitting, make sure you've checked and filled out the following:**
- [ ] **Target branch:** Verify that the pull request targets the `dev` branch. **PRs targeting `main` will be immediately closed.**
- [ ] **Description:** Provide a concise description of the changes made in this pull request down below.
- [ ] **Changelog:** Ensure a changelog entry following the format of [Keep a Changelog](https://keepachangelog.com/) is added at the bottom of the PR description.
- [ ] **Documentation:** Add docs in [Open WebUI Docs Repository](https://github.com/open-webui/docs). Document user-facing behavior, environment variables, public APIs/interfaces, or deployment steps.
- [ ] **Dependencies:** Are there any new or upgraded dependencies? If so, explain why, update the changelog/docs, and include any compatibility notes. Actually run the code/function that uses updated library to ensure it doesn't crash.
- [ ] **Testing:** Perform manual tests to **verify the implemented fix/feature works as intended AND does not break any other functionality**. Include reproducible steps to demonstrate the issue before the fix. Test edge cases (URL encoding, HTML entities, types). Take this as an opportunity to **make screenshots of the feature/fix and include them in the PR description**.
- [ ] **Agentic AI Code:** Confirm this Pull Request is **not written by any AI Agent** or has at least **gone through additional human review AND manual testing**. If any AI Agent is the co-author of this PR, it may lead to immediate closure of the PR.
- [ ] **Code review:** Have you performed a self-review of your code, addressing any coding standard issues and ensuring adherence to the project's coding standards?
- [ ] **Design & Architecture:** Prefer smart defaults over adding new settings; use local state for ephemeral UI logic. Open a Discussion for major architectural or UX changes.
- [ ] **Git Hygiene:** Keep PRs atomic (one logical change). Clean up commits and rebase on `dev` to ensure no unrelated commits (e.g. from `main`) are included. Push updates to the existing PR branch instead of closing and reopening.
- [ ] **Title Prefix:** To clearly categorize this pull request, prefix the pull request title using one of the following:
- **BREAKING CHANGE**: Significant changes that may affect compatibility
- **build**: Changes that affect the build system or external dependencies
- **ci**: Changes to our continuous integration processes or workflows
- **chore**: Refactor, cleanup, or other non-functional code changes
- **docs**: Documentation update or addition
- **feat**: Introduces a new feature or enhancement to the codebase
- **fix**: Bug fix or error correction
- [ ] **Linked Issue/Discussion:** This PR references an existing, well-described [Issue](https://github.com/open-webui/open-webui/issues) for a real bug or an active, substantive [Discussion](https://github.com/open-webui/open-webui/discussions) for a feature request or enhancement — `Closes #___` / `Relates to #___`.
- [ ] **First-time contributor policy:** This is not my first contribution to Open WebUI, this PR contains only i18n/localization updates, or a maintainer explicitly asked me to open this PR after reviewing the linked Issue or Discussion.
- [ ] **Target branch:** The pull request targets the `dev` branch. **PRs targeting `main` will be immediately closed.**
- [ ] **Description:** A concise description of the changes is provided below.
- [ ] **Changelog:** A changelog entry following [Keep a Changelog](https://keepachangelog.com/) format is included at the bottom.
- [ ] **Documentation:** Relevant documentation has been added or updated in the [Open WebUI Docs Repository](https://github.com/open-webui/docs).
- [ ] **Dependencies:** Any new or updated dependencies are explained, tested, and documented.
- [ ] **Testing:** **Manual** end-to-end tests have been performed to verify the fix/feature works correctly and does not introduce regressions. Screenshots or recordings are included where applicable.
- [ ] **User-facing changes:** I have confirmed whether this PR changes the UI. If it does, screenshots are required, and a video recording is recommended.
- [ ] **No Unchecked AI Code:** This PR is either human-written or has undergone thorough human review AND manual testing. Unreviewed AI-generated PRs may be closed immediately.
- [ ] **Self-Review:** A self-review of the code has been performed, ensuring adherence to project coding standards.
- [ ] **Architecture:** Smart defaults are preferred over new settings. Local state is used for ephemeral UI logic. Major architectural or UX changes have been discussed first.
- [ ] **Git Hygiene:** The PR is atomic (one logical change), rebased on `dev`, and contains no unrelated commits.
- [ ] **Title Prefix:** The PR title uses one of the following prefixes:
- **BREAKING CHANGE**: Changes affecting backward compatibility
- **build**: Build system or dependency changes
- **ci**: CI/CD workflow changes
- **chore**: Refactoring, cleanup, or non-functional changes
- **docs**: Documentation additions or updates
- **feat**: New features or enhancements
- **fix**: Bug fixes or corrections
- **i18n**: Internationalization or localization changes
- **perf**: Performance improvement
- **refactor**: Code restructuring for better maintainability, readability, or scalability
- **style**: Changes that do not affect the meaning of the code (white space, formatting, missing semi-colons, etc.)
- **test**: Adding missing tests or correcting existing tests
- **WIP**: Work in progress, a temporary label for incomplete or ongoing work
- **perf**: Performance improvements
- **refactor**: Code restructuring
# Changelog Entry
### Description
- [Concisely describe the changes made in this pull request, including any relevant motivation and impact (e.g., fixing a bug, adding a feature, or improving performance)]
- [Describe the changes, including motivation and impact]
### Added
- [List any new features, functionalities, or additions]
- [New features, functionalities, or additions]
### Changed
- [List any changes, updates, refactorings, or optimizations]
- [Changes, updates, refactorings, or optimizations]
### Deprecated
- [List any deprecated functionality or features that have been removed]
- [Deprecated functionality or features]
### Removed
- [List any removed features, files, or functionalities]
- [Removed features, files, or functionalities]
### Fixed
- [List any fixes, corrections, or bug fixes]
- [Bug fixes or corrections]
### Security
- [List any new or updated security-related changes, including vulnerability fixes]
- [Security-related changes or vulnerability fixes]
### Breaking Changes
- **BREAKING CHANGE**: [List any breaking changes affecting compatibility or functionality]
- **BREAKING CHANGE**: [Changes affecting compatibility or functionality]
---
### Additional Information
- [Insert any additional context, notes, or explanations for the changes]
- [Reference any related issues, commits, or other relevant information]
- [Any additional context, notes, or references to related issues/commits]
### Screenshots or Videos
- [Attach any relevant screenshots or videos demonstrating the changes]
- [Attach screenshots or videos for user-facing changes. For UI changes, screenshots are required, and a video recording is recommended.]
### Contributor License Agreement

43
.github/workflows/backend.yaml vendored Normal file
View file

@ -0,0 +1,43 @@
# ─────────────────────────────────────────────────────────────────────────────
# Backend CI — Python formatting checks via Ruff
# Runs on pushes and PRs to main/dev when backend files change
# ─────────────────────────────────────────────────────────────────────────────
name: Python CI
on:
push:
branches: [main, dev]
paths: ['backend/**', 'pyproject.toml', 'uv.lock', '.github/workflows/backend.yaml']
pull_request:
branches: [main, dev]
paths: ['backend/**', 'pyproject.toml', 'uv.lock', '.github/workflows/backend.yaml']
concurrency:
group: backend-${{ github.ref }}
cancel-in-progress: true
jobs:
# ── Ruff format check across supported Python versions ───────────────────
format-check:
name: Ruff Format (${{ matrix.python-version }})
runs-on: ubuntu-latest
timeout-minutes: 10
strategy:
fail-fast: false
matrix:
python-version: ['3.11', '3.12']
steps:
- uses: actions/checkout@v5
- uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python-version }}
- name: Install formatter
run: pip install "ruff>=0.15.5"
- name: Verify formatting
run: ruff format --check . --exclude .venv --exclude venv
- name: Detect logic errors
run: ruff check --select=F --ignore=F401,F403,F405,F541,F811,F841 --output-format=github .

View file

@ -1,61 +0,0 @@
name: Release
on:
push:
branches:
- main # or whatever branch you want to use
jobs:
release:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v5
- name: Check for changes in package.json
run: |
git diff --cached --diff-filter=d package.json || {
echo "No changes to package.json"
exit 1
}
- name: Get version number from package.json
id: get_version
run: |
VERSION=$(jq -r '.version' package.json)
echo "::set-output name=version::$VERSION"
- name: Extract latest CHANGELOG entry
run: |
VERSION="${{ steps.get_version.outputs.version }}"
awk "/^## \[${VERSION}\]/{found=1; next} /^## \[/{if(found) exit} found{print}" CHANGELOG.md > /tmp/release-notes.md
- name: Create GitHub release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh release create "v${{ steps.get_version.outputs.version }}" \
--title "v${{ steps.get_version.outputs.version }}" \
--notes-file /tmp/release-notes.md
- name: Upload package to GitHub release
uses: actions/upload-artifact@v4
with:
name: package
path: |
.
!.git
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Trigger Docker build workflow
uses: actions/github-script@v8
with:
script: |
github.rest.actions.createWorkflowDispatch({
owner: context.repo.owner,
repo: context.repo.repo,
workflow_id: 'docker-build.yaml',
ref: 'v${{ steps.get_version.outputs.version }}',
})

View file

@ -1,917 +0,0 @@
name: Create and publish Docker images with specific build args
on:
workflow_dispatch:
push:
branches:
- main
- dev
tags:
- v*
env:
REGISTRY: ghcr.io
jobs:
build-main-image:
runs-on: ${{ matrix.runner }}
permissions:
contents: read
packages: write
strategy:
fail-fast: false
matrix:
include:
- platform: linux/amd64
runner: ubuntu-latest
- platform: linux/arm64
runner: ubuntu-24.04-arm
steps:
# GitHub Packages requires the entire repository name to be in lowercase
# although the repository owner has a lowercase username, this prevents some people from running actions after forking
- name: Set repository and image name to lowercase
run: |
echo "IMAGE_NAME=${IMAGE_NAME,,}" >>${GITHUB_ENV}
echo "FULL_IMAGE_NAME=ghcr.io/${IMAGE_NAME,,}" >>${GITHUB_ENV}
env:
IMAGE_NAME: '${{ github.repository }}'
- name: Prepare
run: |
platform=${{ matrix.platform }}
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
- name: Checkout repository
uses: actions/checkout@v5
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to the Container registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata for Docker images (default latest tag)
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.FULL_IMAGE_NAME }}
tags: |
type=ref,event=branch
type=ref,event=tag
type=sha,prefix=git-
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
flavor: |
latest=${{ github.ref == 'refs/heads/main' }}
- name: Extract metadata for Docker cache
id: cache-meta
uses: docker/metadata-action@v5
with:
images: ${{ env.FULL_IMAGE_NAME }}
tags: |
type=ref,event=branch
${{ github.ref_type == 'tag' && 'type=raw,value=main' || '' }}
flavor: |
prefix=cache-${{ matrix.platform }}-
latest=false
- name: Build Docker image (latest)
uses: docker/build-push-action@v5
id: build
with:
context: .
push: true
platforms: ${{ matrix.platform }}
labels: ${{ steps.meta.outputs.labels }}
outputs: type=image,name=${{ env.FULL_IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true
cache-from: type=registry,ref=${{ steps.cache-meta.outputs.tags }}
cache-to: type=registry,ref=${{ steps.cache-meta.outputs.tags }},mode=max
sbom: true
build-args: |
BUILD_HASH=${{ github.sha }}
- name: Export digest
run: |
mkdir -p /tmp/digests
digest="${{ steps.build.outputs.digest }}"
touch "/tmp/digests/${digest#sha256:}"
- name: Upload digest
uses: actions/upload-artifact@v4
with:
name: digests-main-${{ env.PLATFORM_PAIR }}
path: /tmp/digests/*
if-no-files-found: error
retention-days: 1
build-cuda-image:
runs-on: ${{ matrix.runner }}
permissions:
contents: read
packages: write
strategy:
fail-fast: false
matrix:
include:
- platform: linux/amd64
runner: ubuntu-latest
- platform: linux/arm64
runner: ubuntu-24.04-arm
steps:
# GitHub Packages requires the entire repository name to be in lowercase
# although the repository owner has a lowercase username, this prevents some people from running actions after forking
- name: Set repository and image name to lowercase
run: |
echo "IMAGE_NAME=${IMAGE_NAME,,}" >>${GITHUB_ENV}
echo "FULL_IMAGE_NAME=ghcr.io/${IMAGE_NAME,,}" >>${GITHUB_ENV}
env:
IMAGE_NAME: '${{ github.repository }}'
- name: Prepare
run: |
platform=${{ matrix.platform }}
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
- name: Delete huge unnecessary tools folder
run: rm -rf /opt/hostedtoolcache
- name: Checkout repository
uses: actions/checkout@v5
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to the Container registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata for Docker images (cuda tag)
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.FULL_IMAGE_NAME }}
tags: |
type=ref,event=branch
type=ref,event=tag
type=sha,prefix=git-
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=raw,enable=${{ github.ref == 'refs/heads/main' }},prefix=,suffix=,value=cuda
flavor: |
latest=${{ github.ref == 'refs/heads/main' }}
suffix=-cuda,onlatest=true
- name: Extract metadata for Docker cache
id: cache-meta
uses: docker/metadata-action@v5
with:
images: ${{ env.FULL_IMAGE_NAME }}
tags: |
type=ref,event=branch
${{ github.ref_type == 'tag' && 'type=raw,value=main' || '' }}
flavor: |
prefix=cache-cuda-${{ matrix.platform }}-
latest=false
- name: Build Docker image (cuda)
uses: docker/build-push-action@v5
id: build
with:
context: .
push: true
platforms: ${{ matrix.platform }}
labels: ${{ steps.meta.outputs.labels }}
outputs: type=image,name=${{ env.FULL_IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true
cache-from: type=registry,ref=${{ steps.cache-meta.outputs.tags }}
cache-to: type=registry,ref=${{ steps.cache-meta.outputs.tags }},mode=max
sbom: true
build-args: |
BUILD_HASH=${{ github.sha }}
USE_CUDA=true
- name: Export digest
run: |
mkdir -p /tmp/digests
digest="${{ steps.build.outputs.digest }}"
touch "/tmp/digests/${digest#sha256:}"
- name: Upload digest
uses: actions/upload-artifact@v4
with:
name: digests-cuda-${{ env.PLATFORM_PAIR }}
path: /tmp/digests/*
if-no-files-found: error
retention-days: 1
build-cuda126-image:
runs-on: ${{ matrix.runner }}
permissions:
contents: read
packages: write
strategy:
fail-fast: false
matrix:
include:
- platform: linux/amd64
runner: ubuntu-latest
- platform: linux/arm64
runner: ubuntu-24.04-arm
steps:
# GitHub Packages requires the entire repository name to be in lowercase
# although the repository owner has a lowercase username, this prevents some people from running actions after forking
- name: Set repository and image name to lowercase
run: |
echo "IMAGE_NAME=${IMAGE_NAME,,}" >>${GITHUB_ENV}
echo "FULL_IMAGE_NAME=ghcr.io/${IMAGE_NAME,,}" >>${GITHUB_ENV}
env:
IMAGE_NAME: '${{ github.repository }}'
- name: Prepare
run: |
platform=${{ matrix.platform }}
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
- name: Delete huge unnecessary tools folder
run: rm -rf /opt/hostedtoolcache
- name: Checkout repository
uses: actions/checkout@v5
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to the Container registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata for Docker images (cuda126 tag)
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.FULL_IMAGE_NAME }}
tags: |
type=ref,event=branch
type=ref,event=tag
type=sha,prefix=git-
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=raw,enable=${{ github.ref == 'refs/heads/main' }},prefix=,suffix=,value=cuda126
flavor: |
latest=${{ github.ref == 'refs/heads/main' }}
suffix=-cuda126,onlatest=true
- name: Extract metadata for Docker cache
id: cache-meta
uses: docker/metadata-action@v5
with:
images: ${{ env.FULL_IMAGE_NAME }}
tags: |
type=ref,event=branch
${{ github.ref_type == 'tag' && 'type=raw,value=main' || '' }}
flavor: |
prefix=cache-cuda126-${{ matrix.platform }}-
latest=false
- name: Build Docker image (cuda126)
uses: docker/build-push-action@v5
id: build
with:
context: .
push: true
platforms: ${{ matrix.platform }}
labels: ${{ steps.meta.outputs.labels }}
outputs: type=image,name=${{ env.FULL_IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true
cache-from: type=registry,ref=${{ steps.cache-meta.outputs.tags }}
cache-to: type=registry,ref=${{ steps.cache-meta.outputs.tags }},mode=max
sbom: true
build-args: |
BUILD_HASH=${{ github.sha }}
USE_CUDA=true
USE_CUDA_VER=cu126
- name: Export digest
run: |
mkdir -p /tmp/digests
digest="${{ steps.build.outputs.digest }}"
touch "/tmp/digests/${digest#sha256:}"
- name: Upload digest
uses: actions/upload-artifact@v4
with:
name: digests-cuda126-${{ env.PLATFORM_PAIR }}
path: /tmp/digests/*
if-no-files-found: error
retention-days: 1
build-ollama-image:
runs-on: ${{ matrix.runner }}
permissions:
contents: read
packages: write
strategy:
fail-fast: false
matrix:
include:
- platform: linux/amd64
runner: ubuntu-latest
- platform: linux/arm64
runner: ubuntu-24.04-arm
steps:
# GitHub Packages requires the entire repository name to be in lowercase
# although the repository owner has a lowercase username, this prevents some people from running actions after forking
- name: Set repository and image name to lowercase
run: |
echo "IMAGE_NAME=${IMAGE_NAME,,}" >>${GITHUB_ENV}
echo "FULL_IMAGE_NAME=ghcr.io/${IMAGE_NAME,,}" >>${GITHUB_ENV}
env:
IMAGE_NAME: '${{ github.repository }}'
- name: Prepare
run: |
platform=${{ matrix.platform }}
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
- name: Checkout repository
uses: actions/checkout@v5
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to the Container registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata for Docker images (ollama tag)
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.FULL_IMAGE_NAME }}
tags: |
type=ref,event=branch
type=ref,event=tag
type=sha,prefix=git-
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=raw,enable=${{ github.ref == 'refs/heads/main' }},prefix=,suffix=,value=ollama
flavor: |
latest=${{ github.ref == 'refs/heads/main' }}
suffix=-ollama,onlatest=true
- name: Extract metadata for Docker cache
id: cache-meta
uses: docker/metadata-action@v5
with:
images: ${{ env.FULL_IMAGE_NAME }}
tags: |
type=ref,event=branch
${{ github.ref_type == 'tag' && 'type=raw,value=main' || '' }}
flavor: |
prefix=cache-ollama-${{ matrix.platform }}-
latest=false
- name: Build Docker image (ollama)
uses: docker/build-push-action@v5
id: build
with:
context: .
push: true
platforms: ${{ matrix.platform }}
labels: ${{ steps.meta.outputs.labels }}
outputs: type=image,name=${{ env.FULL_IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true
cache-from: type=registry,ref=${{ steps.cache-meta.outputs.tags }}
cache-to: type=registry,ref=${{ steps.cache-meta.outputs.tags }},mode=max
sbom: true
build-args: |
BUILD_HASH=${{ github.sha }}
USE_OLLAMA=true
- name: Export digest
run: |
mkdir -p /tmp/digests
digest="${{ steps.build.outputs.digest }}"
touch "/tmp/digests/${digest#sha256:}"
- name: Upload digest
uses: actions/upload-artifact@v4
with:
name: digests-ollama-${{ env.PLATFORM_PAIR }}
path: /tmp/digests/*
if-no-files-found: error
retention-days: 1
build-slim-image:
runs-on: ${{ matrix.runner }}
permissions:
contents: read
packages: write
strategy:
fail-fast: false
matrix:
include:
- platform: linux/amd64
runner: ubuntu-latest
- platform: linux/arm64
runner: ubuntu-24.04-arm
steps:
# GitHub Packages requires the entire repository name to be in lowercase
# although the repository owner has a lowercase username, this prevents some people from running actions after forking
- name: Set repository and image name to lowercase
run: |
echo "IMAGE_NAME=${IMAGE_NAME,,}" >>${GITHUB_ENV}
echo "FULL_IMAGE_NAME=ghcr.io/${IMAGE_NAME,,}" >>${GITHUB_ENV}
env:
IMAGE_NAME: '${{ github.repository }}'
- name: Prepare
run: |
platform=${{ matrix.platform }}
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
- name: Checkout repository
uses: actions/checkout@v5
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to the Container registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata for Docker images (slim tag)
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.FULL_IMAGE_NAME }}
tags: |
type=ref,event=branch
type=ref,event=tag
type=sha,prefix=git-
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=raw,enable=${{ github.ref == 'refs/heads/main' }},prefix=,suffix=,value=slim
flavor: |
latest=${{ github.ref == 'refs/heads/main' }}
suffix=-slim,onlatest=true
- name: Extract metadata for Docker cache
id: cache-meta
uses: docker/metadata-action@v5
with:
images: ${{ env.FULL_IMAGE_NAME }}
tags: |
type=ref,event=branch
${{ github.ref_type == 'tag' && 'type=raw,value=main' || '' }}
flavor: |
prefix=cache-slim-${{ matrix.platform }}-
latest=false
- name: Build Docker image (slim)
uses: docker/build-push-action@v5
id: build
with:
context: .
push: true
platforms: ${{ matrix.platform }}
labels: ${{ steps.meta.outputs.labels }}
outputs: type=image,name=${{ env.FULL_IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true
cache-from: type=registry,ref=${{ steps.cache-meta.outputs.tags }}
cache-to: type=registry,ref=${{ steps.cache-meta.outputs.tags }},mode=max
sbom: true
build-args: |
BUILD_HASH=${{ github.sha }}
USE_SLIM=true
- name: Export digest
run: |
mkdir -p /tmp/digests
digest="${{ steps.build.outputs.digest }}"
touch "/tmp/digests/${digest#sha256:}"
- name: Upload digest
uses: actions/upload-artifact@v4
with:
name: digests-slim-${{ env.PLATFORM_PAIR }}
path: /tmp/digests/*
if-no-files-found: error
retention-days: 1
merge-main-images:
runs-on: ubuntu-latest
needs: [build-main-image]
steps:
# GitHub Packages requires the entire repository name to be in lowercase
# although the repository owner has a lowercase username, this prevents some people from running actions after forking
- name: Set repository and image name to lowercase
run: |
echo "IMAGE_NAME=${IMAGE_NAME,,}" >>${GITHUB_ENV}
echo "FULL_IMAGE_NAME=ghcr.io/${IMAGE_NAME,,}" >>${GITHUB_ENV}
env:
IMAGE_NAME: '${{ github.repository }}'
- name: Download digests
uses: actions/download-artifact@v5
with:
pattern: digests-main-*
path: /tmp/digests
merge-multiple: true
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to the Container registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata for Docker images (default latest tag)
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.FULL_IMAGE_NAME }}
tags: |
type=ref,event=branch
type=ref,event=tag
type=sha,prefix=git-
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
flavor: |
latest=${{ github.ref == 'refs/heads/main' }}
- name: Create manifest list and push
working-directory: /tmp/digests
run: |
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
$(printf '${{ env.FULL_IMAGE_NAME }}@sha256:%s ' *)
- name: Inspect image
run: |
docker buildx imagetools inspect ${{ env.FULL_IMAGE_NAME }}:${{ steps.meta.outputs.version }}
merge-cuda-images:
runs-on: ubuntu-latest
needs: [build-cuda-image]
steps:
# GitHub Packages requires the entire repository name to be in lowercase
# although the repository owner has a lowercase username, this prevents some people from running actions after forking
- name: Set repository and image name to lowercase
run: |
echo "IMAGE_NAME=${IMAGE_NAME,,}" >>${GITHUB_ENV}
echo "FULL_IMAGE_NAME=ghcr.io/${IMAGE_NAME,,}" >>${GITHUB_ENV}
env:
IMAGE_NAME: '${{ github.repository }}'
- name: Download digests
uses: actions/download-artifact@v5
with:
pattern: digests-cuda-*
path: /tmp/digests
merge-multiple: true
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to the Container registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata for Docker images (default latest tag)
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.FULL_IMAGE_NAME }}
tags: |
type=ref,event=branch
type=ref,event=tag
type=sha,prefix=git-
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=raw,enable=${{ github.ref == 'refs/heads/main' }},prefix=,suffix=,value=cuda
flavor: |
latest=${{ github.ref == 'refs/heads/main' }}
suffix=-cuda,onlatest=true
- name: Create manifest list and push
working-directory: /tmp/digests
run: |
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
$(printf '${{ env.FULL_IMAGE_NAME }}@sha256:%s ' *)
- name: Inspect image
run: |
docker buildx imagetools inspect ${{ env.FULL_IMAGE_NAME }}:${{ steps.meta.outputs.version }}
merge-cuda126-images:
runs-on: ubuntu-latest
needs: [build-cuda126-image]
steps:
# GitHub Packages requires the entire repository name to be in lowercase
# although the repository owner has a lowercase username, this prevents some people from running actions after forking
- name: Set repository and image name to lowercase
run: |
echo "IMAGE_NAME=${IMAGE_NAME,,}" >>${GITHUB_ENV}
echo "FULL_IMAGE_NAME=ghcr.io/${IMAGE_NAME,,}" >>${GITHUB_ENV}
env:
IMAGE_NAME: '${{ github.repository }}'
- name: Download digests
uses: actions/download-artifact@v5
with:
pattern: digests-cuda126-*
path: /tmp/digests
merge-multiple: true
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to the Container registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata for Docker images (default latest tag)
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.FULL_IMAGE_NAME }}
tags: |
type=ref,event=branch
type=ref,event=tag
type=sha,prefix=git-
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=raw,enable=${{ github.ref == 'refs/heads/main' }},prefix=,suffix=,value=cuda126
flavor: |
latest=${{ github.ref == 'refs/heads/main' }}
suffix=-cuda126,onlatest=true
- name: Create manifest list and push
working-directory: /tmp/digests
run: |
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
$(printf '${{ env.FULL_IMAGE_NAME }}@sha256:%s ' *)
- name: Inspect image
run: |
docker buildx imagetools inspect ${{ env.FULL_IMAGE_NAME }}:${{ steps.meta.outputs.version }}
merge-ollama-images:
runs-on: ubuntu-latest
needs: [build-ollama-image]
steps:
# GitHub Packages requires the entire repository name to be in lowercase
# although the repository owner has a lowercase username, this prevents some people from running actions after forking
- name: Set repository and image name to lowercase
run: |
echo "IMAGE_NAME=${IMAGE_NAME,,}" >>${GITHUB_ENV}
echo "FULL_IMAGE_NAME=ghcr.io/${IMAGE_NAME,,}" >>${GITHUB_ENV}
env:
IMAGE_NAME: '${{ github.repository }}'
- name: Download digests
uses: actions/download-artifact@v5
with:
pattern: digests-ollama-*
path: /tmp/digests
merge-multiple: true
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to the Container registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata for Docker images (default ollama tag)
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.FULL_IMAGE_NAME }}
tags: |
type=ref,event=branch
type=ref,event=tag
type=sha,prefix=git-
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=raw,enable=${{ github.ref == 'refs/heads/main' }},prefix=,suffix=,value=ollama
flavor: |
latest=${{ github.ref == 'refs/heads/main' }}
suffix=-ollama,onlatest=true
- name: Create manifest list and push
working-directory: /tmp/digests
run: |
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
$(printf '${{ env.FULL_IMAGE_NAME }}@sha256:%s ' *)
- name: Inspect image
run: |
docker buildx imagetools inspect ${{ env.FULL_IMAGE_NAME }}:${{ steps.meta.outputs.version }}
merge-slim-images:
runs-on: ubuntu-latest
needs: [build-slim-image]
steps:
# GitHub Packages requires the entire repository name to be in lowercase
# although the repository owner has a lowercase username, this prevents some people from running actions after forking
- name: Set repository and image name to lowercase
run: |
echo "IMAGE_NAME=${IMAGE_NAME,,}" >>${GITHUB_ENV}
echo "FULL_IMAGE_NAME=ghcr.io/${IMAGE_NAME,,}" >>${GITHUB_ENV}
env:
IMAGE_NAME: '${{ github.repository }}'
- name: Download digests
uses: actions/download-artifact@v5
with:
pattern: digests-slim-*
path: /tmp/digests
merge-multiple: true
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to the Container registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata for Docker images (default slim tag)
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.FULL_IMAGE_NAME }}
tags: |
type=ref,event=branch
type=ref,event=tag
type=sha,prefix=git-
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=raw,enable=${{ github.ref == 'refs/heads/main' }},prefix=,suffix=,value=slim
flavor: |
latest=${{ github.ref == 'refs/heads/main' }}
suffix=-slim,onlatest=true
- name: Create manifest list and push
working-directory: /tmp/digests
run: |
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
$(printf '${{ env.FULL_IMAGE_NAME }}@sha256:%s ' *)
- name: Inspect image
run: |
docker buildx imagetools inspect ${{ env.FULL_IMAGE_NAME }}:${{ steps.meta.outputs.version }}
# Copy images from GHCR to Docker Hub (best-effort, won't block GHCR)
copy-to-dockerhub:
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v')
needs: [merge-main-images, merge-cuda-images, merge-cuda126-images, merge-ollama-images, merge-slim-images]
continue-on-error: true
strategy:
fail-fast: false
matrix:
include:
- variant: main
suffix: ""
- variant: cuda
suffix: "-cuda"
- variant: cuda126
suffix: "-cuda126"
- variant: ollama
suffix: "-ollama"
- variant: slim
suffix: "-slim"
steps:
- name: Set repository and image name to lowercase
run: |
echo "IMAGE_NAME=${IMAGE_NAME,,}" >>${GITHUB_ENV}
echo "FULL_IMAGE_NAME=ghcr.io/${IMAGE_NAME,,}" >>${GITHUB_ENV}
env:
IMAGE_NAME: '${{ github.repository }}'
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to the Container registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Log in to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Determine source and destination tags
id: tags
run: |
DOCKERHUB_IMAGE="openwebui/open-webui"
SUFFIX="${{ matrix.suffix }}"
if [[ "${{ github.ref }}" == refs/tags/v* ]]; then
# For version tags: copy version tag and major.minor tag
VERSION="${{ github.ref_name }}"
VERSION="${VERSION#v}"
MAJOR_MINOR="${VERSION%.*}"
echo "tags<<EOF" >> $GITHUB_OUTPUT
echo "${VERSION}${SUFFIX}" >> $GITHUB_OUTPUT
echo "${MAJOR_MINOR}${SUFFIX}" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
else
# For main branch
if [ -z "$SUFFIX" ]; then
echo "tags=latest" >> $GITHUB_OUTPUT
else
# e.g. latest-cuda -> also tag as just "cuda"
VARIANT_NAME="${SUFFIX#-}"
echo "tags<<EOF" >> $GITHUB_OUTPUT
echo "latest${SUFFIX}" >> $GITHUB_OUTPUT
echo "${VARIANT_NAME}" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
fi
fi
echo "dockerhub_image=${DOCKERHUB_IMAGE}" >> $GITHUB_OUTPUT
- name: Copy images from GHCR to Docker Hub
run: |
DOCKERHUB_IMAGE="${{ steps.tags.outputs.dockerhub_image }}"
SUFFIX="${{ matrix.suffix }}"
# Determine the source tag on GHCR
if [[ "${{ github.ref }}" == refs/tags/v* ]]; then
VERSION="${{ github.ref_name }}"
VERSION="${VERSION#v}"
SOURCE_TAG="${VERSION}${SUFFIX}"
else
if [ -z "$SUFFIX" ]; then
SOURCE_TAG="latest"
else
SOURCE_TAG="latest${SUFFIX}"
fi
fi
SOURCE="${{ env.FULL_IMAGE_NAME }}:${SOURCE_TAG}"
echo "Copying from ${SOURCE} to Docker Hub..."
# Copy each destination tag
while IFS= read -r TAG; do
[ -z "$TAG" ] && continue
DEST="${DOCKERHUB_IMAGE}:${TAG}"
echo " -> ${DEST}"
docker buildx imagetools create -t "${DEST}" "${SOURCE}"
done <<< "${{ steps.tags.outputs.tags }}"

407
.github/workflows/docker.yaml vendored Normal file
View file

@ -0,0 +1,407 @@
name: Create and publish Docker images with specific build args
on:
workflow_dispatch:
push:
branches:
- main
- dev
tags:
- v*
concurrency:
group: docker-${{ github.ref }}
cancel-in-progress: true
env:
REGISTRY: ghcr.io
jobs:
build:
runs-on: ${{ matrix.platform.runner }}
permissions:
contents: read
packages: write
strategy:
fail-fast: false
matrix:
platform:
- arch: linux/amd64
runner: ubuntu-latest
- arch: linux/arm64
runner: ubuntu-24.04-arm
variant:
- name: main
suffix: ""
build_args: ""
free_disk: false
- name: cuda
suffix: "-cuda"
build_args: "USE_CUDA=true"
free_disk: true
- name: cuda126
suffix: "-cuda126"
build_args: |
USE_CUDA=true
USE_CUDA_VER=cu126
free_disk: true
- name: ollama
suffix: "-ollama"
build_args: "USE_OLLAMA=true"
free_disk: false
- name: slim
suffix: "-slim"
build_args: "USE_SLIM=true"
free_disk: false
steps:
- name: Prepare environment
run: |
echo "IMAGE_NAME=${GITHUB_REPOSITORY,,}" >> ${GITHUB_ENV}
echo "FULL_IMAGE_NAME=${REGISTRY}/${GITHUB_REPOSITORY,,}" >> ${GITHUB_ENV}
platform=${{ matrix.platform.arch }}
echo "PLATFORM_PAIR=${platform//\//-}" >> ${GITHUB_ENV}
- name: Free disk space
if: matrix.variant.free_disk
run: rm -rf /opt/hostedtoolcache
- name: Checkout repository
uses: actions/checkout@v5
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Prepare CI Dockerfile
run: |
awk '
/^FROM --platform=\$BUILDPLATFORM node:/ {
print
print "ENV NODE_OPTIONS=\"--max-old-space-size=12288\""
next
}
{ print }
' Dockerfile > "${RUNNER_TEMP}/Dockerfile"
- name: Log in to the Container registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata for Docker images
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.FULL_IMAGE_NAME }}
tags: |
type=ref,event=branch
type=ref,event=tag
type=sha,prefix=git-
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
${{ matrix.variant.suffix != '' && format('type=raw,enable={0},prefix=,suffix=,value={1}', github.ref == 'refs/heads/main', matrix.variant.name) || '' }}
flavor: |
latest=${{ github.ref == 'refs/heads/main' }}
${{ matrix.variant.suffix != '' && format('suffix={0},onlatest=true', matrix.variant.suffix) || '' }}
- name: Extract metadata for Docker cache
id: cache-meta
uses: docker/metadata-action@v5
with:
images: ${{ env.FULL_IMAGE_NAME }}
tags: |
type=ref,event=branch
${{ github.ref_type == 'tag' && 'type=raw,value=main' || '' }}
flavor: |
prefix=cache-${{ matrix.variant.name }}-${{ matrix.platform.arch }}-
latest=false
- name: Build Docker image
uses: docker/build-push-action@v5
id: build
with:
context: .
file: ${{ runner.temp }}/Dockerfile
push: true
platforms: ${{ matrix.platform.arch }}
labels: ${{ steps.meta.outputs.labels }}
outputs: type=image,name=${{ env.FULL_IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true
cache-from: type=registry,ref=${{ steps.cache-meta.outputs.tags }}
cache-to: type=registry,ref=${{ steps.cache-meta.outputs.tags }},mode=max
sbom: true
build-args: |
BUILD_HASH=${{ github.sha }}
${{ matrix.variant.build_args }}
- name: Export digest
run: |
mkdir -p /tmp/digests
digest="${{ steps.build.outputs.digest }}"
touch "/tmp/digests/${digest#sha256:}"
- name: Upload digest
uses: actions/upload-artifact@v4
with:
name: digests-${{ matrix.variant.name }}-${{ env.PLATFORM_PAIR }}
path: /tmp/digests/*
if-no-files-found: error
retention-days: 1
merge:
runs-on: ubuntu-latest
needs: [build]
if: ${{ !cancelled() }}
permissions:
contents: read
packages: write
strategy:
fail-fast: false
matrix:
variant:
- name: main
suffix: ""
- name: cuda
suffix: "-cuda"
- name: cuda126
suffix: "-cuda126"
- name: ollama
suffix: "-ollama"
- name: slim
suffix: "-slim"
steps:
- name: Prepare environment
run: |
echo "IMAGE_NAME=${GITHUB_REPOSITORY,,}" >> ${GITHUB_ENV}
echo "FULL_IMAGE_NAME=${REGISTRY}/${GITHUB_REPOSITORY,,}" >> ${GITHUB_ENV}
- name: Download digests
id: download
uses: actions/download-artifact@v5
with:
pattern: digests-${{ matrix.variant.name }}-*
path: /tmp/digests
merge-multiple: true
continue-on-error: true
- name: Check digests
id: check
run: |
count=$(find /tmp/digests -type f 2>/dev/null | wc -l | tr -d ' ')
echo "digest_count=$count" >> $GITHUB_OUTPUT
if [ "$count" -lt 2 ]; then
echo "::warning::${{ matrix.variant.name }}: found $count digest(s), need 2 (one per arch). Skipping merge."
echo "skip=true" >> $GITHUB_OUTPUT
else
echo "skip=false" >> $GITHUB_OUTPUT
fi
- name: Set up Docker Buildx
if: steps.check.outputs.skip != 'true'
uses: docker/setup-buildx-action@v3
- name: Log in to the Container registry
if: steps.check.outputs.skip != 'true'
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata for Docker images
if: steps.check.outputs.skip != 'true'
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.FULL_IMAGE_NAME }}
tags: |
type=ref,event=branch
type=ref,event=tag
type=sha,prefix=git-
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
${{ matrix.variant.suffix != '' && format('type=raw,enable={0},prefix=,suffix=,value={1}', github.ref == 'refs/heads/main', matrix.variant.name) || '' }}
flavor: |
latest=${{ github.ref == 'refs/heads/main' }}
${{ matrix.variant.suffix != '' && format('suffix={0},onlatest=true', matrix.variant.suffix) || '' }}
- name: Create manifest list and push
if: steps.check.outputs.skip != 'true'
working-directory: /tmp/digests
run: |
docker buildx imagetools create \
$(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
$(printf '${{ env.FULL_IMAGE_NAME }}@sha256:%s ' *)
- name: Inspect image
if: steps.check.outputs.skip != 'true'
run: |
docker buildx imagetools inspect ${{ env.FULL_IMAGE_NAME }}:${{ steps.meta.outputs.version }}
notify-helm-charts:
runs-on: ubuntu-latest
needs: [merge]
if: ${{ !cancelled() && needs.merge.result == 'success' && (github.ref == 'refs/heads/dev' || startsWith(github.ref, 'refs/tags/v')) }}
steps:
- name: Create Helm charts app token
id: helm-app-token
uses: actions/create-github-app-token@v2
with:
app-id: ${{ secrets.HELM_CHARTS_APP_ID }}
private-key: ${{ secrets.HELM_CHARTS_APP_PRIVATE_KEY }}
owner: ${{ github.repository_owner }}
repositories: helm-charts
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Verify published Open WebUI image
id: image
run: |
set -euo pipefail
image_name="ghcr.io/${GITHUB_REPOSITORY,,}"
ref_name="${GITHUB_REF_NAME}"
if [ "${GITHUB_REF}" = "refs/heads/dev" ]; then
image_tag="dev"
else
image_tag="${ref_name#v}"
fi
docker buildx imagetools inspect "${image_name}:${image_tag}"
echo "tag=${image_tag}" >> "${GITHUB_OUTPUT}"
- name: Dispatch Helm chart automation
uses: actions/github-script@v8
with:
github-token: ${{ steps.helm-app-token.outputs.token }}
script: |
const isDev = context.ref === 'refs/heads/dev';
const eventType = isDev
? 'open-webui-dev-image-published'
: 'open-webui-release-published';
const refName = context.ref.replace('refs/heads/', '').replace('refs/tags/', '');
const appVersion = refName.startsWith('v') ? refName.slice(1) : refName;
const payload = {
image_tag: isDev ? 'dev' : appVersion,
source_ref: context.ref,
source_sha: context.sha,
source_run_id: String(context.runId),
source_repository: context.repo.repo,
};
if (!isDev) {
payload.app_version = appVersion;
}
await github.rest.repos.createDispatchEvent({
owner: context.repo.owner,
repo: 'helm-charts',
event_type: eventType,
client_payload: payload,
});
copy-to-dockerhub:
runs-on: ubuntu-latest
if: ${{ !cancelled() && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v')) }}
needs: [merge]
continue-on-error: true
strategy:
fail-fast: false
matrix:
include:
- variant: main
suffix: ""
- variant: cuda
suffix: "-cuda"
- variant: cuda126
suffix: "-cuda126"
- variant: ollama
suffix: "-ollama"
- variant: slim
suffix: "-slim"
steps:
- name: Prepare environment
run: |
echo "IMAGE_NAME=${GITHUB_REPOSITORY,,}" >> ${GITHUB_ENV}
echo "FULL_IMAGE_NAME=${REGISTRY}/${GITHUB_REPOSITORY,,}" >> ${GITHUB_ENV}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to the Container registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Log in to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Determine source and destination tags
id: tags
run: |
DOCKERHUB_IMAGE="openwebui/open-webui"
SUFFIX="${{ matrix.suffix }}"
if [[ "${{ github.ref }}" == refs/tags/v* ]]; then
VERSION="${{ github.ref_name }}"
VERSION="${VERSION#v}"
MAJOR_MINOR="${VERSION%.*}"
echo "tags<<EOF" >> $GITHUB_OUTPUT
echo "${VERSION}${SUFFIX}" >> $GITHUB_OUTPUT
echo "${MAJOR_MINOR}${SUFFIX}" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
else
if [ -z "$SUFFIX" ]; then
echo "tags=latest" >> $GITHUB_OUTPUT
else
VARIANT_NAME="${SUFFIX#-}"
echo "tags<<EOF" >> $GITHUB_OUTPUT
echo "latest${SUFFIX}" >> $GITHUB_OUTPUT
echo "${VARIANT_NAME}" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
fi
fi
echo "dockerhub_image=${DOCKERHUB_IMAGE}" >> $GITHUB_OUTPUT
- name: Copy images from GHCR to Docker Hub
run: |
DOCKERHUB_IMAGE="${{ steps.tags.outputs.dockerhub_image }}"
SUFFIX="${{ matrix.suffix }}"
if [[ "${{ github.ref }}" == refs/tags/v* ]]; then
VERSION="${{ github.ref_name }}"
VERSION="${VERSION#v}"
SOURCE_TAG="${VERSION}${SUFFIX}"
else
if [ -z "$SUFFIX" ]; then
SOURCE_TAG="latest"
else
SOURCE_TAG="latest${SUFFIX}"
fi
fi
SOURCE="${{ env.FULL_IMAGE_NAME }}:${SOURCE_TAG}"
echo "Copying from ${SOURCE} to Docker Hub..."
while IFS= read -r TAG; do
[ -z "$TAG" ] && continue
DEST="${DOCKERHUB_IMAGE}:${TAG}"
echo " -> ${DEST}"
docker buildx imagetools create -t "${DEST}" "${SOURCE}"
done <<< "${{ steps.tags.outputs.tags }}"

View file

@ -1,46 +0,0 @@
name: Python CI
on:
push:
branches:
- main
- dev
paths:
- 'backend/**'
- 'pyproject.toml'
- 'uv.lock'
pull_request:
branches:
- main
- dev
paths:
- 'backend/**'
- 'pyproject.toml'
- 'uv.lock'
jobs:
build:
name: 'Format Backend'
runs-on: ubuntu-latest
strategy:
matrix:
python-version:
- 3.11.x
- 3.12.x
steps:
- uses: actions/checkout@v5
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: '${{ matrix.python-version }}'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install "ruff>=0.15.5"
- name: Ruff format check
run: ruff format --check . --exclude .venv --exclude venv

View file

@ -1,65 +0,0 @@
name: Frontend Build
on:
push:
branches:
- main
- dev
paths-ignore:
- 'backend/**'
- 'pyproject.toml'
- 'uv.lock'
pull_request:
branches:
- main
- dev
paths-ignore:
- 'backend/**'
- 'pyproject.toml'
- 'uv.lock'
jobs:
build:
name: 'Format & Build Frontend'
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@v5
- name: Setup Node.js
uses: actions/setup-node@v5
with:
node-version: '22'
- name: Install Dependencies
run: npm install --force
- name: Format Frontend
run: npm run format
- name: Run i18next
run: npm run i18n:parse
- name: Check for Changes After Format
run: git diff --exit-code
- name: Build Frontend
run: npm run build
test-frontend:
name: 'Frontend Unit Tests'
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@v5
- name: Setup Node.js
uses: actions/setup-node@v5
with:
node-version: '22'
- name: Install Dependencies
run: npm ci --force
- name: Run vitest
run: npm run test:frontend

65
.github/workflows/frontend.yaml vendored Normal file
View file

@ -0,0 +1,65 @@
# ─────────────────────────────────────────────────────────────────────────────
# Frontend CI — Lint, format check, build, and unit tests
# Runs on pushes and PRs to main/dev, skipping backend-only changes
# ─────────────────────────────────────────────────────────────────────────────
name: Frontend Build
on:
push:
branches: [main, dev]
paths-ignore: ['backend/**', 'pyproject.toml', 'uv.lock']
pull_request:
branches: [main, dev]
paths-ignore: ['backend/**', 'pyproject.toml', 'uv.lock']
concurrency:
group: frontend-${{ github.ref }}
cancel-in-progress: true
jobs:
# ── Format, i18n, and production build ────────────────────────────────────
format-and-build:
name: Format & Build
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v5
with:
node-version: '22'
- name: Install dependencies
run: npm install --force
- name: Verify code formatting
run: npm run format
- name: Verify i18n strings
run: npm run i18n:parse
- name: Ensure working tree is clean
run: git diff --exit-code
- name: Production build
run: npm run build
env:
NODE_OPTIONS: --max-old-space-size=8192
# ── Vitest unit tests ────────────────────────────────────────────────────
unit-tests:
name: Unit Tests
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v5
with:
node-version: '22'
- name: Install dependencies (frozen lockfile)
run: npm ci --force
- name: Execute test suite
run: npm run test:frontend

View file

@ -1,255 +0,0 @@
name: Integration Test
on:
push:
branches:
- main
- dev
pull_request:
branches:
- main
- dev
jobs:
cypress-run:
name: Run Cypress Integration Tests
runs-on: ubuntu-latest
steps:
- name: Maximize build space
uses: AdityaGarg8/remove-unwanted-software@v4.1
with:
remove-android: 'true'
remove-haskell: 'true'
remove-codeql: 'true'
- name: Checkout Repository
uses: actions/checkout@v4
- name: Build and run Compose Stack
run: |
docker compose \
--file docker-compose.yaml \
--file docker-compose.api.yaml \
--file docker-compose.a1111-test.yaml \
up --detach --build
- name: Delete Docker build cache
run: |
docker builder prune --all --force
- name: Wait for Ollama to be up
timeout-minutes: 5
run: |
until curl --output /dev/null --silent --fail http://localhost:11434; do
printf '.'
sleep 1
done
echo "Service is up!"
- name: Preload Ollama model
run: |
docker exec ollama ollama pull qwen:0.5b-chat-v1.5-q2_K
- name: Cypress run
uses: cypress-io/github-action@v6
env:
LIBGL_ALWAYS_SOFTWARE: 1
with:
browser: chrome
wait-on: 'http://localhost:3000'
config: baseUrl=http://localhost:3000
- uses: actions/upload-artifact@v4
if: always()
name: Upload Cypress videos
with:
name: cypress-videos
path: cypress/videos
if-no-files-found: ignore
- name: Extract Compose logs
if: always()
run: |
docker compose logs > compose-logs.txt
- uses: actions/upload-artifact@v4
if: always()
name: Upload Compose logs
with:
name: compose-logs
path: compose-logs.txt
if-no-files-found: ignore
# pytest:
# name: Run Backend Tests
# runs-on: ubuntu-latest
# steps:
# - uses: actions/checkout@v4
# - name: Set up Python
# uses: actions/setup-python@v5
# with:
# python-version: ${{ matrix.python-version }}
# - name: Install dependencies
# run: |
# python -m pip install --upgrade pip
# pip install -r backend/requirements.txt
# - name: pytest run
# run: |
# ls -al
# cd backend
# PYTHONPATH=. pytest . -o log_cli=true -o log_cli_level=INFO
migration_test:
name: Run Migration Tests
runs-on: ubuntu-latest
services:
postgres:
image: postgres
env:
POSTGRES_PASSWORD: postgres
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
# mysql:
# image: mysql
# env:
# MYSQL_ROOT_PASSWORD: mysql
# MYSQL_DATABASE: mysql
# options: >-
# --health-cmd "mysqladmin ping -h localhost"
# --health-interval 10s
# --health-timeout 5s
# --health-retries 5
# ports:
# - 3306:3306
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Set up uv
uses: yezz123/setup-uv@v4
with:
uv-venv: venv
- name: Activate virtualenv
run: |
. venv/bin/activate
echo PATH=$PATH >> $GITHUB_ENV
- name: Install dependencies
run: |
uv pip install -r backend/requirements.txt
- name: Test backend with SQLite
id: sqlite
env:
WEBUI_SECRET_KEY: secret-key
GLOBAL_LOG_LEVEL: debug
run: |
cd backend
uvicorn open_webui.main:app --port "8080" --forwarded-allow-ips '*' &
UVICORN_PID=$!
# Wait up to 40 seconds for the server to start
for i in {1..40}; do
curl -s http://localhost:8080/api/config > /dev/null && break
sleep 1
if [ $i -eq 40 ]; then
echo "Server failed to start"
kill -9 $UVICORN_PID
exit 1
fi
done
# Check that the server is still running after 5 seconds
sleep 5
if ! kill -0 $UVICORN_PID; then
echo "Server has stopped"
exit 1
fi
- name: Test backend with Postgres
if: success() || steps.sqlite.conclusion == 'failure'
env:
WEBUI_SECRET_KEY: secret-key
GLOBAL_LOG_LEVEL: debug
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/postgres
DATABASE_POOL_SIZE: 10
DATABASE_POOL_MAX_OVERFLOW: 10
DATABASE_POOL_TIMEOUT: 30
run: |
cd backend
uvicorn open_webui.main:app --port "8081" --forwarded-allow-ips '*' &
UVICORN_PID=$!
# Wait up to 20 seconds for the server to start
for i in {1..20}; do
curl -s http://localhost:8081/api/config > /dev/null && break
sleep 1
if [ $i -eq 20 ]; then
echo "Server failed to start"
kill -9 $UVICORN_PID
exit 1
fi
done
# Check that the server is still running after 5 seconds
sleep 5
if ! kill -0 $UVICORN_PID; then
echo "Server has stopped"
exit 1
fi
# Check that service will reconnect to postgres when connection will be closed
status_code=$(curl --write-out %{http_code} -s --output /dev/null http://localhost:8081/health/db)
if [[ "$status_code" -ne 200 ]] ; then
echo "Server has failed before postgres reconnect check"
exit 1
fi
echo "Terminating all connections to postgres..."
python -c "import os, psycopg2 as pg2; \
conn = pg2.connect(dsn=os.environ['DATABASE_URL'].replace('+pool', '')); \
cur = conn.cursor(); \
cur.execute('SELECT pg_terminate_backend(psa.pid) FROM pg_stat_activity psa WHERE datname = current_database() AND pid <> pg_backend_pid();')"
status_code=$(curl --write-out %{http_code} -s --output /dev/null http://localhost:8081/health/db)
if [[ "$status_code" -ne 200 ]] ; then
echo "Server has not reconnected to postgres after connection was closed: returned status $status_code"
exit 1
fi
# - name: Test backend with MySQL
# if: success() || steps.sqlite.conclusion == 'failure' || steps.postgres.conclusion == 'failure'
# env:
# WEBUI_SECRET_KEY: secret-key
# GLOBAL_LOG_LEVEL: debug
# DATABASE_URL: mysql://root:mysql@localhost:3306/mysql
# run: |
# cd backend
# uvicorn open_webui.main:app --port "8083" --forwarded-allow-ips '*' &
# UVICORN_PID=$!
# # Wait up to 20 seconds for the server to start
# for i in {1..20}; do
# curl -s http://localhost:8083/api/config > /dev/null && break
# sleep 1
# if [ $i -eq 20 ]; then
# echo "Server failed to start"
# kill -9 $UVICORN_PID
# exit 1
# fi
# done
# # Check that the server is still running after 5 seconds
# sleep 5
# if ! kill -0 $UVICORN_PID; then
# echo "Server has stopped"
# exit 1
# fi

139
.github/workflows/issue-label.yaml vendored Normal file
View file

@ -0,0 +1,139 @@
name: Issue Labeler
on:
issues:
types: [opened, edited]
permissions:
issues: write
jobs:
label-bug-reports:
runs-on: ubuntu-latest
steps:
- name: Add "bug" label to unlabeled bug reports
uses: actions/github-script@v7
with:
script: |
const issue = context.payload.issue;
// Web-form submissions already carry the label from the issue template
if (issue.labels.some((label) => label.name === 'bug')) {
return;
}
const isEdit = context.payload.action === 'edited';
const titleWasEdited = Boolean(context.payload.changes?.title);
if (isEdit && !titleWasEdited) {
return;
}
const title = issue.title ?? '';
const body = issue.body ?? '';
// Freeform bug reports: "issue: ...", "bug: ...", "fix: ...", "[Bug] ...", "issue/UX: ..."
const bugLikeTitle = /^\s*(\[\s*(bug|issue|fix)\b[^\]]*\]|(bug|issue|fix)\s*[:/\-])/i.test(title);
// API/CLI-created issues that reproduce the bug report form structure.
// Only headings distinctive to the bug form (both are required fields there) —
// generic headings like "Expected Behavior" also appear in freeform feature requests.
const bugFormBody = /###\s*(Installation Method|Open WebUI Version)/i.test(body);
if (!bugLikeTitle && !bugFormBody) {
return;
}
if (isEdit) {
const events = await github.paginate(github.rest.issues.listEvents, {
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
per_page: 100
});
const bugLabelWasRemoved = events.some(
(event) => event.event === 'unlabeled' && event.label?.name === 'bug'
);
if (bugLabelWasRemoved) {
return;
}
}
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
labels: ['bug']
});
label-feature-requests:
runs-on: ubuntu-latest
steps:
- name: Add "enhancement" label to unlabeled feature requests
uses: actions/github-script@v7
with:
script: |
const issue = context.payload.issue;
if (issue.labels.some((label) => label.name === 'enhancement')) {
return;
}
// A human (or the bug form) already classified this as a bug;
// do not stack a second, contradictory classification on it.
if (issue.labels.some((label) => label.name === 'bug')) {
return;
}
const isEdit = context.payload.action === 'edited';
const titleWasEdited = Boolean(context.payload.changes?.title);
if (isEdit && !titleWasEdited) {
return;
}
const title = issue.title ?? '';
const body = issue.body ?? '';
// Feature requests: "feat: ...", "feature: ...", "feature request: ...",
// "enhancement: ...", "enh: ...", "[Feature Request] ..." — the feature
// request form titles every submission "feat: ", so form submissions are
// covered by the same pattern.
const featureLikeTitle =
/^\s*(\[\s*(feat|feature|enhancement|enh)\b[^\]]*\]|(feat|feature( request)?|enhancement|enh)\s*[:/\-])/i.test(
title
);
// API/CLI-created issues that reproduce the feature request form structure.
// Only headings distinctive to that form.
const featureFormBody = /###\s*(Proposed Solution|Alternatives Considered)/i.test(body);
if (!featureLikeTitle && !featureFormBody) {
return;
}
if (isEdit) {
const events = await github.paginate(github.rest.issues.listEvents, {
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
per_page: 100
});
const enhancementLabelWasRemoved = events.some(
(event) => event.event === 'unlabeled' && event.label?.name === 'enhancement'
);
if (enhancementLabelWasRemoved) {
return;
}
}
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
labels: ['enhancement']
});

74
.github/workflows/release.yml vendored Normal file
View file

@ -0,0 +1,74 @@
# ─────────────────────────────────────────────────────────────────────────────
# Release — Create GitHub release from CHANGELOG, trigger Docker builds
# Runs on pushes to main when package.json version changes
# ─────────────────────────────────────────────────────────────────────────────
name: Release
on:
push:
branches: [main]
concurrency:
group: release-${{ github.ref }}
cancel-in-progress: false
jobs:
# ── Create release and trigger downstream workflows ──────────────────────
publish:
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: write
actions: write
steps:
- uses: actions/checkout@v5
- name: Abort if package.json unchanged
run: |
git diff --cached --diff-filter=d package.json || {
echo "package.json not modified — skipping release"
exit 1
}
- name: Read version
id: pkg
run: echo "version=$(jq -r '.version' package.json)" >> $GITHUB_OUTPUT
- name: Extract release notes from CHANGELOG
run: |
VER="${{ steps.pkg.outputs.version }}"
awk "/^## \[${VER}\]/{found=1; next} /^## \[/{if(found) exit} found{print}" \
CHANGELOG.md > /tmp/release-notes.md
- name: Publish GitHub release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
if gh release view "v${{ steps.pkg.outputs.version }}" &>/dev/null; then
echo "Release v${{ steps.pkg.outputs.version }} already exists — skipping creation"
else
gh release create "v${{ steps.pkg.outputs.version }}" \
--title "v${{ steps.pkg.outputs.version }}" \
--notes-file /tmp/release-notes.md
fi
- name: Archive source
uses: actions/upload-artifact@v4
with:
name: release-archive
path: |
.
!.git
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Trigger Docker build
uses: actions/github-script@v8
with:
script: |
github.rest.actions.createWorkflowDispatch({
owner: context.repo.owner,
repo: context.repo.repo,
workflow_id: 'docker.yaml',
ref: 'v${{ steps.pkg.outputs.version }}',
})

1
.gitignore vendored
View file

@ -310,3 +310,4 @@ dist
cypress/videos
cypress/screenshots
.vscode/settings.json
.cptr

File diff suppressed because it is too large Load diff

View file

@ -20,7 +20,7 @@ Examples of behavior that contribute to a positive and professional community in
- **Respecting others.** Be considerate, listen actively, and engage with empathy toward others' viewpoints and experiences.
- **Constructive feedback.** Provide actionable, thoughtful, and respectful feedback that helps improve the project and encourages collaboration. Avoid unproductive negativity or hypercriticism.
- **Recognizing volunteer contributions.** Appreciate that contributors dedicate their free time and resources selflessly. Approach them with gratitude and patience.
- **Recognizing volunteer contributions.** Appreciate that **contributors dedicate their free time and resources selflessly**. Approach them with gratitude and patience.
- **Focusing on shared goals.** Collaborate in ways that prioritize the health, success, and sustainability of the community over individual agendas.
Examples of unacceptable behavior include:
@ -32,6 +32,7 @@ Examples of unacceptable behavior include:
- **Entitlement, demand, or aggression toward contributors.** Volunteers are under no obligation to provide immediate or personalized support. Rude or dismissive behavior will not be tolerated.
- **Unproductive or destructive behavior.** This includes venting frustration as hostility ("tantrums"), hypercriticism, attention-seeking negativity, or anything that distracts from the project's goals.
- **Spamming and promotional exploitation.** Sharing irrelevant product promotions or self-promotion in the community is not allowed unless it directly contributes value to the discussion.
- Posting low-effort, hard to read, essay-length AI generated comments or other forms of low-quality, hard to parse content that puts the burden of understanding on the reader.
### Feedback and Community Engagement

View file

@ -43,7 +43,7 @@ ENV APP_BUILD_HASH=${BUILD_HASH}
RUN npm run build
######## WebUI backend ########
FROM python:3.11.14-slim-bookworm AS base
FROM python:3.11-slim-bookworm AS base
# Use args
ARG USE_CUDA
@ -126,7 +126,7 @@ RUN chown -R $UID:$GID /app $HOME
# Install common system dependencies
RUN apt-get update && \
apt-get install -y --no-install-recommends \
git build-essential pandoc gcc netcat-openbsd curl jq \
git build-essential pandoc gcc curl jq ca-certificates \
libmariadb-dev \
python3-dev \
ffmpeg libsm6 libxext6 zstd \
@ -149,7 +149,7 @@ RUN set -e; \
python -c "import os; from sentence_transformers import SentenceTransformer; SentenceTransformer(os.environ.get('AUXILIARY_EMBEDDING_MODEL', 'TaylorAI/bge-micro-v2'), device='cpu')"; \
python -c "import os; from faster_whisper import WhisperModel; WhisperModel(os.environ['WHISPER_MODEL'], device='cpu', compute_type='int8', download_root=os.environ['WHISPER_MODEL_DIR'])"; \
python -c "import os; import tiktoken; tiktoken.get_encoding(os.environ['TIKTOKEN_ENCODING_NAME'])"; \
python -c "import nltk; nltk.download('punkt_tab')"; \
python -c "import nltk; nltk.download('punkt_tab', download_dir='/usr/local/share/nltk_data')"; \
else \
pip3 install 'torch<=2.9.1' torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu --no-cache-dir; \
uv pip install --system -r requirements.txt --no-cache-dir; \
@ -158,12 +158,18 @@ RUN set -e; \
python -c "import os; from sentence_transformers import SentenceTransformer; SentenceTransformer(os.environ.get('AUXILIARY_EMBEDDING_MODEL', 'TaylorAI/bge-micro-v2'), device='cpu')"; \
python -c "import os; from faster_whisper import WhisperModel; WhisperModel(os.environ['WHISPER_MODEL'], device='cpu', compute_type='int8', download_root=os.environ['WHISPER_MODEL_DIR'])"; \
python -c "import os; import tiktoken; tiktoken.get_encoding(os.environ['TIKTOKEN_ENCODING_NAME'])"; \
python -c "import nltk; nltk.download('punkt_tab')"; \
python -c "import nltk; nltk.download('punkt_tab', download_dir='/usr/local/share/nltk_data')"; \
fi; \
fi; \
mkdir -p /app/backend/data; chown -R $UID:$GID /app/backend/data/; \
if [ -d /app/backend/data/cache ]; then chmod -R a+rX /app/backend/data/cache; fi; \
rm -rf /var/lib/apt/lists/*;
# Optional: PPTX parsing through unstructured may need spaCy's English model.
# Keep this out of the default image to avoid the extra image bloat; deployments
# with read-only site-packages can uncomment it and bake the model in.
# RUN python -m spacy download en_core_web_sm
# Install Ollama if requested
RUN if [ "$USE_OLLAMA" = "true" ]; then \
date +%s > /tmp/ollama_build_hash && \
@ -184,6 +190,17 @@ COPY --chown=$UID:$GID --from=build /app/package.json /app/package.json
# copy backend files
COPY --chown=$UID:$GID ./backend .
# The backend rewrites its bundled static assets (favicons, splash, manifest,
# loader.js, ...) under open_webui/static at startup. Make that directory
# writable by an arbitrary UID -- which under OpenShift's restricted SCC is
# always a member of GID 0 -- so those writes don't fail with EACCES and crash
# the boot log with "[Errno 13] Permission denied". `chmod -R g=u` mirrors the
# owner bits onto the group (the Red Hat arbitrary-UID idiom). This is applied
# unconditionally because it targets a directory the app writes on every start;
# the broader, opt-in USE_PERMISSION_HARDENING below covers the rest of /app.
RUN chgrp -R 0 /app/backend/open_webui/static && \
chmod -R g=u /app/backend/open_webui/static
EXPOSE 8080
HEALTHCHECK CMD curl --silent --fail http://localhost:${PORT:-8080}/health | jq -ne 'input.status == true' || exit 1

View file

@ -8,7 +8,7 @@
![GitHub top language](https://img.shields.io/github/languages/top/open-webui/open-webui)
![GitHub last commit](https://img.shields.io/github/last-commit/open-webui/open-webui?color=red)
[![Discord](https://img.shields.io/badge/Discord-Open_WebUI-blue?logo=discord&logoColor=white)](https://discord.gg/5rJgQTnV4s)
[![](https://img.shields.io/static/v1?label=Sponsor&message=%E2%9D%A4&logo=GitHub&color=%23fe8e86)](https://github.com/sponsors/tjbck)
[![](https://img.shields.io/static/v1?label=Sponsor&message=%E2%9D%A4&logo=GitHub&color=%23fe8e86)](https://github.com/sponsors/open-webui)
![Open WebUI Banner](./banner.png)
@ -27,58 +27,82 @@ For more information, be sure to check out our [Open WebUI Documentation](https:
## Key Features of Open WebUI ⭐
- 🚀 **Effortless Setup**: Install seamlessly using Docker or Kubernetes (kubectl, kustomize or helm) for a hassle-free experience with support for both `:ollama` and `:cuda` tagged images.
- 🚀 **Effortless Setup**: Install seamlessly via pip, uv, Docker, or Kubernetes (kubectl, kustomize, or helm), with `:ollama` and `:cuda` tagged images available for container deployments.
- 🤝 **Ollama/OpenAI API Integration**: Effortlessly integrate OpenAI-compatible APIs for versatile conversations alongside Ollama models. Customize the OpenAI API URL to link with **LMStudio, GroqCloud, Mistral, OpenRouter, and more**.
- 🤝 **Broad Model & API Integration**: Connect any OpenAI-compatible API alongside local Ollama models. Point the API URL at **LMStudio, GroqCloud, Mistral, OpenRouter, vLLM, and more** to mix and match providers freely.
- 🛡️ **Granular Permissions and User Groups**: By allowing administrators to create detailed user roles and permissions, we ensure a secure user environment. This granularity not only enhances security but also allows for customized user experiences, fostering a sense of ownership and responsibility amongst users.
- 🔐 **Granular RBAC & User Groups**: Administrators define detailed roles, groups, and permissions, giving each user exactly the access they need. Secure by default, with tailored experiences per group.
- 📱 **Responsive Design**: Enjoy a seamless experience across Desktop PC, Laptop, and Mobile devices.
- 🧩 **Plugin Support**: Extend Open WebUI with **Filters**, **Actions**, **Pipes**, **Tools**, and **Skills**. Connect external services through **MCP**, **MCPO**, and **OpenAPI tool servers**. Build custom integrations, rate limits, approval flows, data connections, and more.
- 📱 **Progressive Web App (PWA) for Mobile**: Enjoy a native app-like experience on your mobile device with our PWA, providing offline access on localhost and a seamless user interface.
- 🤖 **Models & Agents**: Wrap any base model with custom instructions, tools, and knowledge to build specialized agents. Supports dynamic variables, per-user/group access control, and community preset imports via [Open WebUI Community](https://openwebui.com/).
- ✒️🔢 **Full Markdown and LaTeX Support**: Elevate your LLM experience with comprehensive Markdown and LaTeX capabilities for enriched interaction.
- 📝 **Notes**: A dedicated workspace for content outside conversations. Draft with a rich editor, use AI to rewrite selected text, and attach notes to any chat for full-context injection.
- 🎤📹 **Hands-Free Voice/Video Call**: Experience seamless communication with integrated hands-free voice and video call features using multiple Speech-to-Text providers (Local Whisper, OpenAI, Deepgram, Azure) and Text-to-Speech engines (Azure, ElevenLabs, OpenAI, Transformers, WebAPI), allowing for dynamic and interactive chat environments.
- 📢 **Channels**: Real-time shared spaces where your team and AI models collaborate in one timeline. Tag models to draft or critique, with threads, reactions, pins, and access control.
- 🛠️ **Model Builder**: Easily create Ollama models via the Web UI. Create and add custom characters/agents, customize chat elements, and import models effortlessly through [Open WebUI Community](https://openwebui.com/) integration.
- 🧠 **Persistent Memory**: The AI remembers facts about you across conversations, carrying context from one chat to the next.
- 🐍 **Native Python Function Calling Tool**: Enhance your LLMs with built-in code editor support in the tools workspace. Bring Your Own Function (BYOF) by simply adding your pure Python functions, enabling seamless integration with LLMs.
- **Live Workflow & Message Flow**: Watch the AI build and work through checklists in real time. Queue messages while the AI is still responding; they send automatically when it's ready.
- 💾 **Persistent Artifact Storage**: Built-in key-value storage API for artifacts, enabling features like journals, trackers, leaderboards, and collaborative tools with both personal and shared data scopes across sessions.
- 📅 **Calendar & AI Scheduling**: Built-in personal and shared calendars with month/week/day views, recurring events, color coding, attendees, and reminders. Models manage your schedule conversationally through native function calling.
- 📚 **Local RAG Integration**: Dive into the future of chat interactions with groundbreaking Retrieval Augmented Generation (RAG) support using your choice of 9 vector databases and multiple content extraction engines (Tika, Docling, Document Intelligence, Mistral OCR, PaddleOCR-vl, External loaders). Load documents directly into chat or add files to your document library, effortlessly accessing them using the `#` command before a query.
- ⏱️ **Automations**: Schedule prompts to run on recurring schedules, with runs surfaced on your calendar and each completed run linking back to the chat it produced.
- 🔍 **Web Search for RAG**: Perform web searches using 15+ providers including `SearXNG`, `Google PSE`, `Brave Search`, `Kagi`, `Mojeek`, `Tavily`, `Perplexity`, `serpstack`, `serper`, `Serply`, `DuckDuckGo`, `SearchApi`, `SerpApi`, `Bing`, `Jina`, `Exa`, `Sougou`, `Azure AI Search`, and `Ollama Cloud`, injecting results directly into your chat experience.
- 📱 **Responsive Design & PWA**: Seamless experience across desktop, laptop, and mobile, with a Progressive Web App for native app-like feel and offline access on localhost.
- 🌐 **Web Browsing Capability**: Seamlessly integrate websites into your chat experience using the `#` command followed by a URL. This feature allows you to incorporate web content directly into your conversations, enhancing the richness and depth of your interactions.
- ✒️🔢 **Full Markdown and LaTeX Support**: Comprehensive Markdown and LaTeX capabilities for enriched interaction.
- 🎨 **Image Generation & Editing Integration**: Create and edit images using multiple engines including OpenAI's DALL-E, Gemini, ComfyUI (local), and AUTOMATIC1111 (local), with support for both generation and prompt-based editing workflows.
- 🎤📹 **Hands-Free Voice/Video Call**: Integrated voice and video calls with multiple Speech-to-Text providers (Local Whisper, OpenAI, Deepgram, Azure) and Text-to-Speech engines (Azure, ElevenLabs, OpenAI, Transformers, WebAPI).
- ⚙️ **Many Models Conversations**: Effortlessly engage with various models simultaneously, harnessing their unique strengths for optimal responses. Enhance your experience by leveraging a diverse set of models in parallel.
- 💾 **Persistent Artifact Storage**: Built-in key-value storage API for artifacts, enabling journals, trackers, leaderboards, and collaborative tools with personal and shared data scopes.
- 🔐 **Role-Based Access Control (RBAC)**: Ensure secure access with restricted permissions; only authorized individuals can access your Ollama, and exclusive model creation/pulling rights are reserved for administrators.
- 📚 **Local RAG Integration**: Retrieval Augmented Generation backed by 9 vector databases and multiple content-extraction engines (Tika, Docling, Document Intelligence, Mistral OCR, PaddleOCR-vl, external loaders). Supports hybrid search (BM25 + vector) with reranking and full-context mode. Load documents into chat or pull them from your library with the `#` command.
- 🗄️ **Flexible Database & Storage Options**: Choose from SQLite (with optional encryption), PostgreSQL, or configure cloud storage backends (S3, Google Cloud Storage, Azure Blob Storage) for scalable deployments.
- 🔍 **Web Search for RAG**: Search the web through dozens of providers including `SearXNG`, `Google PSE`, `Brave Search`, `Kagi`, `Mojeek`, `Tavily`, `Perplexity`, `Firecrawl`, `serpstack`, `serper`, `Serply`, `DuckDuckGo`, `SearchApi`, `SerpApi`, `Bing`, `Jina`, `Exa`, `Sougou`, `Azure AI Search`, and `Ollama Cloud`, injecting results directly into the conversation.
- 🔍 **Advanced Vector Database Support**: Select from 9 vector database options including ChromaDB, PGVector, Qdrant, Milvus, Elasticsearch, OpenSearch, Pinecone, S3Vector, and Oracle 23ai for optimal RAG performance.
- 🌐 **Web Browsing Capability**: Pull websites into chat with the `#` command followed by a URL, or let the model fetch them on its own when needed.
- 🔐 **Enterprise Authentication**: Full support for LDAP/Active Directory integration, SCIM 2.0 automated provisioning, and SSO via trusted headers alongside OAuth providers. Enterprise-grade user and group provisioning through SCIM 2.0 protocol, enabling seamless integration with identity providers like Okta, Azure AD, and Google Workspace for automated user lifecycle management.
- 🎨 **Image Generation & Editing**: Create and edit images with multiple engines including OpenAI DALL·E, Gemini, ComfyUI (local), and AUTOMATIC1111 (local), supporting both generation and prompt-based editing.
- ☁️ **Cloud-Native Integration**: Native support for Google Drive and OneDrive/SharePoint file picking, enabling seamless document import from enterprise cloud storage.
- ⚙️ **Multi-Model Conversations**: Engage several models at once, harnessing their individual strengths in parallel for the best possible responses.
- 📊 **Production Observability**: Built-in OpenTelemetry support for traces, metrics, and logs, enabling comprehensive monitoring with your existing observability stack.
- 📊 **Usage Analytics & Model Evaluation**: Admin dashboards track message volume, token consumption, and cost across users and models. Evaluate models with a built-in arena, A/B testing, and ELO-based leaderboards.
- ⚖️ **Horizontal Scalability**: Redis-backed session management and WebSocket support for multi-worker and multi-node deployments behind load balancers.
- 🗄️ **Flexible Database & Storage**: Choose SQLite (with optional encryption) or PostgreSQL, and store files locally or on S3, Google Cloud Storage, or Azure Blob Storage.
- 🌐🌍 **Multilingual Support**: Experience Open WebUI in your preferred language with our internationalization (i18n) support. Join us in expanding our supported languages! We're actively seeking contributors!
- 🧬 **Advanced Vector Database Support**: Pick from 9 vector databases: ChromaDB, PGVector, Qdrant, Milvus, Elasticsearch, OpenSearch, Pinecone, S3Vector, and Oracle 23ai.
- 🧩 **Pipelines, Open WebUI Plugin Support**: Seamlessly integrate custom logic and Python libraries into Open WebUI using [Pipelines Plugin Framework](https://github.com/open-webui/pipelines). Launch your Pipelines instance, set the OpenAI URL to the Pipelines URL, and explore endless possibilities. [Examples](https://github.com/open-webui/pipelines/tree/main/examples) include **Function Calling**, User **Rate Limiting** to control access, **Usage Monitoring** with tools like Langfuse, **Live Translation with LibreTranslate** for multilingual support, **Toxic Message Filtering** and much more.
- 🪪 **Enterprise Authentication & Provisioning**: Full LDAP/Active Directory integration, SSO via trusted headers and OAuth providers, and SCIM 2.0 automated provisioning for identity providers like Okta, Azure AD, and Google Workspace.
- 🌟 **Continuous Updates**: We are committed to improving Open WebUI with regular updates, fixes, and new features.
- ☁️ **Cloud-Native File Integration**: Native Google Drive and OneDrive/SharePoint file picking for seamless document import from enterprise cloud storage.
- 🔭 **Production Observability**: Built-in OpenTelemetry support for traces, metrics, and logs, plugging into your existing monitoring stack.
- ⚖️ **Horizontal Scalability**: Redis-backed session management and WebSocket support for multi-worker, multi-node deployments behind load balancers.
- 🌐🌍 **Multilingual Support**: Use Open WebUI in your preferred language with i18n support. We're actively seeking contributors to expand language coverage!
- 🌟 **Continuous Updates**: We're committed to improving Open WebUI with regular updates, fixes, and new features.
- 🛡️ **Transparent Security Process**: Security reports are triaged, fixed, and published as open advisories through a documented responsible-disclosure process. See our [Security Policy](https://github.com/open-webui/open-webui/security).
Want to learn more about Open WebUI's features? Check out our [Open WebUI documentation](https://docs.openwebui.com/features) for a comprehensive overview!
## The Open WebUI Ecosystem 🌐
Open WebUI is the core, surrounded by companion apps and infrastructure that extend what your AI can do, where it can reach, and how you run it:
- 💻 **Open WebUI Computer** ([open-webui/computer](https://github.com/open-webui/computer)): A standalone, mobile-first computer and coding agent that runs on the machine you own. Files, terminal, and git in a browser tab, reachable from your phone. Connect it into Open WebUI as a model, or reach it from Telegram, WhatsApp, and more.
- ⚡ **Open Terminal** and **Terminals (Enterprise)** ([open-webui/open-terminal](https://github.com/open-webui/open-terminal) & [open-webui/terminals](https://github.com/open-webui/terminals)): A self-hosted computing environment that plugs into Open WebUI, giving the AI a place to write code, run it, read output, fix errors, and iterate inside the chat. Terminals gives you per-user isolated containers with separate credentials, resource limits, and network rules. Automatic lifecycle management on Docker or Kubernetes.
- 🔄 **oikb** ([open-webui/oikb](https://github.com/open-webui/oikb)): Feed your Knowledge Bases from 45+ sources (GitHub, Confluence, ServiceNow, Salesforce, Jira, Slack, SharePoint, Notion, and more), keeping the tools your team already uses continuously in sync.
- 🖥️ **Native Desktop App** ([open-webui/desktop](https://github.com/open-webui/desktop)): Run Open WebUI as a native app on macOS, Windows, and Linux. System-wide Spotlight chat bar with screenshot capture, push-to-talk voice, and optional fully-local inference via a built-in llama.cpp engine.
Want to learn more? Check out our [Open WebUI documentation](https://docs.openwebui.com) for more details!
---
We are incredibly grateful for the generous support of our sponsors. Their contributions help us to maintain and improve our project, ensuring we can continue to deliver quality work to our community. Thank you!
@ -222,6 +246,10 @@ This project contains code under multiple licenses. The current codebase include
If you have any questions, suggestions, or need assistance, please open an issue or join our
[Open WebUI Discord community](https://discord.gg/5rJgQTnV4s) to connect with us! 🤝
## Security 🛡️
If you believe you've found a security vulnerability, or something that shouldn't be disclosed publicly, please [reach out confidentially through our responsible disclosure program on GitHub](https://github.com/open-webui/open-webui/security). We accept reports only through GitHub, not through any other platform. Thank you for helping us keep Open WebUI secure!
## Star History
<a href="https://star-history.com/#open-webui/open-webui&Date">

View file

@ -1,3 +1,3 @@
export CORS_ALLOW_ORIGIN="http://localhost:5173;http://localhost:8080"
PORT="${PORT:-8080}"
uvicorn open_webui.main:app --port $PORT --host 0.0.0.0 --forwarded-allow-ips "${FORWARDED_ALLOW_IPS:-*}" --reload
uvicorn open_webui.main:app --port $PORT --host 0.0.0.0 --forwarded-allow-ips "${FORWARDED_ALLOW_IPS:-*}" --ws-per-message-deflate "${UVICORN_WS_PER_MESSAGE_DEFLATE:-true}" --reload

View file

@ -11,12 +11,16 @@ import uvicorn
app = typer.Typer()
KEY_FILE = Path.cwd() / '.webui_secret_key'
DEFAULT_SECRET_KEY_LENGTH = 24
def version_callback(value: bool) -> None:
if value:
from open_webui.env import VERSION
# LICENSE covers this Open WebUI CLI identifier.
# Do not alter, remove, obscure, or replace it except as LICENSE permits:
# https://docs.openwebui.com/license.
typer.echo(f'Open WebUI version: {VERSION}')
raise typer.Exit()
@ -37,8 +41,11 @@ def serve(
if os.getenv('WEBUI_SECRET_KEY') is None:
typer.echo('Loading WEBUI_SECRET_KEY from file, not provided as an environment variable.')
if not KEY_FILE.exists():
key_length = int(os.getenv('WEBUI_SECRET_KEY_LENGTH', DEFAULT_SECRET_KEY_LENGTH))
if key_length < 1:
raise ValueError('WEBUI_SECRET_KEY_LENGTH must be a positive integer')
typer.echo(f'Generating a new secret key and saving it to {KEY_FILE}')
KEY_FILE.write_bytes(base64.b64encode(random.randbytes(12)))
KEY_FILE.write_bytes(base64.b64encode(random.randbytes(key_length)))
typer.echo(f'Loading WEBUI_SECRET_KEY from {KEY_FILE}')
os.environ['WEBUI_SECRET_KEY'] = KEY_FILE.read_text()
@ -67,7 +74,7 @@ def serve(
os.environ['LD_LIBRARY_PATH'] = ':'.join(LD_LIBRARY_PATH)
import open_webui.main # noqa: F401
from open_webui.env import UVICORN_WORKERS # Import the workers setting
from open_webui.env import UVICORN_WORKERS, UVICORN_WS_PER_MESSAGE_DEFLATE
# On Windows, uvicorn's default loop factory hardcodes ProactorEventLoop,
# which is incompatible with psycopg v3 async. Setting loop='none' lets
@ -80,6 +87,7 @@ def serve(
port=port,
forwarded_allow_ips='*',
workers=UVICORN_WORKERS,
ws_per_message_deflate=UVICORN_WS_PER_MESSAGE_DEFLATE,
loop=loop,
)
@ -90,12 +98,15 @@ def dev(
port: int = 8080,
reload: bool = True,
):
from open_webui.env import UVICORN_WS_PER_MESSAGE_DEFLATE
uvicorn.run(
'open_webui.main:app',
host=host,
port=port,
reload=reload,
forwarded_allow_ips='*',
ws_per_message_deflate=UVICORN_WS_PER_MESSAGE_DEFLATE,
)

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,29 @@
from __future__ import annotations
import errno
from enum import Enum
_ERRNO_MESSAGES = {
errno.ENAMETOOLONG: 'File name is too long.',
errno.ENOSPC: 'The server is out of storage space.',
errno.EDQUOT: 'Server storage quota exceeded.',
errno.EACCES: 'Server storage is not writable.',
errno.EPERM: 'Server storage is not writable.',
errno.EROFS: 'Server storage is not writable.',
}
def _error_message(err='', fallback='') -> str:
if not err:
return 'Something went wrong :/'
if isinstance(err, OSError) and err.errno in _ERRNO_MESSAGES:
return f'[ERROR: {_ERRNO_MESSAGES[err.errno]}]'
if isinstance(err, Exception):
return f'[ERROR: {fallback}]' if fallback else 'Something went wrong :/'
return f'[ERROR: {err}]'
class MESSAGES(str, Enum):
DEFAULT = lambda msg='': f'{msg if msg else ""}'
MODEL_ADDED = lambda model='': f"The model '{model}' has been added successfully."
@ -16,7 +39,7 @@ class ERROR_MESSAGES(str, Enum):
def __str__(self) -> str:
return super().__str__()
DEFAULT = lambda err='': f'{"Something went wrong :/" if err == "" else "[ERROR: " + str(err) + "]"}'
DEFAULT = _error_message
ENV_VAR_NOT_FOUND = 'Required environment variable not found. Terminating now.'
CREATE_USER_ERROR = 'Oops! Something went wrong while creating your account. Please try again later. If the issue persists, contact support for assistance.'
DELETE_USER_ERROR = 'Oops! Something went wrong. We encountered an issue while trying to delete the user. Please give it another shot.'
@ -72,11 +95,11 @@ class ERROR_MESSAGES(str, Enum):
EMPTY_CONTENT = 'The content provided is empty. Please ensure that there is text or data present before proceeding.'
DB_NOT_SQLITE = 'This feature is only available when running with SQLite databases.'
DB_NOT_SQLITE = 'This feature is only available with SQLite databases.'
INVALID_URL = 'Oops! The URL you provided is invalid. Please double-check and try again.'
INVALID_URL = 'The URL you provided is invalid. Please double-check and try again.'
WEB_SEARCH_ERROR = lambda err='': f'{err if err else "Oops! Something went wrong while searching the web."}'
WEB_SEARCH_ERROR = 'Something went wrong while searching the web.'
OLLAMA_API_DISABLED = 'The Ollama API is disabled. Please enable it to use this feature.'
@ -95,9 +118,15 @@ class ERROR_MESSAGES(str, Enum):
AUTOMATION_TOO_FREQUENT = lambda interval='': f'Schedule too frequent. Minimum interval is {interval} seconds.'
AUTOMATION_INVALID_RRULE = lambda err='': f'Invalid RRULE: {err}'
AUTOMATION_NO_FUTURE_RUNS = 'RRULE has no future occurrences'
AUTOMATION_COUNT_REQUIRES_DTSTART = (
'RRULE with COUNT requires an explicit DTSTART line to anchor the occurrence window'
)
FEATURE_DISABLED = lambda name='': f'{name} is disabled'
INPUT_TOO_LONG = lambda size='': f'Input prompt exceeds maximum length of {size}'
# LICENSE covers this Open WebUI error identifier.
# Do not alter, remove, obscure, or replace it except as LICENSE permits:
# https://docs.openwebui.com/license.
SERVER_CONNECTION_ERROR = 'Open WebUI: Server Connection Error'
REQUIRED_FIELD_EMPTY = lambda name='': f'Required field {name} is empty'
OAUTH_NOT_CONFIGURED = lambda name='': f"Provider '{name}' is not configured"

File diff suppressed because it is too large Load diff

1273
backend/open_webui/events.py Normal file

File diff suppressed because it is too large Load diff

View file

@ -1,11 +1,9 @@
import asyncio
import inspect
import logging
import sys
import inspect
import json
import asyncio
from pydantic import BaseModel
from typing import AsyncGenerator, Generator, Iterator
from fastapi import (
Depends,
FastAPI,
@ -16,40 +14,36 @@ from fastapi import (
UploadFile,
status,
)
from pydantic import BaseModel
from starlette.responses import Response, StreamingResponse
from open_webui.config import BYPASS_ADMIN_ACCESS_CONTROL
from open_webui.constants import ERROR_MESSAGES
from open_webui.env import BYPASS_MODEL_ACCESS_CONTROL, ENABLE_PLUGINS, GLOBAL_LOG_LEVEL
from open_webui.models.functions import Functions
from open_webui.models.models import Models
from open_webui.models.users import UserModel
from open_webui.socket.main import (
get_event_call,
get_event_emitter,
)
from open_webui.models.users import UserModel
from open_webui.models.functions import Functions
from open_webui.models.models import Models
from open_webui.utils.plugin import (
load_function_module_by_id,
get_function_module_from_cache,
)
from open_webui.utils.access_control import check_model_access
from open_webui.env import GLOBAL_LOG_LEVEL, BYPASS_MODEL_ACCESS_CONTROL
from open_webui.config import BYPASS_ADMIN_ACCESS_CONTROL
from open_webui.utils.json_codec import JSONCodec
from open_webui.utils.misc import (
add_or_update_system_message,
get_last_user_message,
prepend_to_first_user_message_content,
openai_chat_chunk_message_template,
openai_chat_completion_message_template,
prepend_to_first_user_message_content,
)
from open_webui.utils.payload import (
apply_model_params_to_body_openai,
apply_system_prompt_to_body,
)
from open_webui.utils.plugin import (
get_function_module_from_cache,
load_function_module_by_id,
)
logging.basicConfig(stream=sys.stdout, level=GLOBAL_LOG_LEVEL)
log = logging.getLogger(__name__)
@ -75,6 +69,9 @@ async def get_function_module_by_id(request: Request, pipe_id: str):
async def get_function_models(request):
if not ENABLE_PLUGINS:
return []
pipes = await Functions.get_functions_by_type('pipe', active_only=True)
pipe_models = []
@ -103,7 +100,7 @@ async def get_function_models(request):
log.exception(e)
sub_pipes = []
log.debug(f"get_function_models: function '{pipe.id}' is a manifold of {sub_pipes}")
log.debug("get_function_models: function '%s' is a manifold of %s", pipe.id, sub_pipes)
for p in sub_pipes:
sub_pipe_id = f'{pipe.id}.{p["id"]}'
@ -129,7 +126,10 @@ async def get_function_models(request):
pipe_flag = {'type': 'pipe'}
log.debug(
f"get_function_models: function '{pipe.id}' is a single pipe {{ 'id': {pipe.id}, 'name': {pipe.name} }}"
"get_function_models: function '%s' is a single pipe { 'id': %s, 'name': %s }",
pipe.id,
pipe.id,
pipe.name,
)
pipe_models.append(
@ -150,7 +150,10 @@ async def get_function_models(request):
return pipe_models
async def generate_function_chat_completion(request, form_data, user, models: dict = {}):
async def generate_function_chat_completion(request, form_data, user, models: dict | None = None):
if models is None:
models = {}
async def execute_pipe(pipe, params):
if inspect.iscoroutinefunction(pipe):
return await pipe(**params)
@ -170,7 +173,7 @@ async def generate_function_chat_completion(request, form_data, user, models: di
line = line.model_dump_json()
line = f'data: {line}'
if isinstance(line, dict):
line = f'data: {json.dumps(line)}'
line = f'data: {JSONCodec.dumps(line)}'
try:
line = line.decode('utf-8')
@ -181,7 +184,7 @@ async def generate_function_chat_completion(request, form_data, user, models: di
return f'{line}\n\n'
else:
line = openai_chat_chunk_message_template(form_data['model'], line)
return f'data: {json.dumps(line)}\n\n'
return f'data: {JSONCodec.dumps(line)}\n\n'
def get_pipe_id(form_data: dict) -> str:
pipe_id = form_data['model']
@ -209,6 +212,13 @@ async def generate_function_chat_completion(request, form_data, user, models: di
return params
# Set server-side by utils/chat.py, never by client input. Mirrors the routers.
bypass_system_prompt = getattr(request.state, 'bypass_system_prompt', False)
# Copy so the base-model substitution below doesn't leak into the caller's
# payload, which the tool-call continuation re-submits. Mirrors the routers.
form_data = {**form_data}
model_id = form_data.get('model')
model_info = await Models.get_model_by_id(model_id)
@ -284,7 +294,8 @@ async def generate_function_chat_completion(request, form_data, user, models: di
if params:
system = params.pop('system', None)
form_data = apply_model_params_to_body_openai(params, form_data)
form_data = await apply_system_prompt_to_body(system, form_data, metadata, user)
if not bypass_system_prompt:
form_data = await apply_system_prompt_to_body(system, form_data, metadata, user)
pipe_id = get_pipe_id(form_data)
function_module = await get_function_module_by_id(request, pipe_id)
@ -304,17 +315,17 @@ async def generate_function_chat_completion(request, form_data, user, models: di
yield data
return
if isinstance(res, dict):
yield f'data: {json.dumps(res)}\n\n'
yield f'data: {JSONCodec.dumps(res)}\n\n'
return
except Exception as e:
log.error(f'Error: {e}')
yield f'data: {json.dumps({"error": {"detail": str(e)}})}\n\n'
yield f'data: {JSONCodec.dumps({"error": {"detail": str(e)}})}\n\n'
return
if isinstance(res, str):
message = openai_chat_chunk_message_template(form_data['model'], res)
yield f'data: {json.dumps(message)}\n\n'
yield f'data: {JSONCodec.dumps(message)}\n\n'
if isinstance(res, Iterator):
for line in res:
@ -324,11 +335,10 @@ async def generate_function_chat_completion(request, form_data, user, models: di
async for line in res:
yield process_line(form_data, line)
if isinstance(res, str) or isinstance(res, Generator):
finish_message = openai_chat_chunk_message_template(form_data['model'], '')
finish_message['choices'][0]['finish_reason'] = 'stop'
yield f'data: {json.dumps(finish_message)}\n\n'
yield 'data: [DONE]'
finish_message = openai_chat_chunk_message_template(form_data['model'], '')
finish_message['choices'][0]['finish_reason'] = 'stop'
yield f'data: {JSONCodec.dumps(finish_message)}\n\n'
yield 'data: [DONE]'
return StreamingResponse(stream_content(), media_type='text/event-stream')
else:

View file

@ -1,36 +1,39 @@
from __future__ import annotations
import logging
import os
import sys
import json
import logging
from contextlib import asynccontextmanager, contextmanager
from datetime import datetime, timedelta, timezone
from typing import Any, Optional
from urllib.parse import parse_qs, urlencode, urlparse, urlunparse
from open_webui.internal.wrappers import register_connection
from open_webui.env import (
OPEN_WEBUI_DIR,
DATABASE_URL,
DATABASE_SCHEMA,
DATABASE_ENABLE_IAM_TOKEN_AUTH,
DATABASE_ENABLE_SESSION_SHARING,
DATABASE_ENABLE_SQLITE_WAL,
DATABASE_POOL_MAX_OVERFLOW,
DATABASE_POOL_RECYCLE,
DATABASE_POOL_SIZE,
DATABASE_POOL_TIMEOUT,
DATABASE_ENABLE_SQLITE_WAL,
DATABASE_ENABLE_SESSION_SHARING,
DATABASE_SQLITE_PRAGMA_SYNCHRONOUS,
DATABASE_SCHEMA,
DATABASE_SQLITE_PRAGMA_BUSY_TIMEOUT,
DATABASE_SQLITE_PRAGMA_CACHE_SIZE,
DATABASE_SQLITE_PRAGMA_TEMP_STORE,
DATABASE_SQLITE_PRAGMA_MMAP_SIZE,
DATABASE_SQLITE_PRAGMA_JOURNAL_SIZE_LIMIT,
DATABASE_SQLITE_PRAGMA_MMAP_SIZE,
DATABASE_SQLITE_PRAGMA_SYNCHRONOUS,
DATABASE_SQLITE_PRAGMA_TEMP_STORE,
DATABASE_URL,
ENABLE_DB_MIGRATIONS,
OPEN_WEBUI_DIR,
)
from peewee_migrate import Router
from sqlalchemy import Dialect, create_engine, MetaData, event, types
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker
from open_webui.utils.json_codec import JSONCodec
from sqlalchemy import Dialect, MetaData, create_engine, event, types
from sqlalchemy.engine.url import make_url
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import scoped_session, sessionmaker, Session
from sqlalchemy.pool import QueuePool, NullPool
from sqlalchemy.orm import Session, scoped_session, sessionmaker
from sqlalchemy.pool import NullPool, QueuePool
from sqlalchemy.sql.type_api import _T
from typing_extensions import Self
@ -117,61 +120,25 @@ extract_ssl_mode_from_url = extract_ssl_params_from_url
reattach_ssl_mode_to_url = reattach_ssl_params_to_url
class JSONField(types.TypeDecorator):
impl = types.Text
class JSONField(types.TypeDecorator): # TEXT-backed JSON storage
"""Store arbitrary Python objects as JSON-encoded TEXT.
Used instead of native JSON columns for portability across SQLite and
PostgreSQL. Values are serialized with ``JSONCodec.dumps`` on write and
deserialized with ``JSONCodec.loads`` on read.
"""
impl = types.UnicodeText
cache_ok = True
def process_bind_param(self, value: Optional[_T], dialect: Dialect) -> Any:
return json.dumps(value)
def process_bind_param(self, value: _T | None, dialect: Dialect) -> Any:
return JSONCodec.dumps(value) if value is not None else None
def process_result_value(self, value: Optional[_T], dialect: Dialect) -> Any:
if value is not None:
return json.loads(value)
def process_result_value(self, value: _T | None, dialect: Dialect) -> Any:
return JSONCodec.loads(value) if value is not None else None
def copy(self, **kw: Any) -> Self:
return JSONField(self.impl.length)
def db_value(self, value):
return json.dumps(value)
def python_value(self, value):
if value is not None:
return json.loads(value)
# Workaround to handle the peewee migration
# This is required to ensure the peewee migration is handled before the alembic migration
def handle_peewee_migration(DATABASE_URL):
db = None
try:
# Normalize SSL params so psycopg2 always sees `sslmode=` (never `ssl=`)
# and cert-file params are preserved in the connection string.
url_without_ssl, ssl_params = extract_ssl_params_from_url(DATABASE_URL)
normalized_url = reattach_ssl_params_to_url(url_without_ssl, ssl_params)
# Replace the postgresql:// with postgres:// to handle the peewee migration
db = register_connection(normalized_url.replace('postgresql://', 'postgres://'))
migrate_dir = OPEN_WEBUI_DIR / 'internal' / 'migrations'
router = Router(db, logger=log, migrate_dir=migrate_dir)
router.run()
db.close()
except Exception as e:
log.error(f'Failed to initialize the database connection: {e}')
log.warning('Hint: If your database password contains special characters, you may need to URL-encode it.')
raise
finally:
# Properly closing the database connection
if db and not db.is_closed():
db.close()
# Assert if db connection has been closed
if db is not None:
assert db.is_closed(), 'Database connection is still open.'
if ENABLE_DB_MIGRATIONS:
handle_peewee_migration(DATABASE_URL)
def copy(self, **kwargs: Any) -> Self:
return JSONField(length=self.impl.length)
# Normalize SSL params from the URL once; the sync engine needs them
@ -182,6 +149,77 @@ _url_without_ssl, _ssl_dict = extract_ssl_params_from_url(DATABASE_URL)
SQLALCHEMY_DATABASE_URL = reattach_ssl_params_to_url(_url_without_ssl, _ssl_dict) if _ssl_dict else DATABASE_URL
class RDSIAMTokenAuth:
_refresh_after = timedelta(minutes=14)
def __init__(self, database_url: str) -> None:
url = make_url(database_url)
if not url.drivername.startswith(('postgresql', 'postgres')):
raise ValueError('DATABASE_ENABLE_IAM_TOKEN_AUTH is only supported for PostgreSQL databases')
if not url.host or not url.username:
raise ValueError('DATABASE_ENABLE_IAM_TOKEN_AUTH requires a database host and user')
self.host = url.host
self.port = url.port or 5432
self.username = url.username
self._client = None
self._token: str | None = None
self._expires_at = datetime.min.replace(tzinfo=timezone.utc)
@property
def client(self):
if self._client is None:
import boto3
self._client = boto3.client('rds')
return self._client
def get_password(self) -> str:
now = datetime.now(timezone.utc)
if self._token and now < self._expires_at:
return self._token
self._token = self.client.generate_db_auth_token(
DBHostname=self.host,
Port=self.port,
DBUsername=self.username,
)
self._expires_at = now + self._refresh_after
log.info('AWS RDS IAM database token refreshed; next refresh after %s', self._expires_at.isoformat())
return self._token
_rds_iam_token_auth = RDSIAMTokenAuth(SQLALCHEMY_DATABASE_URL) if DATABASE_ENABLE_IAM_TOKEN_AUTH else None
def _set_iam_token_password(dialect, conn_rec, cargs, cparams):
if _rds_iam_token_auth is not None:
cparams['password'] = _rds_iam_token_auth.get_password()
def enable_iam_token_auth(connectable) -> None:
if _rds_iam_token_auth is None:
return
engine = getattr(connectable, 'sync_engine', connectable)
url = engine.url
auth = _rds_iam_token_auth
# The token is bound to one host/port/user pair; leave other databases on their own credentials.
if (url.host, url.port or 5432, url.username) != (auth.host, auth.port, auth.username):
log.warning(
'AWS RDS IAM token auth not applied to %s: the token is issued for %s@%s:%s, '
'so this connection uses the password from its own URL',
url.render_as_string(hide_password=True),
auth.username,
auth.host,
auth.port,
)
return
if not event.contains(engine, 'do_connect', _set_iam_token_password):
event.listen(engine, 'do_connect', _set_iam_token_password)
def _make_async_url(url: str) -> str:
"""Convert a sync database URL to its async driver equivalent.
@ -208,6 +246,27 @@ def _make_async_url(url: str) -> str:
return url
def _json_codec_kwargs(kwargs: dict) -> dict:
"""Default an engine to JSONCodec for native ``JSON`` columns.
Unlike ``JSONField``, those serialize through the engine, which otherwise uses
stdlib ``json``. With ``ENABLE_ORJSON`` off JSONCodec is stdlib ``json`` anyway.
"""
kwargs.setdefault('json_serializer', JSONCodec.dumps)
kwargs.setdefault('json_deserializer', JSONCodec.loads)
return kwargs
def _create_engine(*args, **kwargs):
"""``create_engine`` with the app JSON codec wired in."""
return create_engine(*args, **_json_codec_kwargs(kwargs))
def _create_async_engine(*args, **kwargs):
"""``create_async_engine`` with the app JSON codec wired in."""
return create_async_engine(*args, **_json_codec_kwargs(kwargs))
# ============================================================
# SYNC ENGINE (used only for: startup migrations, config loading,
# Alembic, peewee migration, health checks)
@ -236,7 +295,7 @@ if SQLALCHEMY_DATABASE_URL.startswith('sqlite+sqlcipher://'):
# in the native sqlcipher3 C library. Use NullPool by default for safety,
# or QueuePool if DATABASE_POOL_SIZE is explicitly configured.
if isinstance(DATABASE_POOL_SIZE, int) and DATABASE_POOL_SIZE > 0:
engine = create_engine(
engine = _create_engine(
'sqlite://',
creator=create_sqlcipher_connection,
pool_size=DATABASE_POOL_SIZE,
@ -248,7 +307,7 @@ if SQLALCHEMY_DATABASE_URL.startswith('sqlite+sqlcipher://'):
echo=False,
)
else:
engine = create_engine(
engine = _create_engine(
'sqlite://',
creator=create_sqlcipher_connection,
poolclass=NullPool,
@ -258,7 +317,7 @@ if SQLALCHEMY_DATABASE_URL.startswith('sqlite+sqlcipher://'):
log.info('Connected to encrypted SQLite database using SQLCipher')
elif 'sqlite' in SQLALCHEMY_DATABASE_URL:
engine = create_engine(SQLALCHEMY_DATABASE_URL, connect_args={'check_same_thread': False})
engine = _create_engine(SQLALCHEMY_DATABASE_URL, connect_args={'check_same_thread': False})
def _apply_sqlite_pragmas(dbapi_connection):
"""Apply all configured SQLite PRAGMAs to a raw DBAPI connection."""
@ -290,7 +349,7 @@ elif 'sqlite' in SQLALCHEMY_DATABASE_URL:
else:
if isinstance(DATABASE_POOL_SIZE, int):
if DATABASE_POOL_SIZE > 0:
engine = create_engine(
engine = _create_engine(
SQLALCHEMY_DATABASE_URL,
pool_size=DATABASE_POOL_SIZE,
max_overflow=DATABASE_POOL_MAX_OVERFLOW,
@ -300,9 +359,11 @@ else:
poolclass=QueuePool,
)
else:
engine = create_engine(SQLALCHEMY_DATABASE_URL, pool_pre_ping=True, poolclass=NullPool)
engine = _create_engine(SQLALCHEMY_DATABASE_URL, pool_pre_ping=True, poolclass=NullPool)
else:
engine = create_engine(SQLALCHEMY_DATABASE_URL, pool_pre_ping=True)
engine = _create_engine(SQLALCHEMY_DATABASE_URL, pool_pre_ping=True)
enable_iam_token_auth(engine)
# Sync session — used ONLY for startup config loading (config.py runs at import time)
@ -344,14 +405,15 @@ if sys.platform == 'win32' and _is_postgres_url(DATABASE_URL):
if 'sqlite' in ASYNC_SQLALCHEMY_DATABASE_URL:
# Generous default — async coroutines + no session sharing = high connection demand.
# No pool_pre_ping: a local SQLite file cannot drop connections, and the
# ping costs a worker-thread hop plus a SELECT 1 on every checkout.
_sqlite_pool_size = DATABASE_POOL_SIZE if isinstance(DATABASE_POOL_SIZE, int) and DATABASE_POOL_SIZE > 0 else 512
async_engine = create_async_engine(
async_engine = _create_async_engine(
ASYNC_SQLALCHEMY_DATABASE_URL,
connect_args={'check_same_thread': False},
pool_size=_sqlite_pool_size,
pool_timeout=DATABASE_POOL_TIMEOUT,
pool_recycle=DATABASE_POOL_RECYCLE,
pool_pre_ping=True,
)
@event.listens_for(async_engine.sync_engine, 'connect')
@ -360,7 +422,7 @@ if 'sqlite' in ASYNC_SQLALCHEMY_DATABASE_URL:
else:
if isinstance(DATABASE_POOL_SIZE, int):
if DATABASE_POOL_SIZE > 0:
async_engine = create_async_engine(
async_engine = _create_async_engine(
ASYNC_SQLALCHEMY_DATABASE_URL,
pool_size=DATABASE_POOL_SIZE,
max_overflow=DATABASE_POOL_MAX_OVERFLOW,
@ -369,17 +431,19 @@ else:
pool_pre_ping=True,
)
else:
async_engine = create_async_engine(
async_engine = _create_async_engine(
ASYNC_SQLALCHEMY_DATABASE_URL,
pool_pre_ping=True,
poolclass=NullPool,
)
else:
async_engine = create_async_engine(
async_engine = _create_async_engine(
ASYNC_SQLALCHEMY_DATABASE_URL,
pool_pre_ping=True,
)
enable_iam_token_auth(async_engine)
AsyncSessionLocal = async_sessionmaker(
bind=async_engine,
@ -410,7 +474,7 @@ async def get_async_db():
@asynccontextmanager
async def get_async_db_context(db: Optional[AsyncSession] = None):
async def get_async_db_context(db: AsyncSession | None = None):
"""Async context manager that reuses an existing session if provided and session sharing is enabled."""
if isinstance(db, AsyncSession) and DATABASE_ENABLE_SESSION_SHARING:
yield db

View file

@ -1,253 +0,0 @@
"""Peewee migrations -- 001_initial_schema.py.
Some examples (model - class or model name)::
> Model = migrator.orm['table_name'] # Return model in current state by name
> Model = migrator.ModelClass # Return model in current state by name
> migrator.sql(sql) # Run custom SQL
> migrator.run(func, *args, **kwargs) # Run python function with the given args
> migrator.create_model(Model) # Create a model (could be used as decorator)
> migrator.remove_model(model, cascade=True) # Remove a model
> migrator.add_fields(model, **fields) # Add fields to a model
> migrator.change_fields(model, **fields) # Change fields
> migrator.remove_fields(model, *field_names, cascade=True)
> migrator.rename_field(model, old_field_name, new_field_name)
> migrator.rename_table(model, new_table_name)
> migrator.add_index(model, *col_names, unique=False)
> migrator.add_not_null(model, *field_names)
> migrator.add_default(model, field_name, default)
> migrator.add_constraint(model, name, sql)
> migrator.drop_index(model, *col_names)
> migrator.drop_not_null(model, *field_names)
> migrator.drop_constraints(model, *constraints)
"""
from contextlib import suppress
import peewee as pw
from peewee_migrate import Migrator
with suppress(ImportError):
import playhouse.postgres_ext as pw_pext
def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
"""Write your migrations here."""
# We perform different migrations for SQLite and other databases
# This is because SQLite is very loose with enforcing its schema, and trying to migrate other databases like SQLite
# will require per-database SQL queries.
# Instead, we assume that because external DB support was added at a later date, it is safe to assume a newer base
# schema instead of trying to migrate from an older schema.
if isinstance(database, pw.SqliteDatabase):
migrate_sqlite(migrator, database, fake=fake)
else:
migrate_external(migrator, database, fake=fake)
def migrate_sqlite(migrator: Migrator, database: pw.Database, *, fake=False):
@migrator.create_model
class Auth(pw.Model):
id = pw.CharField(max_length=255, unique=True)
email = pw.CharField(max_length=255)
password = pw.CharField(max_length=255)
active = pw.BooleanField()
class Meta:
table_name = 'auth'
@migrator.create_model
class Chat(pw.Model):
id = pw.CharField(max_length=255, unique=True)
user_id = pw.CharField(max_length=255)
title = pw.CharField()
chat = pw.TextField()
timestamp = pw.BigIntegerField()
class Meta:
table_name = 'chat'
@migrator.create_model
class ChatIdTag(pw.Model):
id = pw.CharField(max_length=255, unique=True)
tag_name = pw.CharField(max_length=255)
chat_id = pw.CharField(max_length=255)
user_id = pw.CharField(max_length=255)
timestamp = pw.BigIntegerField()
class Meta:
table_name = 'chatidtag'
@migrator.create_model
class Document(pw.Model):
id = pw.AutoField()
collection_name = pw.CharField(max_length=255, unique=True)
name = pw.CharField(max_length=255, unique=True)
title = pw.CharField()
filename = pw.CharField()
content = pw.TextField(null=True)
user_id = pw.CharField(max_length=255)
timestamp = pw.BigIntegerField()
class Meta:
table_name = 'document'
@migrator.create_model
class Modelfile(pw.Model):
id = pw.AutoField()
tag_name = pw.CharField(max_length=255, unique=True)
user_id = pw.CharField(max_length=255)
modelfile = pw.TextField()
timestamp = pw.BigIntegerField()
class Meta:
table_name = 'modelfile'
@migrator.create_model
class Prompt(pw.Model):
id = pw.AutoField()
command = pw.CharField(max_length=255, unique=True)
user_id = pw.CharField(max_length=255)
title = pw.CharField()
content = pw.TextField()
timestamp = pw.BigIntegerField()
class Meta:
table_name = 'prompt'
@migrator.create_model
class Tag(pw.Model):
id = pw.CharField(max_length=255, unique=True)
name = pw.CharField(max_length=255)
user_id = pw.CharField(max_length=255)
data = pw.TextField(null=True)
class Meta:
table_name = 'tag'
@migrator.create_model
class User(pw.Model):
id = pw.CharField(max_length=255, unique=True)
name = pw.CharField(max_length=255)
email = pw.CharField(max_length=255)
role = pw.CharField(max_length=255)
profile_image_url = pw.CharField(max_length=255)
timestamp = pw.BigIntegerField()
class Meta:
table_name = 'user'
def migrate_external(migrator: Migrator, database: pw.Database, *, fake=False):
@migrator.create_model
class Auth(pw.Model):
id = pw.CharField(max_length=255, unique=True)
email = pw.CharField(max_length=255)
password = pw.TextField()
active = pw.BooleanField()
class Meta:
table_name = 'auth'
@migrator.create_model
class Chat(pw.Model):
id = pw.CharField(max_length=255, unique=True)
user_id = pw.CharField(max_length=255)
title = pw.TextField()
chat = pw.TextField()
timestamp = pw.BigIntegerField()
class Meta:
table_name = 'chat'
@migrator.create_model
class ChatIdTag(pw.Model):
id = pw.CharField(max_length=255, unique=True)
tag_name = pw.CharField(max_length=255)
chat_id = pw.CharField(max_length=255)
user_id = pw.CharField(max_length=255)
timestamp = pw.BigIntegerField()
class Meta:
table_name = 'chatidtag'
@migrator.create_model
class Document(pw.Model):
id = pw.AutoField()
collection_name = pw.CharField(max_length=255, unique=True)
name = pw.CharField(max_length=255, unique=True)
title = pw.TextField()
filename = pw.TextField()
content = pw.TextField(null=True)
user_id = pw.CharField(max_length=255)
timestamp = pw.BigIntegerField()
class Meta:
table_name = 'document'
@migrator.create_model
class Modelfile(pw.Model):
id = pw.AutoField()
tag_name = pw.CharField(max_length=255, unique=True)
user_id = pw.CharField(max_length=255)
modelfile = pw.TextField()
timestamp = pw.BigIntegerField()
class Meta:
table_name = 'modelfile'
@migrator.create_model
class Prompt(pw.Model):
id = pw.AutoField()
command = pw.CharField(max_length=255, unique=True)
user_id = pw.CharField(max_length=255)
title = pw.TextField()
content = pw.TextField()
timestamp = pw.BigIntegerField()
class Meta:
table_name = 'prompt'
@migrator.create_model
class Tag(pw.Model):
id = pw.CharField(max_length=255, unique=True)
name = pw.CharField(max_length=255)
user_id = pw.CharField(max_length=255)
data = pw.TextField(null=True)
class Meta:
table_name = 'tag'
@migrator.create_model
class User(pw.Model):
id = pw.CharField(max_length=255, unique=True)
name = pw.CharField(max_length=255)
email = pw.CharField(max_length=255)
role = pw.CharField(max_length=255)
profile_image_url = pw.TextField()
timestamp = pw.BigIntegerField()
class Meta:
table_name = 'user'
def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
"""Write your rollback migrations here."""
migrator.remove_model('user')
migrator.remove_model('tag')
migrator.remove_model('prompt')
migrator.remove_model('modelfile')
migrator.remove_model('document')
migrator.remove_model('chatidtag')
migrator.remove_model('chat')
migrator.remove_model('auth')

View file

@ -1,45 +0,0 @@
"""Peewee migrations -- 002_add_local_sharing.py.
Some examples (model - class or model name)::
> Model = migrator.orm['table_name'] # Return model in current state by name
> Model = migrator.ModelClass # Return model in current state by name
> migrator.sql(sql) # Run custom SQL
> migrator.run(func, *args, **kwargs) # Run python function with the given args
> migrator.create_model(Model) # Create a model (could be used as decorator)
> migrator.remove_model(model, cascade=True) # Remove a model
> migrator.add_fields(model, **fields) # Add fields to a model
> migrator.change_fields(model, **fields) # Change fields
> migrator.remove_fields(model, *field_names, cascade=True)
> migrator.rename_field(model, old_field_name, new_field_name)
> migrator.rename_table(model, new_table_name)
> migrator.add_index(model, *col_names, unique=False)
> migrator.add_not_null(model, *field_names)
> migrator.add_default(model, field_name, default)
> migrator.add_constraint(model, name, sql)
> migrator.drop_index(model, *col_names)
> migrator.drop_not_null(model, *field_names)
> migrator.drop_constraints(model, *constraints)
"""
from contextlib import suppress
import peewee as pw
from peewee_migrate import Migrator
with suppress(ImportError):
import playhouse.postgres_ext as pw_pext
def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
"""Write your migrations here."""
migrator.add_fields('chat', share_id=pw.CharField(max_length=255, null=True, unique=True))
def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
"""Write your rollback migrations here."""
migrator.remove_fields('chat', 'share_id')

View file

@ -1,45 +0,0 @@
"""Peewee migrations -- 002_add_local_sharing.py.
Some examples (model - class or model name)::
> Model = migrator.orm['table_name'] # Return model in current state by name
> Model = migrator.ModelClass # Return model in current state by name
> migrator.sql(sql) # Run custom SQL
> migrator.run(func, *args, **kwargs) # Run python function with the given args
> migrator.create_model(Model) # Create a model (could be used as decorator)
> migrator.remove_model(model, cascade=True) # Remove a model
> migrator.add_fields(model, **fields) # Add fields to a model
> migrator.change_fields(model, **fields) # Change fields
> migrator.remove_fields(model, *field_names, cascade=True)
> migrator.rename_field(model, old_field_name, new_field_name)
> migrator.rename_table(model, new_table_name)
> migrator.add_index(model, *col_names, unique=False)
> migrator.add_not_null(model, *field_names)
> migrator.add_default(model, field_name, default)
> migrator.add_constraint(model, name, sql)
> migrator.drop_index(model, *col_names)
> migrator.drop_not_null(model, *field_names)
> migrator.drop_constraints(model, *constraints)
"""
from contextlib import suppress
import peewee as pw
from peewee_migrate import Migrator
with suppress(ImportError):
import playhouse.postgres_ext as pw_pext
def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
"""Write your migrations here."""
migrator.add_fields('user', api_key=pw.CharField(max_length=255, null=True, unique=True))
def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
"""Write your rollback migrations here."""
migrator.remove_fields('user', 'api_key')

View file

@ -1,45 +0,0 @@
"""Peewee migrations -- 002_add_local_sharing.py.
Some examples (model - class or model name)::
> Model = migrator.orm['table_name'] # Return model in current state by name
> Model = migrator.ModelClass # Return model in current state by name
> migrator.sql(sql) # Run custom SQL
> migrator.run(func, *args, **kwargs) # Run python function with the given args
> migrator.create_model(Model) # Create a model (could be used as decorator)
> migrator.remove_model(model, cascade=True) # Remove a model
> migrator.add_fields(model, **fields) # Add fields to a model
> migrator.change_fields(model, **fields) # Change fields
> migrator.remove_fields(model, *field_names, cascade=True)
> migrator.rename_field(model, old_field_name, new_field_name)
> migrator.rename_table(model, new_table_name)
> migrator.add_index(model, *col_names, unique=False)
> migrator.add_not_null(model, *field_names)
> migrator.add_default(model, field_name, default)
> migrator.add_constraint(model, name, sql)
> migrator.drop_index(model, *col_names)
> migrator.drop_not_null(model, *field_names)
> migrator.drop_constraints(model, *constraints)
"""
from contextlib import suppress
import peewee as pw
from peewee_migrate import Migrator
with suppress(ImportError):
import playhouse.postgres_ext as pw_pext
def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
"""Write your migrations here."""
migrator.add_fields('chat', archived=pw.BooleanField(default=False))
def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
"""Write your rollback migrations here."""
migrator.remove_fields('chat', 'archived')

View file

@ -1,125 +0,0 @@
"""Peewee migrations -- 002_add_local_sharing.py.
Some examples (model - class or model name)::
> Model = migrator.orm['table_name'] # Return model in current state by name
> Model = migrator.ModelClass # Return model in current state by name
> migrator.sql(sql) # Run custom SQL
> migrator.run(func, *args, **kwargs) # Run python function with the given args
> migrator.create_model(Model) # Create a model (could be used as decorator)
> migrator.remove_model(model, cascade=True) # Remove a model
> migrator.add_fields(model, **fields) # Add fields to a model
> migrator.change_fields(model, **fields) # Change fields
> migrator.remove_fields(model, *field_names, cascade=True)
> migrator.rename_field(model, old_field_name, new_field_name)
> migrator.rename_table(model, new_table_name)
> migrator.add_index(model, *col_names, unique=False)
> migrator.add_not_null(model, *field_names)
> migrator.add_default(model, field_name, default)
> migrator.add_constraint(model, name, sql)
> migrator.drop_index(model, *col_names)
> migrator.drop_not_null(model, *field_names)
> migrator.drop_constraints(model, *constraints)
"""
from contextlib import suppress
import peewee as pw
from peewee_migrate import Migrator
with suppress(ImportError):
import playhouse.postgres_ext as pw_pext
def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
"""Write your migrations here."""
if isinstance(database, pw.SqliteDatabase):
migrate_sqlite(migrator, database, fake=fake)
else:
migrate_external(migrator, database, fake=fake)
def migrate_sqlite(migrator: Migrator, database: pw.Database, *, fake=False):
# Adding fields created_at and updated_at to the 'chat' table
migrator.add_fields(
'chat',
created_at=pw.DateTimeField(null=True), # Allow null for transition
updated_at=pw.DateTimeField(null=True), # Allow null for transition
)
# Populate the new fields from an existing 'timestamp' field
migrator.sql('UPDATE chat SET created_at = timestamp, updated_at = timestamp WHERE timestamp IS NOT NULL')
# Now that the data has been copied, remove the original 'timestamp' field
migrator.remove_fields('chat', 'timestamp')
# Update the fields to be not null now that they are populated
migrator.change_fields(
'chat',
created_at=pw.DateTimeField(null=False),
updated_at=pw.DateTimeField(null=False),
)
def migrate_external(migrator: Migrator, database: pw.Database, *, fake=False):
# Adding fields created_at and updated_at to the 'chat' table
migrator.add_fields(
'chat',
created_at=pw.BigIntegerField(null=True), # Allow null for transition
updated_at=pw.BigIntegerField(null=True), # Allow null for transition
)
# Populate the new fields from an existing 'timestamp' field
migrator.sql('UPDATE chat SET created_at = timestamp, updated_at = timestamp WHERE timestamp IS NOT NULL')
# Now that the data has been copied, remove the original 'timestamp' field
migrator.remove_fields('chat', 'timestamp')
# Update the fields to be not null now that they are populated
migrator.change_fields(
'chat',
created_at=pw.BigIntegerField(null=False),
updated_at=pw.BigIntegerField(null=False),
)
def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
"""Write your rollback migrations here."""
if isinstance(database, pw.SqliteDatabase):
rollback_sqlite(migrator, database, fake=fake)
else:
rollback_external(migrator, database, fake=fake)
def rollback_sqlite(migrator: Migrator, database: pw.Database, *, fake=False):
# Recreate the timestamp field initially allowing null values for safe transition
migrator.add_fields('chat', timestamp=pw.DateTimeField(null=True))
# Copy the earliest created_at date back into the new timestamp field
# This assumes created_at was originally a copy of timestamp
migrator.sql('UPDATE chat SET timestamp = created_at')
# Remove the created_at and updated_at fields
migrator.remove_fields('chat', 'created_at', 'updated_at')
# Finally, alter the timestamp field to not allow nulls if that was the original setting
migrator.change_fields('chat', timestamp=pw.DateTimeField(null=False))
def rollback_external(migrator: Migrator, database: pw.Database, *, fake=False):
# Recreate the timestamp field initially allowing null values for safe transition
migrator.add_fields('chat', timestamp=pw.BigIntegerField(null=True))
# Copy the earliest created_at date back into the new timestamp field
# This assumes created_at was originally a copy of timestamp
migrator.sql('UPDATE chat SET timestamp = created_at')
# Remove the created_at and updated_at fields
migrator.remove_fields('chat', 'created_at', 'updated_at')
# Finally, alter the timestamp field to not allow nulls if that was the original setting
migrator.change_fields('chat', timestamp=pw.BigIntegerField(null=False))

View file

@ -1,129 +0,0 @@
"""Peewee migrations -- 006_migrate_timestamps_and_charfields.py.
Some examples (model - class or model name)::
> Model = migrator.orm['table_name'] # Return model in current state by name
> Model = migrator.ModelClass # Return model in current state by name
> migrator.sql(sql) # Run custom SQL
> migrator.run(func, *args, **kwargs) # Run python function with the given args
> migrator.create_model(Model) # Create a model (could be used as decorator)
> migrator.remove_model(model, cascade=True) # Remove a model
> migrator.add_fields(model, **fields) # Add fields to a model
> migrator.change_fields(model, **fields) # Change fields
> migrator.remove_fields(model, *field_names, cascade=True)
> migrator.rename_field(model, old_field_name, new_field_name)
> migrator.rename_table(model, new_table_name)
> migrator.add_index(model, *col_names, unique=False)
> migrator.add_not_null(model, *field_names)
> migrator.add_default(model, field_name, default)
> migrator.add_constraint(model, name, sql)
> migrator.drop_index(model, *col_names)
> migrator.drop_not_null(model, *field_names)
> migrator.drop_constraints(model, *constraints)
"""
from contextlib import suppress
import peewee as pw
from peewee_migrate import Migrator
with suppress(ImportError):
import playhouse.postgres_ext as pw_pext
def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
"""Write your migrations here."""
# Alter the tables with timestamps
migrator.change_fields(
'chatidtag',
timestamp=pw.BigIntegerField(),
)
migrator.change_fields(
'document',
timestamp=pw.BigIntegerField(),
)
migrator.change_fields(
'modelfile',
timestamp=pw.BigIntegerField(),
)
migrator.change_fields(
'prompt',
timestamp=pw.BigIntegerField(),
)
migrator.change_fields(
'user',
timestamp=pw.BigIntegerField(),
)
# Alter the tables with varchar to text where necessary
migrator.change_fields(
'auth',
password=pw.TextField(),
)
migrator.change_fields(
'chat',
title=pw.TextField(),
)
migrator.change_fields(
'document',
title=pw.TextField(),
filename=pw.TextField(),
)
migrator.change_fields(
'prompt',
title=pw.TextField(),
)
migrator.change_fields(
'user',
profile_image_url=pw.TextField(),
)
def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
"""Write your rollback migrations here."""
if isinstance(database, pw.SqliteDatabase):
# Alter the tables with timestamps
migrator.change_fields(
'chatidtag',
timestamp=pw.DateField(),
)
migrator.change_fields(
'document',
timestamp=pw.DateField(),
)
migrator.change_fields(
'modelfile',
timestamp=pw.DateField(),
)
migrator.change_fields(
'prompt',
timestamp=pw.DateField(),
)
migrator.change_fields(
'user',
timestamp=pw.DateField(),
)
migrator.change_fields(
'auth',
password=pw.CharField(max_length=255),
)
migrator.change_fields(
'chat',
title=pw.CharField(),
)
migrator.change_fields(
'document',
title=pw.CharField(),
filename=pw.CharField(),
)
migrator.change_fields(
'prompt',
title=pw.CharField(),
)
migrator.change_fields(
'user',
profile_image_url=pw.CharField(),
)

View file

@ -1,78 +0,0 @@
"""Peewee migrations -- 002_add_local_sharing.py.
Some examples (model - class or model name)::
> Model = migrator.orm['table_name'] # Return model in current state by name
> Model = migrator.ModelClass # Return model in current state by name
> migrator.sql(sql) # Run custom SQL
> migrator.run(func, *args, **kwargs) # Run python function with the given args
> migrator.create_model(Model) # Create a model (could be used as decorator)
> migrator.remove_model(model, cascade=True) # Remove a model
> migrator.add_fields(model, **fields) # Add fields to a model
> migrator.change_fields(model, **fields) # Change fields
> migrator.remove_fields(model, *field_names, cascade=True)
> migrator.rename_field(model, old_field_name, new_field_name)
> migrator.rename_table(model, new_table_name)
> migrator.add_index(model, *col_names, unique=False)
> migrator.add_not_null(model, *field_names)
> migrator.add_default(model, field_name, default)
> migrator.add_constraint(model, name, sql)
> migrator.drop_index(model, *col_names)
> migrator.drop_not_null(model, *field_names)
> migrator.drop_constraints(model, *constraints)
"""
from contextlib import suppress
import peewee as pw
from peewee_migrate import Migrator
with suppress(ImportError):
import playhouse.postgres_ext as pw_pext
def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
"""Write your migrations here."""
# Adding fields created_at and updated_at to the 'user' table
migrator.add_fields(
'user',
created_at=pw.BigIntegerField(null=True), # Allow null for transition
updated_at=pw.BigIntegerField(null=True), # Allow null for transition
last_active_at=pw.BigIntegerField(null=True), # Allow null for transition
)
# Populate the new fields from an existing 'timestamp' field
migrator.sql(
'UPDATE "user" SET created_at = timestamp, updated_at = timestamp, last_active_at = timestamp WHERE timestamp IS NOT NULL'
)
# Now that the data has been copied, remove the original 'timestamp' field
migrator.remove_fields('user', 'timestamp')
# Update the fields to be not null now that they are populated
migrator.change_fields(
'user',
created_at=pw.BigIntegerField(null=False),
updated_at=pw.BigIntegerField(null=False),
last_active_at=pw.BigIntegerField(null=False),
)
def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
"""Write your rollback migrations here."""
# Recreate the timestamp field initially allowing null values for safe transition
migrator.add_fields('user', timestamp=pw.BigIntegerField(null=True))
# Copy the earliest created_at date back into the new timestamp field
# This assumes created_at was originally a copy of timestamp
migrator.sql('UPDATE "user" SET timestamp = created_at')
# Remove the created_at and updated_at fields
migrator.remove_fields('user', 'created_at', 'updated_at', 'last_active_at')
# Finally, alter the timestamp field to not allow nulls if that was the original setting
migrator.change_fields('user', timestamp=pw.BigIntegerField(null=False))

View file

@ -1,52 +0,0 @@
"""Peewee migrations -- 002_add_local_sharing.py.
Some examples (model - class or model name)::
> Model = migrator.orm['table_name'] # Return model in current state by name
> Model = migrator.ModelClass # Return model in current state by name
> migrator.sql(sql) # Run custom SQL
> migrator.run(func, *args, **kwargs) # Run python function with the given args
> migrator.create_model(Model) # Create a model (could be used as decorator)
> migrator.remove_model(model, cascade=True) # Remove a model
> migrator.add_fields(model, **fields) # Add fields to a model
> migrator.change_fields(model, **fields) # Change fields
> migrator.remove_fields(model, *field_names, cascade=True)
> migrator.rename_field(model, old_field_name, new_field_name)
> migrator.rename_table(model, new_table_name)
> migrator.add_index(model, *col_names, unique=False)
> migrator.add_not_null(model, *field_names)
> migrator.add_default(model, field_name, default)
> migrator.add_constraint(model, name, sql)
> migrator.drop_index(model, *col_names)
> migrator.drop_not_null(model, *field_names)
> migrator.drop_constraints(model, *constraints)
"""
from contextlib import suppress
import peewee as pw
from peewee_migrate import Migrator
with suppress(ImportError):
import playhouse.postgres_ext as pw_pext
def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
@migrator.create_model
class Memory(pw.Model):
id = pw.CharField(max_length=255, unique=True)
user_id = pw.CharField(max_length=255)
content = pw.TextField(null=False)
updated_at = pw.BigIntegerField(null=False)
created_at = pw.BigIntegerField(null=False)
class Meta:
table_name = 'memory'
def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
"""Write your rollback migrations here."""
migrator.remove_model('memory')

View file

@ -1,60 +0,0 @@
"""Peewee migrations -- 009_add_models.py.
Some examples (model - class or model name)::
> Model = migrator.orm['table_name'] # Return model in current state by name
> Model = migrator.ModelClass # Return model in current state by name
> migrator.sql(sql) # Run custom SQL
> migrator.run(func, *args, **kwargs) # Run python function with the given args
> migrator.create_model(Model) # Create a model (could be used as decorator)
> migrator.remove_model(model, cascade=True) # Remove a model
> migrator.add_fields(model, **fields) # Add fields to a model
> migrator.change_fields(model, **fields) # Change fields
> migrator.remove_fields(model, *field_names, cascade=True)
> migrator.rename_field(model, old_field_name, new_field_name)
> migrator.rename_table(model, new_table_name)
> migrator.add_index(model, *col_names, unique=False)
> migrator.add_not_null(model, *field_names)
> migrator.add_default(model, field_name, default)
> migrator.add_constraint(model, name, sql)
> migrator.drop_index(model, *col_names)
> migrator.drop_not_null(model, *field_names)
> migrator.drop_constraints(model, *constraints)
"""
from contextlib import suppress
import peewee as pw
from peewee_migrate import Migrator
with suppress(ImportError):
import playhouse.postgres_ext as pw_pext
def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
"""Write your migrations here."""
@migrator.create_model
class Model(pw.Model):
id = pw.TextField(unique=True)
user_id = pw.TextField()
base_model_id = pw.TextField(null=True)
name = pw.TextField()
meta = pw.TextField()
params = pw.TextField()
created_at = pw.BigIntegerField(null=False)
updated_at = pw.BigIntegerField(null=False)
class Meta:
table_name = 'model'
def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
"""Write your rollback migrations here."""
migrator.remove_model('model')

View file

@ -1,130 +0,0 @@
"""Peewee migrations -- 009_add_models.py.
Some examples (model - class or model name)::
> Model = migrator.orm['table_name'] # Return model in current state by name
> Model = migrator.ModelClass # Return model in current state by name
> migrator.sql(sql) # Run custom SQL
> migrator.run(func, *args, **kwargs) # Run python function with the given args
> migrator.create_model(Model) # Create a model (could be used as decorator)
> migrator.remove_model(model, cascade=True) # Remove a model
> migrator.add_fields(model, **fields) # Add fields to a model
> migrator.change_fields(model, **fields) # Change fields
> migrator.remove_fields(model, *field_names, cascade=True)
> migrator.rename_field(model, old_field_name, new_field_name)
> migrator.rename_table(model, new_table_name)
> migrator.add_index(model, *col_names, unique=False)
> migrator.add_not_null(model, *field_names)
> migrator.add_default(model, field_name, default)
> migrator.add_constraint(model, name, sql)
> migrator.drop_index(model, *col_names)
> migrator.drop_not_null(model, *field_names)
> migrator.drop_constraints(model, *constraints)
"""
from contextlib import suppress
import peewee as pw
from peewee_migrate import Migrator
import json
from open_webui.utils.misc import parse_ollama_modelfile
with suppress(ImportError):
import playhouse.postgres_ext as pw_pext
def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
"""Write your migrations here."""
# Fetch data from 'modelfile' table and insert into 'model' table
migrate_modelfile_to_model(migrator, database)
# Drop the 'modelfile' table
migrator.remove_model('modelfile')
def migrate_modelfile_to_model(migrator: Migrator, database: pw.Database):
ModelFile = migrator.orm['modelfile']
Model = migrator.orm['model']
modelfiles = ModelFile.select()
for modelfile in modelfiles:
# Extract and transform data in Python
modelfile.modelfile = json.loads(modelfile.modelfile)
meta = json.dumps(
{
'description': modelfile.modelfile.get('desc'),
'profile_image_url': modelfile.modelfile.get('imageUrl'),
'ollama': {'modelfile': modelfile.modelfile.get('content')},
'suggestion_prompts': modelfile.modelfile.get('suggestionPrompts'),
'categories': modelfile.modelfile.get('categories'),
'user': {**modelfile.modelfile.get('user', {}), 'community': True},
}
)
info = parse_ollama_modelfile(modelfile.modelfile.get('content'))
# Insert the processed data into the 'model' table
Model.create(
id=f'ollama-{modelfile.tag_name}',
user_id=modelfile.user_id,
base_model_id=info.get('base_model_id'),
name=modelfile.modelfile.get('title'),
meta=meta,
params=json.dumps(info.get('params', {})),
created_at=modelfile.timestamp,
updated_at=modelfile.timestamp,
)
def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
"""Write your rollback migrations here."""
recreate_modelfile_table(migrator, database)
move_data_back_to_modelfile(migrator, database)
migrator.remove_model('model')
def recreate_modelfile_table(migrator: Migrator, database: pw.Database):
query = """
CREATE TABLE IF NOT EXISTS modelfile (
user_id TEXT,
tag_name TEXT,
modelfile JSON,
timestamp BIGINT
)
"""
migrator.sql(query)
def move_data_back_to_modelfile(migrator: Migrator, database: pw.Database):
Model = migrator.orm['model']
Modelfile = migrator.orm['modelfile']
models = Model.select()
for model in models:
# Extract and transform data in Python
meta = json.loads(model.meta)
modelfile_data = {
'title': model.name,
'desc': meta.get('description'),
'imageUrl': meta.get('profile_image_url'),
'content': meta.get('ollama', {}).get('modelfile'),
'suggestionPrompts': meta.get('suggestion_prompts'),
'categories': meta.get('categories'),
'user': {k: v for k, v in meta.get('user', {}).items() if k != 'community'},
}
# Insert the processed data back into the 'modelfile' table
Modelfile.create(
user_id=model.user_id,
tag_name=model.id,
modelfile=modelfile_data,
timestamp=model.created_at,
)

View file

@ -1,47 +0,0 @@
"""Peewee migrations -- 002_add_local_sharing.py.
Some examples (model - class or model name)::
> Model = migrator.orm['table_name'] # Return model in current state by name
> Model = migrator.ModelClass # Return model in current state by name
> migrator.sql(sql) # Run custom SQL
> migrator.run(func, *args, **kwargs) # Run python function with the given args
> migrator.create_model(Model) # Create a model (could be used as decorator)
> migrator.remove_model(model, cascade=True) # Remove a model
> migrator.add_fields(model, **fields) # Add fields to a model
> migrator.change_fields(model, **fields) # Change fields
> migrator.remove_fields(model, *field_names, cascade=True)
> migrator.rename_field(model, old_field_name, new_field_name)
> migrator.rename_table(model, new_table_name)
> migrator.add_index(model, *col_names, unique=False)
> migrator.add_not_null(model, *field_names)
> migrator.add_default(model, field_name, default)
> migrator.add_constraint(model, name, sql)
> migrator.drop_index(model, *col_names)
> migrator.drop_not_null(model, *field_names)
> migrator.drop_constraints(model, *constraints)
"""
from contextlib import suppress
import peewee as pw
from peewee_migrate import Migrator
with suppress(ImportError):
import playhouse.postgres_ext as pw_pext
def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
"""Write your migrations here."""
# Adding fields settings to the 'user' table
migrator.add_fields('user', settings=pw.TextField(null=True))
def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
"""Write your rollback migrations here."""
# Remove the settings field
migrator.remove_fields('user', 'settings')

View file

@ -1,60 +0,0 @@
"""Peewee migrations -- 009_add_models.py.
Some examples (model - class or model name)::
> Model = migrator.orm['table_name'] # Return model in current state by name
> Model = migrator.ModelClass # Return model in current state by name
> migrator.sql(sql) # Run custom SQL
> migrator.run(func, *args, **kwargs) # Run python function with the given args
> migrator.create_model(Model) # Create a model (could be used as decorator)
> migrator.remove_model(model, cascade=True) # Remove a model
> migrator.add_fields(model, **fields) # Add fields to a model
> migrator.change_fields(model, **fields) # Change fields
> migrator.remove_fields(model, *field_names, cascade=True)
> migrator.rename_field(model, old_field_name, new_field_name)
> migrator.rename_table(model, new_table_name)
> migrator.add_index(model, *col_names, unique=False)
> migrator.add_not_null(model, *field_names)
> migrator.add_default(model, field_name, default)
> migrator.add_constraint(model, name, sql)
> migrator.drop_index(model, *col_names)
> migrator.drop_not_null(model, *field_names)
> migrator.drop_constraints(model, *constraints)
"""
from contextlib import suppress
import peewee as pw
from peewee_migrate import Migrator
with suppress(ImportError):
import playhouse.postgres_ext as pw_pext
def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
"""Write your migrations here."""
@migrator.create_model
class Tool(pw.Model):
id = pw.TextField(unique=True)
user_id = pw.TextField()
name = pw.TextField()
content = pw.TextField()
specs = pw.TextField()
meta = pw.TextField()
created_at = pw.BigIntegerField(null=False)
updated_at = pw.BigIntegerField(null=False)
class Meta:
table_name = 'tool'
def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
"""Write your rollback migrations here."""
migrator.remove_model('tool')

View file

@ -1,47 +0,0 @@
"""Peewee migrations -- 002_add_local_sharing.py.
Some examples (model - class or model name)::
> Model = migrator.orm['table_name'] # Return model in current state by name
> Model = migrator.ModelClass # Return model in current state by name
> migrator.sql(sql) # Run custom SQL
> migrator.run(func, *args, **kwargs) # Run python function with the given args
> migrator.create_model(Model) # Create a model (could be used as decorator)
> migrator.remove_model(model, cascade=True) # Remove a model
> migrator.add_fields(model, **fields) # Add fields to a model
> migrator.change_fields(model, **fields) # Change fields
> migrator.remove_fields(model, *field_names, cascade=True)
> migrator.rename_field(model, old_field_name, new_field_name)
> migrator.rename_table(model, new_table_name)
> migrator.add_index(model, *col_names, unique=False)
> migrator.add_not_null(model, *field_names)
> migrator.add_default(model, field_name, default)
> migrator.add_constraint(model, name, sql)
> migrator.drop_index(model, *col_names)
> migrator.drop_not_null(model, *field_names)
> migrator.drop_constraints(model, *constraints)
"""
from contextlib import suppress
import peewee as pw
from peewee_migrate import Migrator
with suppress(ImportError):
import playhouse.postgres_ext as pw_pext
def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
"""Write your migrations here."""
# Adding fields info to the 'user' table
migrator.add_fields('user', info=pw.TextField(null=True))
def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
"""Write your rollback migrations here."""
# Remove the settings field
migrator.remove_fields('user', 'info')

View file

@ -1,54 +0,0 @@
"""Peewee migrations -- 009_add_models.py.
Some examples (model - class or model name)::
> Model = migrator.orm['table_name'] # Return model in current state by name
> Model = migrator.ModelClass # Return model in current state by name
> migrator.sql(sql) # Run custom SQL
> migrator.run(func, *args, **kwargs) # Run python function with the given args
> migrator.create_model(Model) # Create a model (could be used as decorator)
> migrator.remove_model(model, cascade=True) # Remove a model
> migrator.add_fields(model, **fields) # Add fields to a model
> migrator.change_fields(model, **fields) # Change fields
> migrator.remove_fields(model, *field_names, cascade=True)
> migrator.rename_field(model, old_field_name, new_field_name)
> migrator.rename_table(model, new_table_name)
> migrator.add_index(model, *col_names, unique=False)
> migrator.add_not_null(model, *field_names)
> migrator.add_default(model, field_name, default)
> migrator.add_constraint(model, name, sql)
> migrator.drop_index(model, *col_names)
> migrator.drop_not_null(model, *field_names)
> migrator.drop_constraints(model, *constraints)
"""
from contextlib import suppress
import peewee as pw
from peewee_migrate import Migrator
with suppress(ImportError):
import playhouse.postgres_ext as pw_pext
def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
"""Write your migrations here."""
@migrator.create_model
class File(pw.Model):
id = pw.TextField(unique=True)
user_id = pw.TextField()
filename = pw.TextField()
meta = pw.TextField()
created_at = pw.BigIntegerField(null=False)
class Meta:
table_name = 'file'
def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
"""Write your rollback migrations here."""
migrator.remove_model('file')

View file

@ -1,60 +0,0 @@
"""Peewee migrations -- 009_add_models.py.
Some examples (model - class or model name)::
> Model = migrator.orm['table_name'] # Return model in current state by name
> Model = migrator.ModelClass # Return model in current state by name
> migrator.sql(sql) # Run custom SQL
> migrator.run(func, *args, **kwargs) # Run python function with the given args
> migrator.create_model(Model) # Create a model (could be used as decorator)
> migrator.remove_model(model, cascade=True) # Remove a model
> migrator.add_fields(model, **fields) # Add fields to a model
> migrator.change_fields(model, **fields) # Change fields
> migrator.remove_fields(model, *field_names, cascade=True)
> migrator.rename_field(model, old_field_name, new_field_name)
> migrator.rename_table(model, new_table_name)
> migrator.add_index(model, *col_names, unique=False)
> migrator.add_not_null(model, *field_names)
> migrator.add_default(model, field_name, default)
> migrator.add_constraint(model, name, sql)
> migrator.drop_index(model, *col_names)
> migrator.drop_not_null(model, *field_names)
> migrator.drop_constraints(model, *constraints)
"""
from contextlib import suppress
import peewee as pw
from peewee_migrate import Migrator
with suppress(ImportError):
import playhouse.postgres_ext as pw_pext
def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
"""Write your migrations here."""
@migrator.create_model
class Function(pw.Model):
id = pw.TextField(unique=True)
user_id = pw.TextField()
name = pw.TextField()
type = pw.TextField()
content = pw.TextField()
meta = pw.TextField()
created_at = pw.BigIntegerField(null=False)
updated_at = pw.BigIntegerField(null=False)
class Meta:
table_name = 'function'
def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
"""Write your rollback migrations here."""
migrator.remove_model('function')

View file

@ -1,49 +0,0 @@
"""Peewee migrations -- 009_add_models.py.
Some examples (model - class or model name)::
> Model = migrator.orm['table_name'] # Return model in current state by name
> Model = migrator.ModelClass # Return model in current state by name
> migrator.sql(sql) # Run custom SQL
> migrator.run(func, *args, **kwargs) # Run python function with the given args
> migrator.create_model(Model) # Create a model (could be used as decorator)
> migrator.remove_model(model, cascade=True) # Remove a model
> migrator.add_fields(model, **fields) # Add fields to a model
> migrator.change_fields(model, **fields) # Change fields
> migrator.remove_fields(model, *field_names, cascade=True)
> migrator.rename_field(model, old_field_name, new_field_name)
> migrator.rename_table(model, new_table_name)
> migrator.add_index(model, *col_names, unique=False)
> migrator.add_not_null(model, *field_names)
> migrator.add_default(model, field_name, default)
> migrator.add_constraint(model, name, sql)
> migrator.drop_index(model, *col_names)
> migrator.drop_not_null(model, *field_names)
> migrator.drop_constraints(model, *constraints)
"""
from contextlib import suppress
import peewee as pw
from peewee_migrate import Migrator
with suppress(ImportError):
import playhouse.postgres_ext as pw_pext
def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
"""Write your migrations here."""
migrator.add_fields('tool', valves=pw.TextField(null=True))
migrator.add_fields('function', valves=pw.TextField(null=True))
migrator.add_fields('function', is_active=pw.BooleanField(default=False))
def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
"""Write your rollback migrations here."""
migrator.remove_fields('tool', 'valves')
migrator.remove_fields('function', 'valves')
migrator.remove_fields('function', 'is_active')

View file

@ -1,44 +0,0 @@
"""Peewee migrations -- 017_add_user_oauth_sub.py.
Some examples (model - class or model name)::
> Model = migrator.orm['table_name'] # Return model in current state by name
> Model = migrator.ModelClass # Return model in current state by name
> migrator.sql(sql) # Run custom SQL
> migrator.run(func, *args, **kwargs) # Run python function with the given args
> migrator.create_model(Model) # Create a model (could be used as decorator)
> migrator.remove_model(model, cascade=True) # Remove a model
> migrator.add_fields(model, **fields) # Add fields to a model
> migrator.change_fields(model, **fields) # Change fields
> migrator.remove_fields(model, *field_names, cascade=True)
> migrator.rename_field(model, old_field_name, new_field_name)
> migrator.rename_table(model, new_table_name)
> migrator.add_index(model, *col_names, unique=False)
> migrator.add_not_null(model, *field_names)
> migrator.add_default(model, field_name, default)
> migrator.add_constraint(model, name, sql)
> migrator.drop_index(model, *col_names)
> migrator.drop_not_null(model, *field_names)
> migrator.drop_constraints(model, *constraints)
"""
from contextlib import suppress
import peewee as pw
from peewee_migrate import Migrator
with suppress(ImportError):
import playhouse.postgres_ext as pw_pext
def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
"""Write your migrations here."""
migrator.add_fields(
'user',
oauth_sub=pw.TextField(null=True, unique=True),
)
def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
"""Write your rollback migrations here."""
migrator.remove_fields('user', 'oauth_sub')

View file

@ -1,48 +0,0 @@
"""Peewee migrations -- 017_add_user_oauth_sub.py.
Some examples (model - class or model name)::
> Model = migrator.orm['table_name'] # Return model in current state by name
> Model = migrator.ModelClass # Return model in current state by name
> migrator.sql(sql) # Run custom SQL
> migrator.run(func, *args, **kwargs) # Run python function with the given args
> migrator.create_model(Model) # Create a model (could be used as decorator)
> migrator.remove_model(model, cascade=True) # Remove a model
> migrator.add_fields(model, **fields) # Add fields to a model
> migrator.change_fields(model, **fields) # Change fields
> migrator.remove_fields(model, *field_names, cascade=True)
> migrator.rename_field(model, old_field_name, new_field_name)
> migrator.rename_table(model, new_table_name)
> migrator.add_index(model, *col_names, unique=False)
> migrator.add_not_null(model, *field_names)
> migrator.add_default(model, field_name, default)
> migrator.add_constraint(model, name, sql)
> migrator.drop_index(model, *col_names)
> migrator.drop_not_null(model, *field_names)
> migrator.drop_constraints(model, *constraints)
"""
from contextlib import suppress
import peewee as pw
from peewee_migrate import Migrator
with suppress(ImportError):
import playhouse.postgres_ext as pw_pext
def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
"""Write your migrations here."""
migrator.add_fields(
'function',
is_global=pw.BooleanField(default=False),
)
def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
"""Write your rollback migrations here."""
migrator.remove_fields('function', 'is_global')

View file

@ -1,84 +0,0 @@
import logging
import os
from contextvars import ContextVar
from peewee import *
from peewee import InterfaceError as PeeWeeInterfaceError
from peewee import PostgresqlDatabase
from playhouse.db_url import connect, parse
from playhouse.shortcuts import ReconnectMixin
log = logging.getLogger(__name__)
db_state_default = {'closed': None, 'conn': None, 'ctx': None, 'transactions': None}
db_state = ContextVar('db_state', default=db_state_default.copy())
class PeeweeConnectionState(object):
def __init__(self, **kwargs):
super().__setattr__('_state', db_state)
super().__init__(**kwargs)
def __setattr__(self, name, value):
self._state.get()[name] = value
def __getattr__(self, name):
value = self._state.get()[name]
return value
class CustomReconnectMixin(ReconnectMixin):
reconnect_errors = (
# psycopg2
(OperationalError, 'termin'),
(InterfaceError, 'closed'),
# peewee
(PeeWeeInterfaceError, 'closed'),
)
class ReconnectingPostgresqlDatabase(CustomReconnectMixin, PostgresqlDatabase):
pass
def register_connection(db_url):
# Check if using SQLCipher protocol
if db_url.startswith('sqlite+sqlcipher://'):
database_password = os.environ.get('DATABASE_PASSWORD')
if not database_password or database_password.strip() == '':
raise ValueError('DATABASE_PASSWORD is required when using sqlite+sqlcipher:// URLs')
from playhouse.sqlcipher_ext import SqlCipherDatabase
# Parse the database path from SQLCipher URL
# Convert sqlite+sqlcipher:///path/to/db.sqlite to /path/to/db.sqlite
db_path = db_url.replace('sqlite+sqlcipher://', '')
# Use Peewee's native SqlCipherDatabase with encryption
db = SqlCipherDatabase(db_path, passphrase=database_password)
db.autoconnect = True
db.reuse_if_open = True
log.info('Connected to encrypted SQLite database using SQLCipher')
else:
# Standard database connection (existing logic)
db = connect(db_url, unquote_user=True, unquote_password=True)
if isinstance(db, PostgresqlDatabase):
# Enable autoconnect for SQLite databases, managed by Peewee
db.autoconnect = True
db.reuse_if_open = True
log.info('Connected to PostgreSQL database')
# Get the connection details
connection = parse(db_url, unquote_user=True, unquote_password=True)
# Use our custom database class that supports reconnection
db = ReconnectingPostgresqlDatabase(**connection)
db.connect(reuse_if_open=True)
elif isinstance(db, SqliteDatabase):
# Enable autoconnect for SQLite databases, managed by Peewee
db.autoconnect = True
db.reuse_if_open = True
log.info('Connected to SQLite database')
else:
raise ValueError('Unsupported database connection')
return db

File diff suppressed because it is too large Load diff

View file

@ -1,120 +1,87 @@
import logging
from logging.config import fileConfig
from __future__ import annotations
from alembic import context
# Alembic environment configuration runner.
# Coordinates database migrations in both offline and online execution modes.
import logging.config
import logging
import alembic.context
from open_webui.env import DATABASE_PASSWORD, DATABASE_URL, LOG_FORMAT
from open_webui.internal.db import enable_iam_token_auth, extract_ssl_params_from_url, reattach_ssl_params_to_url
from open_webui.models.auths import Auth
from open_webui.models.calendar import Calendar, CalendarEvent, CalendarEventAttendee # noqa: F401
from open_webui.env import DATABASE_URL, DATABASE_PASSWORD, LOG_FORMAT
from open_webui.internal.db import extract_ssl_params_from_url, reattach_ssl_params_to_url
from sqlalchemy import engine_from_config, pool, create_engine
from open_webui.models.chat_messages import ChatMessage # noqa: F401
from open_webui.models.chats import Chat # noqa: F401
from sqlalchemy import create_engine, engine_from_config, pool
# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
config = context.config
# Interpret the config file for Python logging.
# This line sets up loggers basically.
if config.config_file_name is not None:
fileConfig(config.config_file_name, disable_existing_loggers=False)
# Re-apply JSON formatter after fileConfig replaces handlers.
alembic_config = alembic.context.config
if alembic_config.config_file_name:
logging.config.fileConfig(alembic_config.config_file_name, disable_existing_loggers=False)
if LOG_FORMAT == 'json':
from open_webui.env import JSONFormatter
for handler in logging.root.handlers:
handler.setFormatter(JSONFormatter())
# add your model's MetaData object here
# for 'autogenerate' support
# from myapp import mymodel
# target_metadata = mymodel.Base.metadata
target_metadata = Auth.metadata
# other values from the config, defined by the needs of env.py,
# can be acquired:
# my_important_option = config.get_main_option("my_important_option")
# ... etc.
DB_URL = DATABASE_URL
# Normalize SSL query params for psycopg2 (Alembic uses psycopg2 for sync migrations).
url_without_ssl, ssl_params = extract_ssl_params_from_url(DB_URL)
DB_URL = reattach_ssl_params_to_url(url_without_ssl, ssl_params) if ssl_params else DB_URL
if DB_URL:
config.set_main_option('sqlalchemy.url', DB_URL.replace('%', '%%'))
for log_handler in logging.root.handlers:
log_handler.setFormatter(JSONFormatter())
migration_metadata = Auth.metadata
target_db_url = DATABASE_URL
base_url, ssl_query_params = extract_ssl_params_from_url(target_db_url)
if ssl_query_params:
target_db_url = reattach_ssl_params_to_url(base_url, ssl_query_params)
if target_db_url:
alembic_config.set_main_option('sqlalchemy.url', target_db_url.replace('%', '%%'))
def run_migrations_offline() -> None:
"""Run migrations in 'offline' mode.
This configures the context with just a URL
and not an Engine, though an Engine is acceptable
here as well. By skipping the Engine creation
we don't even need a DBAPI to be available.
Calls to context.execute() here emit the given string to the
script output.
"""
url = config.get_main_option('sqlalchemy.url')
context.configure(
url=url,
target_metadata=target_metadata,
"""Execute Alembic migrations in offline mode (outputs raw SQL DDL)."""
db_connection_url = alembic_config.get_main_option('sqlalchemy.url')
alembic.context.configure(
url=db_connection_url,
target_metadata=migration_metadata,
literal_binds=True,
dialect_opts={'paramstyle': 'named'},
)
with alembic.context.begin_transaction():
alembic.context.run_migrations()
with context.begin_transaction():
context.run_migrations()
def _get_engine_connectable():
"""Build the database engine based on target URL and authentication credentials."""
if target_db_url and target_db_url.startswith('sqlite+sqlcipher://'):
if not DATABASE_PASSWORD or not DATABASE_PASSWORD.strip():
raise ValueError('DATABASE_PASSWORD is required when using sqlite+sqlcipher:// URLs')
raw_db_path = target_db_url.replace('sqlite+sqlcipher://', '')
if raw_db_path.startswith('/'):
raw_db_path = raw_db_path[1:]
def _sqlite_cipher_creator():
import sqlcipher3
cipher_conn = sqlcipher3.connect(raw_db_path, check_same_thread=False)
cipher_conn.execute(f"PRAGMA key = '{DATABASE_PASSWORD}'")
return cipher_conn
return create_engine('sqlite://', creator=_sqlite_cipher_creator, echo=False)
return engine_from_config(
alembic_config.get_section(alembic_config.config_ini_section, {}),
prefix='sqlalchemy.',
poolclass=pool.NullPool,
)
def run_migrations_online() -> None:
"""Run migrations in 'online' mode.
In this scenario we need to create an Engine
and associate a connection with the context.
"""
# Handle SQLCipher URLs
if DB_URL and DB_URL.startswith('sqlite+sqlcipher://'):
if not DATABASE_PASSWORD or DATABASE_PASSWORD.strip() == '':
raise ValueError('DATABASE_PASSWORD is required when using sqlite+sqlcipher:// URLs')
# Extract database path from SQLCipher URL
db_path = DB_URL.replace('sqlite+sqlcipher://', '')
if db_path.startswith('/'):
db_path = db_path[1:] # Remove leading slash for relative paths
# Create a custom creator function that uses sqlcipher3
def create_sqlcipher_connection():
import sqlcipher3
conn = sqlcipher3.connect(db_path, check_same_thread=False)
conn.execute(f"PRAGMA key = '{DATABASE_PASSWORD}'")
return conn
connectable = create_engine(
'sqlite://', # Dummy URL since we're using creator
creator=create_sqlcipher_connection,
echo=False,
"""Execute migrations against a live database connection."""
live_connectable = _get_engine_connectable()
enable_iam_token_auth(live_connectable)
with live_connectable.connect() as live_connection:
alembic.context.configure(
connection=live_connection,
target_metadata=migration_metadata,
)
else:
# Standard database connection (existing logic)
connectable = engine_from_config(
config.get_section(config.config_ini_section, {}),
prefix='sqlalchemy.',
poolclass=pool.NullPool,
)
with connectable.connect() as connection:
context.configure(connection=connection, target_metadata=target_metadata)
with context.begin_transaction():
context.run_migrations()
with alembic.context.begin_transaction():
alembic.context.run_migrations()
if context.is_offline_mode():
run_migrations_offline()
else:
run_migrations_online()
# Alembic execution entrypoint branch
if alembic.context.is_offline_mode():
run_migrations_offline() # run in offline mode
if not alembic.context.is_offline_mode():
run_migrations_online() # run in online mode

View file

@ -1,15 +1,20 @@
from alembic import op
from sqlalchemy import Inspector
from __future__ import annotations
"""Alembic migration utilities."""
from alembic import op # noqa: E402 — alembic runtime context
from sqlalchemy import inspect # metadata inspection
def get_existing_tables():
con = op.get_bind()
inspector = Inspector.from_engine(con)
tables = set(inspector.get_table_names())
return tables
# --- database helper functions ---
def get_existing_tables() -> set[str]:
"""Return table names already present in the database."""
conn = op.get_bind()
return set(inspect(conn).get_table_names())
def get_revision_id():
def get_revision_id() -> str:
"""Generate a short random revision identifier."""
import uuid
return str(uuid.uuid4()).replace('-', '')[:12]
return uuid.uuid4().hex[:12]

View file

@ -6,8 +6,8 @@ Create Date: 2025-08-13 03:00:00.000000
"""
from alembic import op
import sqlalchemy as sa
from alembic import op
revision = '018012973d35'
down_revision = 'd31026856c01'
@ -16,18 +16,34 @@ depends_on = None
def upgrade():
conn = op.get_bind()
inspector = sa.inspect(conn)
def _idx_exists(table, idx_name):
return any(i['name'] == idx_name for i in inspector.get_indexes(table))
# Chat table indexes
op.create_index('folder_id_idx', 'chat', ['folder_id'])
op.create_index('user_id_pinned_idx', 'chat', ['user_id', 'pinned'])
op.create_index('user_id_archived_idx', 'chat', ['user_id', 'archived'])
op.create_index('updated_at_user_id_idx', 'chat', ['updated_at', 'user_id'])
op.create_index('folder_id_user_id_idx', 'chat', ['folder_id', 'user_id'])
if not _idx_exists('chat', 'folder_id_idx'):
op.create_index('folder_id_idx', 'chat', ['folder_id'])
if not _idx_exists('chat', 'user_id_pinned_idx'):
op.create_index('user_id_pinned_idx', 'chat', ['user_id', 'pinned'])
if not _idx_exists('chat', 'user_id_archived_idx'):
op.create_index('user_id_archived_idx', 'chat', ['user_id', 'archived'])
if not _idx_exists('chat', 'updated_at_user_id_idx'):
op.create_index('updated_at_user_id_idx', 'chat', ['updated_at', 'user_id'])
if not _idx_exists('chat', 'folder_id_user_id_idx'):
op.create_index('folder_id_user_id_idx', 'chat', ['folder_id', 'user_id'])
# Tag table index
op.create_index('user_id_idx', 'tag', ['user_id'])
if not _idx_exists('tag', 'user_id_idx'):
op.create_index('user_id_idx', 'tag', ['user_id'])
# Function table index
op.create_index('is_global_idx', 'function', ['is_global'])
# Function table index (only if is_global column exists — added by a later migration)
conn = op.get_bind()
inspector = sa.inspect(conn)
func_cols = {c['name'] for c in inspector.get_columns('function')}
if 'is_global' in func_cols and not _idx_exists('function', 'is_global_idx'):
op.create_index('is_global_idx', 'function', ['is_global'])
def downgrade():

View file

@ -6,13 +6,13 @@ Create Date: 2024-10-09 21:02:35.241684
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.sql import table, select, update, column
from sqlalchemy.engine.reflection import Inspector
import json
import sqlalchemy as sa
from alembic import op
from sqlalchemy.engine.reflection import Inspector
from sqlalchemy.sql import column, select, table, update
revision = '1af9b942657b'
down_revision = '242a2047eae0'
branch_labels = None
@ -93,8 +93,11 @@ def upgrade():
conn.execute(update_stmt)
# Add columns `pinned` and `meta` to 'chat'
op.add_column('chat', sa.Column('pinned', sa.Boolean(), nullable=True))
op.add_column('chat', sa.Column('meta', sa.JSON(), nullable=False, server_default='{}'))
chat_columns = {c['name'] for c in inspector.get_columns('chat')}
if 'pinned' not in chat_columns:
op.add_column('chat', sa.Column('pinned', sa.Boolean(), nullable=True))
if 'meta' not in chat_columns:
op.add_column('chat', sa.Column('meta', sa.JSON(), nullable=False, server_default='{}'))
chatidtag = table('chatidtag', column('chat_id', sa.String()), column('tag_name', sa.String()))
chat = table(

View file

@ -0,0 +1,28 @@
"""Add group_member user_id index
Revision ID: 1ce6ade7d93b
Revises: f0bd01a18a3d
Create Date: 2026-07-31 03:00:00.000000
"""
import sqlalchemy as sa
from alembic import op
revision = '1ce6ade7d93b'
down_revision = 'f0bd01a18a3d'
branch_labels = None
depends_on = None
def upgrade():
conn = op.get_bind()
inspector = sa.inspect(conn)
existing_indexes = {idx['name'] for idx in inspector.get_indexes('group_member')}
if 'ix_group_member_user_id_group_id' not in existing_indexes:
op.create_index('ix_group_member_user_id_group_id', 'group_member', ['user_id', 'group_id'])
def downgrade():
op.drop_index('ix_group_member_user_id_group_id', table_name='group_member')

View file

@ -6,12 +6,12 @@ Create Date: 2024-10-09 21:02:35.241684
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.sql import table, select, update
import json
import sqlalchemy as sa
from alembic import op
from sqlalchemy.sql import select, table, update
revision = '242a2047eae0'
down_revision = '6a39f3d8e55c'
branch_labels = None
@ -47,34 +47,41 @@ def upgrade():
# If the column is already JSON, no need to do anything
pass
# Step 3: Migrate data from 'old_chat' to 'chat'
chat_table = table(
'chat',
sa.Column('id', sa.String(), primary_key=True),
sa.Column('old_chat', sa.Text()),
sa.Column('chat', sa.JSON()),
)
# Step 3: Migrate data from 'old_chat' to 'chat' (only if old_chat exists)
# Re-check columns after potential rename above
current_cols = {c['name'] for c in sa.inspect(conn).get_columns('chat')}
if 'old_chat' in current_cols:
chat_table = table(
'chat',
sa.Column('id', sa.String(), primary_key=True),
sa.Column('old_chat', sa.Text()),
sa.Column('chat', sa.JSON()),
)
# - Selecting all data from the table
connection = op.get_bind()
results = connection.execute(select(chat_table.c.id, chat_table.c.old_chat))
for row in results:
try:
# Convert text JSON to actual JSON object, assuming the text is in JSON format
json_data = json.loads(row.old_chat)
except json.JSONDecodeError:
json_data = None # Handle cases where the text cannot be converted to JSON
# - Selecting all data from the table
connection = op.get_bind()
results = connection.execute(select(chat_table.c.id, chat_table.c.old_chat))
for row in results:
try:
# Convert text JSON to actual JSON object, assuming the text is in JSON format
json_data = json.loads(row.old_chat)
except json.JSONDecodeError:
json_data = None # Handle cases where the text cannot be converted to JSON
connection.execute(sa.update(chat_table).where(chat_table.c.id == row.id).values(chat=json_data))
connection.execute(sa.update(chat_table).where(chat_table.c.id == row.id).values(chat=json_data))
# Step 4: Drop 'old_chat' column
print("Dropping 'old_chat' column")
op.drop_column('chat', 'old_chat')
# Step 4: Drop 'old_chat' column
print("Dropping 'old_chat' column")
op.drop_column('chat', 'old_chat')
def downgrade():
conn = op.get_bind()
columns = {col['name'] for col in sa.inspect(conn).get_columns('chat')}
# Step 1: Add 'old_chat' column back as Text
op.add_column('chat', sa.Column('old_chat', sa.Text(), nullable=True))
if 'old_chat' not in columns:
op.add_column('chat', sa.Column('old_chat', sa.Text(), nullable=True))
# Step 2: Convert 'chat' JSON data back to text and store in 'old_chat'
chat_table = table(
@ -84,14 +91,14 @@ def downgrade():
sa.Column('old_chat', sa.Text()),
)
connection = op.get_bind()
results = connection.execute(select(chat_table.c.id, chat_table.c.chat))
for row in results:
text_data = json.dumps(row.chat) if row.chat is not None else None
connection.execute(sa.update(chat_table).where(chat_table.c.id == row.id).values(old_chat=text_data))
if 'chat' in columns:
results = conn.execute(select(chat_table.c.id, chat_table.c.chat))
for row in results:
text_data = json.dumps(row.chat) if row.chat is not None else None
conn.execute(sa.update(chat_table).where(chat_table.c.id == row.id).values(old_chat=text_data))
# Step 3: Remove the new 'chat' JSON column
op.drop_column('chat', 'chat')
# Step 3: Remove the new 'chat' JSON column
op.drop_column('chat', 'chat')
# Step 4: Rename 'old_chat' back to 'chat'
op.alter_column('chat', 'old_chat', new_column_name='chat', existing_type=sa.Text())

View file

@ -8,9 +8,9 @@ Create Date: 2025-11-27 03:07:56.200231
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
import open_webui.internal.db
import sqlalchemy as sa
from alembic import op
# revision identifiers, used by Alembic.
revision: str = '2f1211949ecc'
@ -20,63 +20,76 @@ depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
conn = op.get_bind()
inspector = sa.inspect(conn)
# New columns to be added to channel_member table
op.add_column('channel_member', sa.Column('status', sa.Text(), nullable=True))
op.add_column(
'channel_member',
sa.Column(
'is_active',
sa.Boolean(),
nullable=False,
default=True,
server_default=sa.sql.expression.true(),
),
)
op.add_column(
'channel_member',
sa.Column(
'is_channel_muted',
sa.Boolean(),
nullable=False,
default=False,
server_default=sa.sql.expression.false(),
),
)
op.add_column(
'channel_member',
sa.Column(
'is_channel_pinned',
sa.Boolean(),
nullable=False,
default=False,
server_default=sa.sql.expression.false(),
),
)
op.add_column('channel_member', sa.Column('data', sa.JSON(), nullable=True))
op.add_column('channel_member', sa.Column('meta', sa.JSON(), nullable=True))
op.add_column('channel_member', sa.Column('joined_at', sa.BigInteger(), nullable=False))
op.add_column('channel_member', sa.Column('left_at', sa.BigInteger(), nullable=True))
op.add_column('channel_member', sa.Column('last_read_at', sa.BigInteger(), nullable=True))
op.add_column('channel_member', sa.Column('updated_at', sa.BigInteger(), nullable=True))
cm_cols = {c['name'] for c in inspector.get_columns('channel_member')}
if 'status' not in cm_cols:
op.add_column('channel_member', sa.Column('status', sa.Text(), nullable=True))
if 'is_active' not in cm_cols:
op.add_column(
'channel_member',
sa.Column(
'is_active',
sa.Boolean(),
nullable=False,
default=True,
server_default=sa.sql.expression.true(),
),
)
if 'is_channel_muted' not in cm_cols:
op.add_column(
'channel_member',
sa.Column(
'is_channel_muted',
sa.Boolean(),
nullable=False,
default=False,
server_default=sa.sql.expression.false(),
),
)
if 'is_channel_pinned' not in cm_cols:
op.add_column(
'channel_member',
sa.Column(
'is_channel_pinned',
sa.Boolean(),
nullable=False,
default=False,
server_default=sa.sql.expression.false(),
),
)
if 'data' not in cm_cols:
op.add_column('channel_member', sa.Column('data', sa.JSON(), nullable=True))
if 'meta' not in cm_cols:
op.add_column('channel_member', sa.Column('meta', sa.JSON(), nullable=True))
if 'joined_at' not in cm_cols:
op.add_column('channel_member', sa.Column('joined_at', sa.BigInteger(), nullable=False))
if 'left_at' not in cm_cols:
op.add_column('channel_member', sa.Column('left_at', sa.BigInteger(), nullable=True))
if 'last_read_at' not in cm_cols:
op.add_column('channel_member', sa.Column('last_read_at', sa.BigInteger(), nullable=True))
if 'updated_at' not in cm_cols:
op.add_column('channel_member', sa.Column('updated_at', sa.BigInteger(), nullable=True))
# New columns to be added to message table
op.add_column(
'message',
sa.Column(
'is_pinned',
sa.Boolean(),
nullable=False,
default=False,
server_default=sa.sql.expression.false(),
),
)
op.add_column('message', sa.Column('pinned_at', sa.BigInteger(), nullable=True))
op.add_column('message', sa.Column('pinned_by', sa.Text(), nullable=True))
msg_cols = {c['name'] for c in inspector.get_columns('message')}
if 'is_pinned' not in msg_cols:
op.add_column(
'message',
sa.Column(
'is_pinned',
sa.Boolean(),
nullable=False,
default=False,
server_default=sa.sql.expression.false(),
),
)
if 'pinned_at' not in msg_cols:
op.add_column('message', sa.Column('pinned_at', sa.BigInteger(), nullable=True))
if 'pinned_by' not in msg_cols:
op.add_column('message', sa.Column('pinned_by', sa.Text(), nullable=True))
def downgrade() -> None:

View file

@ -6,11 +6,11 @@ Create Date: 2026-01-23 17:15:00.000000
"""
from typing import Sequence, Union
import uuid
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from alembic import op
revision: str = '374d2f66af06'
down_revision: Union[str, None] = 'c440947495f3'
@ -20,150 +20,167 @@ depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
conn = op.get_bind()
inspector = sa.inspect(conn)
existing_tables = set(inspector.get_table_names())
# Step 1: Read existing data from OLD table (schema likely command as PK)
# We use batch_alter previously, but we want to move to new table.
# We need to assume the OLD structure.
# If the final state already exists (prompt has 'id' PK + prompt_history exists),
# the migration completed successfully on a prior run — nothing to do.
if 'prompt_history' in existing_tables and 'prompt_new' not in existing_tables:
# prompt_history exists and prompt_new was already renamed → done
prompt_cols = {c['name'] for c in inspector.get_columns('prompt')}
if 'id' in prompt_cols and 'version_id' in prompt_cols:
return
old_prompt_table = sa.table(
'prompt',
sa.column('command', sa.Text()),
sa.column('user_id', sa.Text()),
sa.column('title', sa.Text()),
sa.column('content', sa.Text()),
sa.column('timestamp', sa.BigInteger()),
sa.column('access_control', sa.JSON()),
)
# Check if table exists/read data
try:
existing_prompts = conn.execute(
sa.select(
old_prompt_table.c.command,
old_prompt_table.c.user_id,
old_prompt_table.c.title,
old_prompt_table.c.content,
old_prompt_table.c.timestamp,
old_prompt_table.c.access_control,
# Step 1: Read existing data from OLD table (schema: command as PK)
# Only read if the old-schema prompt table still exists (has 'command' but no 'version_id')
existing_prompts = []
if 'prompt' in existing_tables and 'prompt_new' not in existing_tables:
prompt_cols = {c['name'] for c in inspector.get_columns('prompt')}
if 'command' in prompt_cols and 'version_id' not in prompt_cols:
old_prompt_table = sa.table(
'prompt',
sa.column('command', sa.Text()),
sa.column('user_id', sa.Text()),
sa.column('title', sa.Text()),
sa.column('content', sa.Text()),
sa.column('timestamp', sa.BigInteger()),
sa.column('access_control', sa.JSON()),
)
).fetchall()
except Exception:
# Fallback if table doesn't exist (new install)
existing_prompts = []
try:
existing_prompts = conn.execute(
sa.select(
old_prompt_table.c.command,
old_prompt_table.c.user_id,
old_prompt_table.c.title,
old_prompt_table.c.content,
old_prompt_table.c.timestamp,
old_prompt_table.c.access_control,
)
).fetchall()
except Exception:
existing_prompts = []
# Step 2: Create new prompt table with 'id' as PRIMARY KEY
op.create_table(
'prompt_new',
sa.Column('id', sa.Text(), primary_key=True),
sa.Column('command', sa.String(), unique=True, index=True),
sa.Column('user_id', sa.String(), nullable=False),
sa.Column('name', sa.Text(), nullable=False),
sa.Column('content', sa.Text(), nullable=False),
sa.Column('data', sa.JSON(), nullable=True),
sa.Column('meta', sa.JSON(), nullable=True),
sa.Column('access_control', sa.JSON(), nullable=True),
sa.Column('is_active', sa.Boolean(), nullable=False, server_default='1'),
sa.Column('version_id', sa.Text(), nullable=True),
sa.Column('tags', sa.JSON(), nullable=True),
sa.Column('created_at', sa.BigInteger(), nullable=False),
sa.Column('updated_at', sa.BigInteger(), nullable=False),
)
# Step 3: Create prompt_history table
op.create_table(
'prompt_history',
sa.Column('id', sa.Text(), primary_key=True),
sa.Column('prompt_id', sa.Text(), nullable=False, index=True),
sa.Column('parent_id', sa.Text(), nullable=True),
sa.Column('snapshot', sa.JSON(), nullable=False),
sa.Column('user_id', sa.Text(), nullable=False),
sa.Column('commit_message', sa.Text(), nullable=True),
sa.Column('created_at', sa.BigInteger(), nullable=False),
)
# Step 4: Migrate data
prompt_new_table = sa.table(
'prompt_new',
sa.column('id', sa.Text()),
sa.column('command', sa.String()),
sa.column('user_id', sa.String()),
sa.column('name', sa.Text()),
sa.column('content', sa.Text()),
sa.column('data', sa.JSON()),
sa.column('meta', sa.JSON()),
sa.column('access_control', sa.JSON()),
sa.column('is_active', sa.Boolean()),
sa.column('version_id', sa.Text()),
sa.column('tags', sa.JSON()),
sa.column('created_at', sa.BigInteger()),
sa.column('updated_at', sa.BigInteger()),
)
prompt_history_table = sa.table(
'prompt_history',
sa.column('id', sa.Text()),
sa.column('prompt_id', sa.Text()),
sa.column('parent_id', sa.Text()),
sa.column('snapshot', sa.JSON()),
sa.column('user_id', sa.Text()),
sa.column('commit_message', sa.Text()),
sa.column('created_at', sa.BigInteger()),
)
for row in existing_prompts:
command = row[0]
user_id = row[1]
title = row[2]
content = row[3]
timestamp = row[4]
access_control = row[5]
new_uuid = str(uuid.uuid4())
history_uuid = str(uuid.uuid4())
clean_command = command[1:] if command and command.startswith('/') else command
# Insert into prompt_new
conn.execute(
sa.insert(prompt_new_table).values(
id=new_uuid,
command=clean_command,
user_id=user_id,
name=title,
content=content,
data={},
meta={},
access_control=access_control,
is_active=True,
version_id=history_uuid,
tags=[],
created_at=timestamp,
updated_at=timestamp,
)
# Step 2: Create new prompt table with 'id' as PRIMARY KEY (if not already created)
if 'prompt_new' not in existing_tables:
op.create_table(
'prompt_new',
sa.Column('id', sa.Text(), primary_key=True),
sa.Column('command', sa.String(), unique=True, index=True),
sa.Column('user_id', sa.String(), nullable=False),
sa.Column('name', sa.Text(), nullable=False),
sa.Column('content', sa.Text(), nullable=False),
sa.Column('data', sa.JSON(), nullable=True),
sa.Column('meta', sa.JSON(), nullable=True),
sa.Column('access_control', sa.JSON(), nullable=True),
sa.Column('is_active', sa.Boolean(), nullable=False, server_default='1'),
sa.Column('version_id', sa.Text(), nullable=True),
sa.Column('tags', sa.JSON(), nullable=True),
sa.Column('created_at', sa.BigInteger(), nullable=False),
sa.Column('updated_at', sa.BigInteger(), nullable=False),
)
# Create initial history entry
conn.execute(
sa.insert(prompt_history_table).values(
id=history_uuid,
prompt_id=new_uuid,
parent_id=None,
snapshot={
'name': title,
'content': content,
'command': clean_command,
'data': {},
'meta': {},
'access_control': access_control,
},
user_id=user_id,
commit_message=None,
created_at=timestamp,
)
# Step 3: Create prompt_history table (if not already created)
if 'prompt_history' not in existing_tables:
op.create_table(
'prompt_history',
sa.Column('id', sa.Text(), primary_key=True),
sa.Column('prompt_id', sa.Text(), nullable=False, index=True),
sa.Column('parent_id', sa.Text(), nullable=True),
sa.Column('snapshot', sa.JSON(), nullable=False),
sa.Column('user_id', sa.Text(), nullable=False),
sa.Column('commit_message', sa.Text(), nullable=True),
sa.Column('created_at', sa.BigInteger(), nullable=False),
)
# Step 5: Replace old table with new one
op.drop_table('prompt')
op.rename_table('prompt_new', 'prompt')
# Step 4: Migrate data (only if we have old data to migrate)
if existing_prompts:
prompt_new_table = sa.table(
'prompt_new',
sa.column('id', sa.Text()),
sa.column('command', sa.String()),
sa.column('user_id', sa.String()),
sa.column('name', sa.Text()),
sa.column('content', sa.Text()),
sa.column('data', sa.JSON()),
sa.column('meta', sa.JSON()),
sa.column('access_control', sa.JSON()),
sa.column('is_active', sa.Boolean()),
sa.column('version_id', sa.Text()),
sa.column('tags', sa.JSON()),
sa.column('created_at', sa.BigInteger()),
sa.column('updated_at', sa.BigInteger()),
)
prompt_history_table = sa.table(
'prompt_history',
sa.column('id', sa.Text()),
sa.column('prompt_id', sa.Text()),
sa.column('parent_id', sa.Text()),
sa.column('snapshot', sa.JSON()),
sa.column('user_id', sa.Text()),
sa.column('commit_message', sa.Text()),
sa.column('created_at', sa.BigInteger()),
)
for row in existing_prompts:
command = row[0]
user_id = row[1]
title = row[2]
content = row[3]
timestamp = row[4]
access_control = row[5]
new_uuid = str(uuid.uuid4())
history_uuid = str(uuid.uuid4())
clean_command = command[1:] if command and command.startswith('/') else command
# Insert into prompt_new
conn.execute(
sa.insert(prompt_new_table).values(
id=new_uuid,
command=clean_command,
user_id=user_id,
name=title,
content=content,
data={},
meta={},
access_control=access_control,
is_active=True,
version_id=history_uuid,
tags=[],
created_at=timestamp,
updated_at=timestamp,
)
)
# Create initial history entry
conn.execute(
sa.insert(prompt_history_table).values(
id=history_uuid,
prompt_id=new_uuid,
parent_id=None,
snapshot={
'name': title,
'content': content,
'command': clean_command,
'data': {},
'meta': {},
'access_control': access_control,
},
user_id=user_id,
commit_message=None,
created_at=timestamp,
)
)
# Step 5: Replace old table with new one (only if prompt_new exists)
# Re-check tables after potential creation above
inspector.clear_cache()
current_tables = set(inspector.get_table_names())
if 'prompt_new' in current_tables:
if 'prompt' in current_tables:
op.drop_table('prompt')
op.rename_table('prompt_new', 'prompt')
def downgrade() -> None:

View file

@ -6,8 +6,8 @@ Create Date: 2024-12-30 03:00:00.000000
"""
from alembic import op
import sqlalchemy as sa
from alembic import op
revision = '3781e22d8b01'
down_revision = '7826ab40b532'
@ -16,38 +16,50 @@ depends_on = None
def upgrade():
conn = op.get_bind()
inspector = sa.inspect(conn)
existing_tables = set(inspector.get_table_names())
# Add 'type' column to the 'channel' table
op.add_column(
'channel',
sa.Column(
'type',
sa.Text(),
nullable=True,
),
)
channel_cols = {c['name'] for c in inspector.get_columns('channel')}
if 'type' not in channel_cols:
op.add_column(
'channel',
sa.Column(
'type',
sa.Text(),
nullable=True,
),
)
# Add 'parent_id' column to the 'message' table for threads
op.add_column(
'message',
sa.Column('parent_id', sa.Text(), nullable=True),
)
message_cols = {c['name'] for c in inspector.get_columns('message')}
if 'parent_id' not in message_cols:
op.add_column(
'message',
sa.Column('parent_id', sa.Text(), nullable=True),
)
op.create_table(
'message_reaction',
sa.Column('id', sa.Text(), nullable=False, primary_key=True, unique=True), # Unique reaction ID
sa.Column('user_id', sa.Text(), nullable=False), # User who reacted
sa.Column('message_id', sa.Text(), nullable=False), # Message that was reacted to
sa.Column('name', sa.Text(), nullable=False), # Reaction name (e.g. "thumbs_up")
sa.Column('created_at', sa.BigInteger(), nullable=True), # Timestamp of when the reaction was added
)
if 'message_reaction' not in existing_tables:
op.create_table(
'message_reaction',
sa.Column('id', sa.Text(), nullable=False, primary_key=True, unique=True), # Unique reaction ID
sa.Column('user_id', sa.Text(), nullable=False), # User who reacted
sa.Column('message_id', sa.Text(), nullable=False), # Message that was reacted to
sa.Column('name', sa.Text(), nullable=False), # Reaction name (e.g. "thumbs_up")
sa.Column('created_at', sa.BigInteger(), nullable=True), # Timestamp of when the reaction was added
)
op.create_table(
'channel_member',
sa.Column('id', sa.Text(), nullable=False, primary_key=True, unique=True), # Record ID for the membership row
sa.Column('channel_id', sa.Text(), nullable=False), # Associated channel
sa.Column('user_id', sa.Text(), nullable=False), # Associated user
sa.Column('created_at', sa.BigInteger(), nullable=True), # Timestamp of when the user joined the channel
)
if 'channel_member' not in existing_tables:
op.create_table(
'channel_member',
sa.Column(
'id', sa.Text(), nullable=False, primary_key=True, unique=True
), # Record ID for the membership row
sa.Column('channel_id', sa.Text(), nullable=False), # Associated channel
sa.Column('user_id', sa.Text(), nullable=False), # Associated user
sa.Column('created_at', sa.BigInteger(), nullable=True), # Timestamp of when the user joined the channel
)
def downgrade():

View file

@ -6,13 +6,13 @@ Create Date: 2025-11-17 03:45:25.123939
"""
import uuid
import time
import json
import time
import uuid
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from alembic import op
# revision identifiers, used by Alembic.
revision: str = '37f288994c47'
@ -22,6 +22,13 @@ depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
conn = op.get_bind()
inspector = sa.inspect(conn)
existing_tables = set(inspector.get_table_names())
if 'group_member' in existing_tables:
return # Already created — skip everything
# 1. Create new table
op.create_table(
'group_member',

View file

@ -8,8 +8,8 @@ Create Date: 2025-09-08 14:19:59.583921
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from alembic import op
# revision identifiers, used by Alembic.
revision: str = '38d63c18f30f'
@ -19,50 +19,39 @@ depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# Ensure 'id' column in 'user' table is unique and primary key (ForeignKey constraint)
inspector = sa.inspect(op.get_bind())
columns = inspector.get_columns('user')
existing_tables = set(inspector.get_table_names())
pk_columns = inspector.get_pk_constraint('user')['constrained_columns']
id_column = next((col for col in columns if col['name'] == 'id'), None)
# ── Create oauth_session table (idempotent) ───────────────────────
if 'oauth_session' not in existing_tables:
op.create_table(
'oauth_session',
sa.Column('id', sa.Text(), primary_key=True, nullable=False, unique=True),
sa.Column(
'user_id',
sa.Text(),
sa.ForeignKey('user.id', ondelete='CASCADE'),
nullable=False,
),
sa.Column('provider', sa.Text(), nullable=False),
sa.Column('token', sa.Text(), nullable=False),
sa.Column('expires_at', sa.BigInteger(), nullable=False),
sa.Column('created_at', sa.BigInteger(), nullable=False),
sa.Column('updated_at', sa.BigInteger(), nullable=False),
)
if id_column and not id_column.get('unique', False):
unique_constraints = inspector.get_unique_constraints('user')
unique_columns = {tuple(u['column_names']) for u in unique_constraints}
with op.batch_alter_table('user') as batch_op:
# If primary key is wrong, drop it
if pk_columns and pk_columns != ['id']:
batch_op.drop_constraint(inspector.get_pk_constraint('user')['name'], type_='primary')
# Add unique constraint if missing
if ('id',) not in unique_columns:
batch_op.create_unique_constraint('uq_user_id', ['id'])
# Re-create correct primary key
batch_op.create_primary_key('pk_user_id', ['id'])
# Create oauth_session table
op.create_table(
'oauth_session',
sa.Column('id', sa.Text(), primary_key=True, nullable=False, unique=True),
sa.Column(
'user_id',
sa.Text(),
sa.ForeignKey('user.id', ondelete='CASCADE'),
nullable=False,
),
sa.Column('provider', sa.Text(), nullable=False),
sa.Column('token', sa.Text(), nullable=False),
sa.Column('expires_at', sa.BigInteger(), nullable=False),
sa.Column('created_at', sa.BigInteger(), nullable=False),
sa.Column('updated_at', sa.BigInteger(), nullable=False),
# Create indexes (idempotent — no-ops when table was just created
# with the columns above, and safe to call if indexes already exist).
existing_indexes = (
{idx['name'] for idx in inspector.get_indexes('oauth_session')} if 'oauth_session' in existing_tables else set()
)
# Create indexes for better performance
op.create_index('idx_oauth_session_user_id', 'oauth_session', ['user_id'])
op.create_index('idx_oauth_session_expires_at', 'oauth_session', ['expires_at'])
op.create_index('idx_oauth_session_user_provider', 'oauth_session', ['user_id', 'provider'])
if 'idx_oauth_session_user_id' not in existing_indexes:
op.create_index('idx_oauth_session_user_id', 'oauth_session', ['user_id'])
if 'idx_oauth_session_expires_at' not in existing_indexes:
op.create_index('idx_oauth_session_expires_at', 'oauth_session', ['expires_at'])
if 'idx_oauth_session_user_provider' not in existing_indexes:
op.create_index('idx_oauth_session_user_provider', 'oauth_session', ['user_id', 'provider'])
def downgrade() -> None:

View file

@ -6,13 +6,13 @@ Create Date: 2024-10-09 21:02:35.241684
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.sql import table, select, update, column
from sqlalchemy.engine.reflection import Inspector
import json
import sqlalchemy as sa
from alembic import op
from sqlalchemy.engine.reflection import Inspector
from sqlalchemy.sql import column, select, table, update
revision = '3ab32c4b8f59'
down_revision = '1af9b942657b'
branch_labels = None

View file

@ -8,8 +8,8 @@ Create Date: 2025-08-21 02:07:18.078283
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from alembic import op
# revision identifiers, used by Alembic.
revision: str = '3af16a1c9fb6'
@ -19,10 +19,18 @@ depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
op.add_column('user', sa.Column('username', sa.String(length=50), nullable=True))
op.add_column('user', sa.Column('bio', sa.Text(), nullable=True))
op.add_column('user', sa.Column('gender', sa.Text(), nullable=True))
op.add_column('user', sa.Column('date_of_birth', sa.Date(), nullable=True))
conn = op.get_bind()
inspector = sa.inspect(conn)
user_cols = {c['name'] for c in inspector.get_columns('user')}
if 'username' not in user_cols:
op.add_column('user', sa.Column('username', sa.String(length=50), nullable=True))
if 'bio' not in user_cols:
op.add_column('user', sa.Column('bio', sa.Text(), nullable=True))
if 'gender' not in user_cols:
op.add_column('user', sa.Column('gender', sa.Text(), nullable=True))
if 'date_of_birth' not in user_cols:
op.add_column('user', sa.Column('date_of_birth', sa.Date(), nullable=True))
def downgrade() -> None:

View file

@ -0,0 +1,72 @@
"""add knowledge_directory table
Revision ID: 3c9b0ca343fd
Revises: a0b1c2d3e4f5
Create Date: 2026-05-13 21:58:40.832482
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = '3c9b0ca343fd'
down_revision: Union[str, None] = 'a0b1c2d3e4f5'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
conn = op.get_bind()
inspector = sa.inspect(conn)
existing_tables = set(inspector.get_table_names())
if 'knowledge_directory' not in existing_tables:
# Create knowledge_directory table
op.create_table(
'knowledge_directory',
sa.Column('id', sa.Text(), nullable=False),
sa.Column('knowledge_id', sa.Text(), nullable=False),
sa.Column('parent_id', sa.Text(), nullable=True),
sa.Column('name', sa.Text(), nullable=False),
sa.Column('user_id', sa.Text(), nullable=False),
sa.Column('created_at', sa.BigInteger(), nullable=False),
sa.Column('updated_at', sa.BigInteger(), nullable=False),
sa.ForeignKeyConstraint(['knowledge_id'], ['knowledge.id'], ondelete='CASCADE'),
sa.ForeignKeyConstraint(['parent_id'], ['knowledge_directory.id'], ondelete='CASCADE'),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint(
'knowledge_id', 'parent_id', 'name', name='uq_knowledge_directory_knowledge_parent_name'
),
)
op.create_index('ix_knowledge_directory_knowledge_id', 'knowledge_directory', ['knowledge_id'])
op.create_index('ix_knowledge_directory_parent_id', 'knowledge_directory', ['parent_id'])
# Add directory_id column to knowledge_file
kf_cols = {c['name'] for c in inspector.get_columns('knowledge_file')}
if 'directory_id' not in kf_cols:
with op.batch_alter_table('knowledge_file') as batch:
batch.add_column(sa.Column('directory_id', sa.Text(), nullable=True))
batch.create_foreign_key(
'fk_knowledge_file_directory_id',
'knowledge_directory',
['directory_id'],
['id'],
ondelete='SET NULL',
)
batch.create_index('ix_knowledge_file_directory_id', ['directory_id'])
def downgrade() -> None:
# Remove directory_id from knowledge_file
with op.batch_alter_table('knowledge_file') as batch:
batch.drop_index('ix_knowledge_file_directory_id')
batch.drop_constraint('fk_knowledge_file_directory_id', type_='foreignkey')
batch.drop_column('directory_id')
# Drop knowledge_directory table
op.drop_index('ix_knowledge_directory_parent_id', table_name='knowledge_directory')
op.drop_index('ix_knowledge_directory_knowledge_id', table_name='knowledge_directory')
op.drop_table('knowledge_directory')

View file

@ -6,16 +6,15 @@ Create Date: 2025-12-02 06:54:19.401334
"""
import json
import time
import uuid
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from sqlalchemy import inspect
import open_webui.internal.db
import time
import json
import uuid
import sqlalchemy as sa
from alembic import op
from sqlalchemy import inspect
# revision identifiers, used by Alembic.
revision: str = '3e0e00844bb0'
@ -25,6 +24,13 @@ depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
conn = op.get_bind()
inspector = sa.inspect(conn)
existing_tables = set(inspector.get_table_names())
if 'knowledge_file' in existing_tables:
return # Already created — skip everything
op.create_table(
'knowledge_file',
sa.Column('id', sa.Text(), primary_key=True),

View file

@ -0,0 +1,584 @@
"""reshape config to per key rows
Revision ID: 3ff2c63645b8
Revises: 461111b60977
Create Date: 2026-06-17 00:50:51.477073
"""
import json
import time
from typing import Sequence, Union
import sqlalchemy as sa
from alembic import op
# revision identifiers, used by Alembic.
revision: str = '3ff2c63645b8'
down_revision: Union[str, None] = '461111b60977'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
# Maps every dot-notation blob path to its legacy env/config key name.
# Built from the legacy persistent config declarations in config.py.
BLOB_PATH_TO_KEY = {
'audio.stt.allowed_extensions': 'AUDIO_STT_ALLOWED_EXTENSIONS',
'audio.stt.azure.api_key': 'AUDIO_STT_AZURE_API_KEY',
'audio.stt.azure.base_url': 'AUDIO_STT_AZURE_BASE_URL',
'audio.stt.azure.locales': 'AUDIO_STT_AZURE_LOCALES',
'audio.stt.azure.max_speakers': 'AUDIO_STT_AZURE_MAX_SPEAKERS',
'audio.stt.azure.region': 'AUDIO_STT_AZURE_REGION',
'audio.stt.deepgram.api_key': 'DEEPGRAM_API_KEY',
'audio.stt.engine': 'AUDIO_STT_ENGINE',
'audio.stt.mistral.api_base_url': 'AUDIO_STT_MISTRAL_API_BASE_URL',
'audio.stt.mistral.api_key': 'AUDIO_STT_MISTRAL_API_KEY',
'audio.stt.mistral.use_chat_completions': 'AUDIO_STT_MISTRAL_USE_CHAT_COMPLETIONS',
'audio.stt.model': 'AUDIO_STT_MODEL',
'audio.stt.openai.api_base_url': 'AUDIO_STT_OPENAI_API_BASE_URL',
'audio.stt.openai.api_key': 'AUDIO_STT_OPENAI_API_KEY',
'audio.stt.supported_content_types': 'AUDIO_STT_SUPPORTED_CONTENT_TYPES',
'audio.stt.whisper_model': 'WHISPER_MODEL',
'audio.tts.api_key': 'AUDIO_TTS_API_KEY',
'audio.tts.azure.speech_base_url': 'AUDIO_TTS_AZURE_SPEECH_BASE_URL',
'audio.tts.azure.speech_output_format': 'AUDIO_TTS_AZURE_SPEECH_OUTPUT_FORMAT',
'audio.tts.azure.speech_region': 'AUDIO_TTS_AZURE_SPEECH_REGION',
'audio.tts.engine': 'AUDIO_TTS_ENGINE',
'audio.tts.mistral.api_base_url': 'AUDIO_TTS_MISTRAL_API_BASE_URL',
'audio.tts.mistral.api_key': 'AUDIO_TTS_MISTRAL_API_KEY',
'audio.tts.model': 'AUDIO_TTS_MODEL',
'audio.tts.openai.api_base_url': 'AUDIO_TTS_OPENAI_API_BASE_URL',
'audio.tts.openai.api_key': 'AUDIO_TTS_OPENAI_API_KEY',
'audio.tts.openai.params': 'AUDIO_TTS_OPENAI_PARAMS',
'audio.tts.split_on': 'AUDIO_TTS_SPLIT_ON',
'audio.tts.voice': 'AUDIO_TTS_VOICE',
'auth.admin.email': 'ADMIN_EMAIL',
'auth.admin.show': 'SHOW_ADMIN_DETAILS',
'auth.api_key.allowed_endpoints': 'API_KEYS_ALLOWED_ENDPOINTS',
'auth.api_key.endpoint_restrictions': 'ENABLE_API_KEYS_ENDPOINT_RESTRICTIONS',
'auth.enable_api_keys': 'ENABLE_API_KEYS',
'auth.jwt_expiry': 'JWT_EXPIRES_IN',
'automations.enable': 'ENABLE_AUTOMATIONS',
'automations.max_count': 'AUTOMATION_MAX_COUNT',
'automations.min_interval': 'AUTOMATION_MIN_INTERVAL',
'calendar.enable': 'ENABLE_CALENDAR',
'channels.enable': 'ENABLE_CHANNELS',
'code_execution.enable': 'ENABLE_CODE_EXECUTION',
'code_execution.engine': 'CODE_EXECUTION_ENGINE',
'code_execution.jupyter.auth': 'CODE_EXECUTION_JUPYTER_AUTH',
'code_execution.jupyter.auth_password': 'CODE_EXECUTION_JUPYTER_AUTH_PASSWORD',
'code_execution.jupyter.auth_token': 'CODE_EXECUTION_JUPYTER_AUTH_TOKEN',
'code_execution.jupyter.timeout': 'CODE_EXECUTION_JUPYTER_TIMEOUT',
'code_execution.jupyter.url': 'CODE_EXECUTION_JUPYTER_URL',
'code_interpreter.enable': 'ENABLE_CODE_INTERPRETER',
'code_interpreter.engine': 'CODE_INTERPRETER_ENGINE',
'code_interpreter.jupyter.auth': 'CODE_INTERPRETER_JUPYTER_AUTH',
'code_interpreter.jupyter.auth_password': 'CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD',
'code_interpreter.jupyter.auth_token': 'CODE_INTERPRETER_JUPYTER_AUTH_TOKEN',
'code_interpreter.jupyter.timeout': 'CODE_INTERPRETER_JUPYTER_TIMEOUT',
'code_interpreter.jupyter.url': 'CODE_INTERPRETER_JUPYTER_URL',
'code_interpreter.prompt_template': 'CODE_INTERPRETER_PROMPT_TEMPLATE',
'direct.enable': 'ENABLE_DIRECT_CONNECTIONS',
'evaluation.arena.enable': 'ENABLE_EVALUATION_ARENA_MODELS',
'evaluation.arena.models': 'EVALUATION_ARENA_MODELS',
'file.image_compression_height': 'FILE_IMAGE_COMPRESSION_HEIGHT',
'file.image_compression_width': 'FILE_IMAGE_COMPRESSION_WIDTH',
'folders.enable': 'ENABLE_FOLDERS',
'folders.max_file_count': 'FOLDER_MAX_FILE_COUNT',
'google_drive.api_key': 'GOOGLE_DRIVE_API_KEY',
'google_drive.client_id': 'GOOGLE_DRIVE_CLIENT_ID',
'google_drive.enable': 'ENABLE_GOOGLE_DRIVE_INTEGRATION',
'image_generation.automatic1111.api_auth': 'AUTOMATIC1111_API_AUTH',
'image_generation.automatic1111.api_params': 'AUTOMATIC1111_PARAMS',
'image_generation.automatic1111.base_url': 'AUTOMATIC1111_BASE_URL',
'image_generation.comfyui.api_key': 'COMFYUI_API_KEY',
'image_generation.comfyui.base_url': 'COMFYUI_BASE_URL',
'image_generation.comfyui.nodes': 'COMFYUI_WORKFLOW_NODES',
'image_generation.comfyui.workflow': 'COMFYUI_WORKFLOW',
'image_generation.enable': 'ENABLE_IMAGE_GENERATION',
'image_generation.engine': 'IMAGE_GENERATION_ENGINE',
'image_generation.gemini.api_base_url': 'IMAGES_GEMINI_API_BASE_URL',
'image_generation.gemini.api_key': 'IMAGES_GEMINI_API_KEY',
'image_generation.gemini.endpoint_method': 'IMAGES_GEMINI_ENDPOINT_METHOD',
'image_generation.model': 'IMAGE_GENERATION_MODEL',
'image_generation.openai.api_base_url': 'IMAGES_OPENAI_API_BASE_URL',
'image_generation.openai.api_key': 'IMAGES_OPENAI_API_KEY',
'image_generation.openai.api_version': 'IMAGES_OPENAI_API_VERSION',
'image_generation.openai.params': 'IMAGES_OPENAI_API_PARAMS',
'image_generation.prompt.enable': 'ENABLE_IMAGE_PROMPT_GENERATION',
'image_generation.size': 'IMAGE_SIZE',
'image_generation.steps': 'IMAGE_STEPS',
'images.edit.comfyui.api_key': 'IMAGES_EDIT_COMFYUI_API_KEY',
'images.edit.comfyui.base_url': 'IMAGES_EDIT_COMFYUI_BASE_URL',
'images.edit.comfyui.nodes': 'IMAGES_EDIT_COMFYUI_WORKFLOW_NODES',
'images.edit.comfyui.workflow': 'IMAGES_EDIT_COMFYUI_WORKFLOW',
'images.edit.enable': 'ENABLE_IMAGE_EDIT',
'images.edit.engine': 'IMAGE_EDIT_ENGINE',
'images.edit.gemini.api_base_url': 'IMAGES_EDIT_GEMINI_API_BASE_URL',
'images.edit.gemini.api_key': 'IMAGES_EDIT_GEMINI_API_KEY',
'images.edit.model': 'IMAGE_EDIT_MODEL',
'images.edit.openai.api_base_url': 'IMAGES_EDIT_OPENAI_API_BASE_URL',
'images.edit.openai.api_key': 'IMAGES_EDIT_OPENAI_API_KEY',
'images.edit.openai.api_version': 'IMAGES_EDIT_OPENAI_API_VERSION',
'images.edit.size': 'IMAGE_EDIT_SIZE',
'ldap.enable': 'ENABLE_LDAP',
'ldap.group.enable_creation': 'ENABLE_LDAP_GROUP_CREATION',
'ldap.group.enable_management': 'ENABLE_LDAP_GROUP_MANAGEMENT',
'ldap.server.app_dn': 'LDAP_APP_DN',
'ldap.server.app_password': 'LDAP_APP_PASSWORD',
'ldap.server.attribute_for_groups': 'LDAP_ATTRIBUTE_FOR_GROUPS',
'ldap.server.attribute_for_mail': 'LDAP_ATTRIBUTE_FOR_MAIL',
'ldap.server.attribute_for_username': 'LDAP_ATTRIBUTE_FOR_USERNAME',
'ldap.server.ca_cert_file': 'LDAP_CA_CERT_FILE',
'ldap.server.ciphers': 'LDAP_CIPHERS',
'ldap.server.host': 'LDAP_SERVER_HOST',
'ldap.server.label': 'LDAP_SERVER_LABEL',
'ldap.server.port': 'LDAP_SERVER_PORT',
'ldap.server.search_filter': 'LDAP_SEARCH_FILTER',
'ldap.server.use_tls': 'LDAP_USE_TLS',
'ldap.server.users_dn': 'LDAP_SEARCH_BASE',
'ldap.server.validate_cert': 'LDAP_VALIDATE_CERT',
'memories.enable': 'ENABLE_MEMORIES',
'models.base_models_cache': 'ENABLE_BASE_MODELS_CACHE',
'models.default_metadata': 'DEFAULT_MODEL_METADATA',
'models.default_params': 'DEFAULT_MODEL_PARAMS',
'notes.enable': 'ENABLE_NOTES',
# OAuth — direct paths
'oauth.admin_roles': 'OAUTH_ADMIN_ROLES',
'oauth.allowed_domains': 'OAUTH_ALLOWED_DOMAINS',
'oauth.allowed_roles': 'OAUTH_ALLOWED_ROLES',
'oauth.audience': 'OAUTH_AUDIENCE',
'oauth.auto_redirect': 'OAUTH_AUTO_REDIRECT',
'oauth.blocked_groups': 'OAUTH_BLOCKED_GROUPS',
'oauth.client.timeout': 'OAUTH_CLIENT_TIMEOUT',
'oauth.enable_group_creation': 'ENABLE_OAUTH_GROUP_CREATION',
'oauth.enable_group_mapping': 'ENABLE_OAUTH_GROUP_MANAGEMENT',
'oauth.enable_role_mapping': 'ENABLE_OAUTH_ROLE_MANAGEMENT',
'oauth.enable_signup': 'ENABLE_OAUTH_SIGNUP',
'oauth.group_default_share': 'OAUTH_GROUP_DEFAULT_SHARE',
'oauth.merge_accounts_by_email': 'OAUTH_MERGE_ACCOUNTS_BY_EMAIL',
'oauth.refresh_token_include_scope': 'OAUTH_REFRESH_TOKEN_INCLUDE_SCOPE',
'oauth.roles_claim': 'OAUTH_ROLES_CLAIM',
'oauth.update_email_on_login': 'OAUTH_UPDATE_EMAIL_ON_LOGIN',
'oauth.update_name_on_login': 'OAUTH_UPDATE_NAME_ON_LOGIN',
'oauth.update_picture_on_login': 'OAUTH_UPDATE_PICTURE_ON_LOGIN',
# OAuth — generic provider paths
'oauth.client_id': 'OAUTH_CLIENT_ID',
'oauth.client_secret': 'OAUTH_CLIENT_SECRET',
'oauth.code_challenge_method': 'OAUTH_CODE_CHALLENGE_METHOD',
'oauth.email_claim': 'OAUTH_EMAIL_CLAIM',
'oauth.end_session_endpoint': 'OPENID_END_SESSION_ENDPOINT',
'oauth.group_claim': 'OAUTH_GROUP_CLAIM',
'oauth.picture_claim': 'OAUTH_PICTURE_CLAIM',
'oauth.provider_name': 'OAUTH_PROVIDER_NAME',
'oauth.provider_url': 'OPENID_PROVIDER_URL',
'oauth.redirect_uri': 'OPENID_REDIRECT_URI',
'oauth.scopes': 'OAUTH_SCOPES',
'oauth.sub_claim': 'OAUTH_SUB_CLAIM',
'oauth.timeout': 'OAUTH_TIMEOUT',
'oauth.token_endpoint_auth_method': 'OAUTH_TOKEN_ENDPOINT_AUTH_METHOD',
'oauth.username_claim': 'OAUTH_USERNAME_CLAIM',
# OAuth — OIDC nested paths (flattened)
'oauth.oidc.avatar_claim': 'OAUTH_PICTURE_CLAIM',
'oauth.oidc.client_id': 'OAUTH_CLIENT_ID',
'oauth.oidc.client_secret': 'OAUTH_CLIENT_SECRET',
'oauth.oidc.code_challenge_method': 'OAUTH_CODE_CHALLENGE_METHOD',
'oauth.oidc.email_claim': 'OAUTH_EMAIL_CLAIM',
'oauth.oidc.end_session_endpoint': 'OPENID_END_SESSION_ENDPOINT',
'oauth.oidc.group_claim': 'OAUTH_GROUP_CLAIM', # renamed from OAUTH_GROUPS_CLAIM
'oauth.oidc.oauth_timeout': 'OAUTH_TIMEOUT',
'oauth.oidc.provider_name': 'OAUTH_PROVIDER_NAME',
'oauth.oidc.provider_url': 'OPENID_PROVIDER_URL',
'oauth.oidc.redirect_uri': 'OPENID_REDIRECT_URI',
'oauth.oidc.scopes': 'OAUTH_SCOPES',
'oauth.oidc.sub_claim': 'OAUTH_SUB_CLAIM',
'oauth.oidc.token_endpoint_auth_method': 'OAUTH_TOKEN_ENDPOINT_AUTH_METHOD',
'oauth.oidc.username_claim': 'OAUTH_USERNAME_CLAIM',
# OAuth — provider-specific
'oauth.feishu.client_id': 'FEISHU_CLIENT_ID',
'oauth.feishu.client_secret': 'FEISHU_CLIENT_SECRET',
'oauth.feishu.redirect_uri': 'FEISHU_REDIRECT_URI',
'oauth.feishu.scope': 'FEISHU_OAUTH_SCOPE',
'oauth.github.client_id': 'GITHUB_CLIENT_ID',
'oauth.github.client_secret': 'GITHUB_CLIENT_SECRET',
'oauth.github.redirect_uri': 'GITHUB_CLIENT_REDIRECT_URI',
'oauth.github.scope': 'GITHUB_CLIENT_SCOPE',
'oauth.google.client_id': 'GOOGLE_CLIENT_ID',
'oauth.google.client_secret': 'GOOGLE_CLIENT_SECRET',
'oauth.google.redirect_uri': 'GOOGLE_REDIRECT_URI',
'oauth.google.scope': 'GOOGLE_OAUTH_SCOPE',
'oauth.microsoft.client_id': 'MICROSOFT_CLIENT_ID',
'oauth.microsoft.client_secret': 'MICROSOFT_CLIENT_SECRET',
'oauth.microsoft.login_base_url': 'MICROSOFT_CLIENT_LOGIN_BASE_URL',
'oauth.microsoft.picture_url': 'MICROSOFT_CLIENT_PICTURE_URL',
'oauth.microsoft.redirect_uri': 'MICROSOFT_REDIRECT_URI',
'oauth.microsoft.scope': 'MICROSOFT_OAUTH_SCOPE',
'oauth.microsoft.tenant_id': 'MICROSOFT_CLIENT_TENANT_ID',
# Ollama / OpenAI
'ollama.api_configs': 'OLLAMA_API_CONFIGS',
'ollama.base_urls': 'OLLAMA_BASE_URLS',
'ollama.enable': 'ENABLE_OLLAMA_API',
'onedrive.enable': 'ENABLE_ONEDRIVE_INTEGRATION',
'onedrive.sharepoint_tenant_id': 'ONEDRIVE_SHAREPOINT_TENANT_ID',
'onedrive.sharepoint_url': 'ONEDRIVE_SHAREPOINT_URL',
'openai.api_base_urls': 'OPENAI_API_BASE_URLS',
'openai.api_configs': 'OPENAI_API_CONFIGS',
'openai.api_keys': 'OPENAI_API_KEYS',
'openai.enable': 'ENABLE_OPENAI_API',
# RAG
'rag.content_extraction_engine': 'CONTENT_EXTRACTION_ENGINE',
'rag.datalab_marker_use_llm': 'DATALAB_MARKER_USE_LLM',
'rag.mistral_ocr_api_base_url': 'MISTRAL_OCR_API_BASE_URL',
'rag.azure_openai.api_key': 'RAG_AZURE_OPENAI_API_KEY',
'rag.azure_openai.api_version': 'RAG_AZURE_OPENAI_API_VERSION',
'rag.azure_openai.base_url': 'RAG_AZURE_OPENAI_BASE_URL',
'rag.bypass_embedding_and_retrieval': 'BYPASS_EMBEDDING_AND_RETRIEVAL',
'rag.chunk_min_size_target': 'CHUNK_MIN_SIZE_TARGET',
'rag.chunk_overlap': 'CHUNK_OVERLAP',
'rag.chunk_size': 'CHUNK_SIZE',
'rag.datalab_marker_additional_config': 'DATALAB_MARKER_ADDITIONAL_CONFIG',
'rag.datalab_marker_api_base_url': 'DATALAB_MARKER_API_BASE_URL',
'rag.datalab_marker_api_key': 'DATALAB_MARKER_API_KEY',
'rag.datalab_marker_disable_image_extraction': 'DATALAB_MARKER_DISABLE_IMAGE_EXTRACTION',
'rag.datalab_marker_force_ocr': 'DATALAB_MARKER_FORCE_OCR',
'rag.datalab_marker_format_lines': 'DATALAB_MARKER_FORMAT_LINES',
'rag.datalab_marker_output_format': 'DATALAB_MARKER_OUTPUT_FORMAT',
'rag.datalab_marker_paginate': 'DATALAB_MARKER_PAGINATE',
'rag.datalab_marker_skip_cache': 'DATALAB_MARKER_SKIP_CACHE',
'rag.datalab_marker_strip_existing_ocr': 'DATALAB_MARKER_STRIP_EXISTING_OCR',
'rag.docling_api_key': 'DOCLING_API_KEY',
'rag.docling_params': 'DOCLING_PARAMS',
'rag.docling_server_url': 'DOCLING_SERVER_URL',
'rag.document_intelligence_endpoint': 'DOCUMENT_INTELLIGENCE_ENDPOINT',
'rag.document_intelligence_key': 'DOCUMENT_INTELLIGENCE_KEY',
'rag.document_intelligence_model': 'DOCUMENT_INTELLIGENCE_MODEL',
'rag.embedding_batch_size': 'RAG_EMBEDDING_BATCH_SIZE',
'rag.embedding_concurrent_requests': 'RAG_EMBEDDING_CONCURRENT_REQUESTS',
'rag.embedding_engine': 'RAG_EMBEDDING_ENGINE',
'rag.embedding_model': 'RAG_EMBEDDING_MODEL',
'rag.enable_async_embedding': 'ENABLE_ASYNC_EMBEDDING',
'rag.enable_hybrid_search': 'ENABLE_RAG_HYBRID_SEARCH',
'rag.enable_hybrid_search_enriched_texts': 'ENABLE_RAG_HYBRID_SEARCH_ENRICHED_TEXTS',
'rag.enable_markdown_header_text_splitter': 'ENABLE_MARKDOWN_HEADER_TEXT_SPLITTER',
'rag.external_document_loader_api_key': 'EXTERNAL_DOCUMENT_LOADER_API_KEY',
'rag.external_document_loader_url': 'EXTERNAL_DOCUMENT_LOADER_URL',
'rag.external_reranker_api_key': 'RAG_EXTERNAL_RERANKER_API_KEY',
'rag.external_reranker_timeout': 'RAG_EXTERNAL_RERANKER_TIMEOUT',
'rag.external_reranker_url': 'RAG_EXTERNAL_RERANKER_URL',
'rag.file.allowed_extensions': 'RAG_ALLOWED_FILE_EXTENSIONS',
'rag.file.max_count': 'RAG_FILE_MAX_COUNT',
'rag.file.max_size': 'RAG_FILE_MAX_SIZE',
'rag.full_context': 'RAG_FULL_CONTEXT',
'rag.hybrid_bm25_weight': 'RAG_HYBRID_BM25_WEIGHT',
'rag.mineru_api_key': 'MINERU_API_KEY',
'rag.mineru_api_mode': 'MINERU_API_MODE',
'rag.mineru_api_timeout': 'MINERU_API_TIMEOUT',
'rag.mineru_api_url': 'MINERU_API_URL',
'rag.mineru_file_extensions': 'MINERU_FILE_EXTENSIONS',
'rag.mineru_params': 'MINERU_PARAMS',
'rag.mistral_ocr_api_key': 'MISTRAL_OCR_API_KEY',
'rag.ollama.key': 'RAG_OLLAMA_API_KEY',
'rag.ollama.url': 'RAG_OLLAMA_BASE_URL',
'rag.openai_api_base_url': 'RAG_OPENAI_API_BASE_URL',
'rag.openai_api_key': 'RAG_OPENAI_API_KEY',
'rag.paddleocr_vl_base_url': 'PADDLEOCR_VL_BASE_URL',
'rag.paddleocr_vl_token': 'PADDLEOCR_VL_TOKEN',
'rag.pdf_extract_images': 'PDF_EXTRACT_IMAGES',
'rag.pdf_loader_mode': 'PDF_LOADER_MODE',
'rag.relevance_threshold': 'RAG_RELEVANCE_THRESHOLD',
'rag.reranking_batch_size': 'RAG_RERANKING_BATCH_SIZE',
'rag.reranking_engine': 'RAG_RERANKING_ENGINE',
'rag.reranking_model': 'RAG_RERANKING_MODEL',
'rag.template': 'RAG_TEMPLATE',
'rag.text_splitter': 'RAG_TEXT_SPLITTER',
'rag.tika_server_url': 'TIKA_SERVER_URL',
'rag.tiktoken_encoding_name': 'TIKTOKEN_ENCODING_NAME',
'rag.top_k': 'RAG_TOP_K',
'rag.top_k_reranker': 'RAG_TOP_K_RERANKER',
# RAG — Web
'rag.web.fetch.max_content_length': 'WEB_FETCH_MAX_CONTENT_LENGTH',
'rag.web.loader.concurrent_requests': 'WEB_LOADER_CONCURRENT_REQUESTS',
'rag.web.loader.engine': 'WEB_LOADER_ENGINE',
'rag.web.loader.external_web_loader_api_key': 'EXTERNAL_WEB_LOADER_API_KEY',
'rag.web.loader.external_web_loader_url': 'EXTERNAL_WEB_LOADER_URL',
'rag.web.loader.firecrawl_api_key': 'FIRECRAWL_API_KEY',
'rag.web.loader.firecrawl_api_url': 'FIRECRAWL_API_BASE_URL',
'rag.web.loader.firecrawl_timeout': 'FIRECRAWL_TIMEOUT',
'rag.web.loader.playwright_timeout': 'PLAYWRIGHT_TIMEOUT',
'rag.web.loader.playwright_ws_url': 'PLAYWRIGHT_WS_URL',
'rag.web.loader.ssl_verification': 'ENABLE_WEB_LOADER_SSL_VERIFICATION',
'rag.web.loader.timeout': 'WEB_LOADER_TIMEOUT',
'rag.web.search.azure_ai_search_api_key': 'AZURE_AI_SEARCH_API_KEY',
'rag.web.search.azure_ai_search_endpoint': 'AZURE_AI_SEARCH_ENDPOINT',
'rag.web.search.azure_ai_search_index_name': 'AZURE_AI_SEARCH_INDEX_NAME',
'rag.web.search.bing_search_v7_endpoint': 'BING_SEARCH_V7_ENDPOINT',
'rag.web.search.bing_search_v7_subscription_key': 'BING_SEARCH_V7_SUBSCRIPTION_KEY',
'rag.web.search.bocha_search_api_key': 'BOCHA_SEARCH_API_KEY',
'rag.web.search.brave_search_api_key': 'BRAVE_SEARCH_API_KEY',
'rag.web.search.brave_search_context_tokens': 'BRAVE_SEARCH_CONTEXT_TOKENS',
'rag.web.search.bypass_embedding_and_retrieval': 'BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL',
'rag.web.search.bypass_web_loader': 'BYPASS_WEB_SEARCH_WEB_LOADER',
'rag.web.search.concurrent_requests': 'WEB_SEARCH_CONCURRENT_REQUESTS',
'rag.web.search.ddgs_backend': 'DDGS_BACKEND',
'rag.web.search.domain.filter_list': 'WEB_SEARCH_DOMAIN_FILTER_LIST',
'rag.web.search.enable': 'ENABLE_WEB_SEARCH',
'rag.web.search.engine': 'WEB_SEARCH_ENGINE',
'rag.web.search.exa_api_key': 'EXA_API_KEY',
'rag.web.search.external_web_search_api_key': 'EXTERNAL_WEB_SEARCH_API_KEY',
'rag.web.search.external_web_search_url': 'EXTERNAL_WEB_SEARCH_URL',
'rag.web.search.google_pse_api_key': 'GOOGLE_PSE_API_KEY',
'rag.web.search.google_pse_engine_id': 'GOOGLE_PSE_ENGINE_ID',
'rag.web.search.jina_api_base_url': 'JINA_API_BASE_URL',
'rag.web.search.jina_api_key': 'JINA_API_KEY',
'rag.web.search.kagi_search_api_key': 'KAGI_SEARCH_API_KEY',
'rag.web.search.linkup_api_key': 'LINKUP_API_KEY',
'rag.web.search.linkup_search_params': 'LINKUP_SEARCH_PARAMS',
'rag.web.search.mojeek_search_api_key': 'MOJEEK_SEARCH_API_KEY',
'rag.web.search.ollama_cloud_api_key': 'OLLAMA_CLOUD_WEB_SEARCH_API_KEY',
'rag.web.search.perplexity_api_key': 'PERPLEXITY_API_KEY',
'rag.web.search.perplexity_model': 'PERPLEXITY_MODEL',
'rag.web.search.perplexity_search_api_url': 'PERPLEXITY_SEARCH_API_URL',
'rag.web.search.perplexity_search_context_usage': 'PERPLEXITY_SEARCH_CONTEXT_USAGE',
'rag.web.search.result_count': 'WEB_SEARCH_RESULT_COUNT',
'rag.web.search.searchapi_api_key': 'SEARCHAPI_API_KEY',
'rag.web.search.searchapi_engine': 'SEARCHAPI_ENGINE',
'rag.web.search.searxng_language': 'SEARXNG_LANGUAGE',
'rag.web.search.searxng_query_url': 'SEARXNG_QUERY_URL',
'rag.web.search.serpapi_api_key': 'SERPAPI_API_KEY',
'rag.web.search.serpapi_engine': 'SERPAPI_ENGINE',
'rag.web.search.serper_api_key': 'SERPER_API_KEY',
'rag.web.search.serply_api_key': 'SERPLY_API_KEY',
'rag.web.search.serpstack_api_key': 'SERPSTACK_API_KEY',
'rag.web.search.serpstack_https': 'SERPSTACK_HTTPS',
'rag.web.search.sougou_api_sid': 'SOUGOU_API_SID',
'rag.web.search.sougou_api_sk': 'SOUGOU_API_SK',
'rag.web.search.tavily_api_key': 'TAVILY_API_KEY',
'rag.web.search.tavily_extract_depth': 'TAVILY_EXTRACT_DEPTH',
'rag.web.search.trust_env': 'WEB_SEARCH_TRUST_ENV',
'rag.web.search.yacy_password': 'YACY_PASSWORD',
'rag.web.search.yacy_query_url': 'YACY_QUERY_URL',
'rag.web.search.yacy_username': 'YACY_USERNAME',
'rag.web.search.yandex_web_search_api_key': 'YANDEX_WEB_SEARCH_API_KEY',
'rag.web.search.yandex_web_search_config': 'YANDEX_WEB_SEARCH_CONFIG',
'rag.web.search.yandex_web_search_url': 'YANDEX_WEB_SEARCH_URL',
'rag.web.search.youcom_api_key': 'YOUCOM_API_KEY',
'rag.youtube_loader_language': 'YOUTUBE_LOADER_LANGUAGE',
'rag.youtube_loader_proxy_url': 'YOUTUBE_LOADER_PROXY_URL',
# Tasks
'task.autocomplete.enable': 'ENABLE_AUTOCOMPLETE_GENERATION',
'task.autocomplete.input_max_length': 'AUTOCOMPLETE_GENERATION_INPUT_MAX_LENGTH',
'task.autocomplete.prompt_template': 'AUTOCOMPLETE_GENERATION_PROMPT_TEMPLATE',
'task.follow_up.enable': 'ENABLE_FOLLOW_UP_GENERATION',
'task.follow_up.prompt_template': 'FOLLOW_UP_GENERATION_PROMPT_TEMPLATE',
'task.image.prompt_template': 'IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE',
'task.model.default': 'TASK_MODEL',
'task.model.external': 'TASK_MODEL_EXTERNAL',
'task.query.prompt_template': 'QUERY_GENERATION_PROMPT_TEMPLATE',
'task.query.retrieval.enable': 'ENABLE_RETRIEVAL_QUERY_GENERATION',
'task.query.search.enable': 'ENABLE_SEARCH_QUERY_GENERATION',
'task.tags.enable': 'ENABLE_TAGS_GENERATION',
'task.tags.prompt_template': 'TAGS_GENERATION_PROMPT_TEMPLATE',
'task.title.enable': 'ENABLE_TITLE_GENERATION',
'task.title.prompt_template': 'TITLE_GENERATION_PROMPT_TEMPLATE',
'task.tools.prompt_template': 'TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE',
'task.voice.prompt.enable': 'ENABLE_VOICE_MODE_PROMPT',
'task.voice.prompt_template': 'VOICE_MODE_PROMPT_TEMPLATE',
# Misc
'terminal_server.connections': 'TERMINAL_SERVER_CONNECTIONS',
'tool_server.connections': 'TOOL_SERVER_CONNECTIONS',
'ui.banners': 'WEBUI_BANNERS',
'ui.default_group_id': 'DEFAULT_GROUP_ID',
'ui.default_locale': 'DEFAULT_LOCALE',
'ui.default_models': 'DEFAULT_MODELS',
'ui.default_pinned_models': 'DEFAULT_PINNED_MODELS',
'ui.default_user_role': 'DEFAULT_USER_ROLE',
'ui.enable_community_sharing': 'ENABLE_COMMUNITY_SHARING',
'ui.enable_login_form': 'ENABLE_LOGIN_FORM',
'ui.enable_message_rating': 'ENABLE_MESSAGE_RATING',
'ui.enable_password_change_form': 'ENABLE_PASSWORD_CHANGE_FORM',
'ui.enable_signup': 'ENABLE_SIGNUP',
'ui.enable_user_webhooks': 'ENABLE_USER_WEBHOOKS',
'ui.model_order_list': 'MODEL_ORDER_LIST',
'ui.pending_user_overlay_content': 'PENDING_USER_OVERLAY_CONTENT',
'ui.pending_user_overlay_title': 'PENDING_USER_OVERLAY_TITLE',
'ui.prompt_suggestions': 'DEFAULT_PROMPT_SUGGESTIONS',
'ui.watermark': 'RESPONSE_WATERMARK',
'user.permissions': 'USER_PERMISSIONS',
'users.enable_status': 'ENABLE_USER_STATUS',
'webhook_url': 'WEBHOOK_URL',
'webui.url': 'WEBUI_URL',
}
STORAGE_KEY_REWRITES = {
'oauth.refresh_token_include_scope': 'oauth.refresh_token.include_scope',
'rag.openai_api_base_url': 'rag.openai.api_base_url',
'rag.openai_api_key': 'rag.openai.api_key',
'rag.ollama.url': 'rag.ollama.base_url',
'rag.ollama.key': 'rag.ollama.api_key',
'oauth.oidc.avatar_claim': 'oauth.picture_claim',
'oauth.oidc.client_id': 'oauth.client_id',
'oauth.oidc.client_secret': 'oauth.client_secret',
'oauth.oidc.code_challenge_method': 'oauth.code_challenge_method',
'oauth.oidc.email_claim': 'oauth.email_claim',
'oauth.oidc.end_session_endpoint': 'oauth.end_session_endpoint',
'oauth.oidc.group_claim': 'oauth.group_claim',
'oauth.oidc.oauth_timeout': 'oauth.timeout',
'oauth.oidc.provider_name': 'oauth.provider_name',
'oauth.oidc.provider_url': 'oauth.provider_url',
'oauth.oidc.redirect_uri': 'oauth.redirect_uri',
'oauth.oidc.scopes': 'oauth.scopes',
'oauth.oidc.sub_claim': 'oauth.sub_claim',
'oauth.oidc.token_endpoint_auth_method': 'oauth.token_endpoint_auth_method',
'oauth.oidc.username_claim': 'oauth.username_claim',
}
LEGACY_KEY_TO_STORAGE_KEY = {
legacy_key: STORAGE_KEY_REWRITES.get(blob_path, blob_path) for blob_path, legacy_key in BLOB_PATH_TO_KEY.items()
}
def _walk_blob(data: dict, prefix: str = '') -> dict:
"""Recursively walk a nested config blob, preserving known config values.
Some config values are intentionally dictionaries, e.g. OPENAI_API_CONFIGS
and OLLAMA_API_CONFIGS. Once the current path is a known config key, keep
that value intact instead of flattening its internals into orphaned rows.
"""
result = {}
for key, value in data.items():
path = f'{prefix}{key}' if not prefix else f'{prefix}.{key}'
if path in BLOB_PATH_TO_KEY or path in LEGACY_KEY_TO_STORAGE_KEY:
result[path] = value
elif isinstance(value, dict):
result.update(_walk_blob(value, path))
else:
result[path] = value
return result
def upgrade() -> None:
"""Reshape config from single-row JSON blob to per-key rows."""
conn = op.get_bind()
inspector = sa.inspect(conn)
table_names = set(inspector.get_table_names())
config_columns = (
{column['name'] for column in inspector.get_columns('config')} if 'config' in table_names else set()
)
has_old_config = {'id', 'data'}.issubset(config_columns)
has_new_config = {'key', 'value'}.issubset(config_columns)
# Ad-hoc table reference for reading the old schema
old_config = sa.table(
'config',
sa.column('id', sa.Integer),
sa.column('data', sa.JSON),
)
# 1. Read existing blob
blob_data = {}
if has_old_config:
try:
result = conn.execute(sa.select(old_config.c.data).order_by(old_config.c.id.desc()).limit(1))
row = result.fetchone()
if row and row[0]:
raw = row[0]
blob_data = json.loads(raw) if isinstance(raw, str) else raw
except Exception:
pass # Table might be partially migrated or empty
# 2. Preserve old blob table for rollback/inspection, then create per-key table.
if has_old_config:
if 'config_old' in table_names:
op.drop_table('config_old')
op.rename_table('config', 'config_old')
# 3. Create new per-key table
new_config = (
sa.table(
'config',
sa.column('key', sa.Text),
sa.column('value', sa.JSON()),
sa.column('updated_at', sa.BigInteger),
)
if has_new_config
else op.create_table(
'config',
sa.Column('key', sa.Text(), primary_key=True),
sa.Column('value', sa.JSON(), nullable=False),
sa.Column('updated_at', sa.BigInteger(), nullable=True),
)
)
# 4. Flatten blob and insert per-key rows
if blob_data:
flat = _walk_blob(blob_data)
# Keep stable dot-notation paths as the database keys.
# Known legacy env-style keys are rewritten to their dotted keys; unknown
# keys are still copied so custom/future config is not silently lost.
rows = {}
for blob_path, value in flat.items():
if blob_path in BLOB_PATH_TO_KEY:
storage_key = STORAGE_KEY_REWRITES.get(blob_path, blob_path)
elif blob_path in LEGACY_KEY_TO_STORAGE_KEY:
storage_key = LEGACY_KEY_TO_STORAGE_KEY[blob_path]
else:
storage_key = STORAGE_KEY_REWRITES.get(blob_path, blob_path)
if storage_key not in rows:
rows[storage_key] = value
# Batch insert via SQLAlchemy table reference
if rows:
now = int(time.time())
op.bulk_insert(
new_config,
[{'key': k, 'value': v, 'updated_at': now} for k, v in rows.items()],
)
def downgrade() -> None:
"""Restore preserved old single-row config table when available."""
conn = op.get_bind()
inspector = sa.inspect(conn)
table_names = set(inspector.get_table_names())
if 'config_old' in table_names:
if 'config' in table_names:
op.drop_table('config')
op.rename_table('config_old', 'config')
return
config_columns = (
{column['name'] for column in inspector.get_columns('config')} if 'config' in table_names else set()
)
has_per_key_config = {'key', 'value'}.issubset(config_columns)
blob_data = {}
if has_per_key_config:
config = sa.table(
'config',
sa.column('key', sa.Text),
sa.column('value', sa.JSON),
)
for key, value in conn.execute(sa.select(config.c.key, config.c.value)):
blob_data[key] = json.loads(value) if isinstance(value, str) else value
op.drop_table('config')
if 'config' in table_names and not has_per_key_config:
return
old_config = op.create_table(
'config',
sa.Column('id', sa.Integer(), primary_key=True),
sa.Column('data', sa.JSON(), nullable=False),
sa.Column('version', sa.Integer(), nullable=False, server_default='0'),
sa.Column('created_at', sa.DateTime(), nullable=False, server_default=sa.func.now()),
sa.Column('updated_at', sa.DateTime(), nullable=True),
)
if blob_data:
op.bulk_insert(old_config, [{'data': blob_data, 'version': 0}])

View file

@ -0,0 +1,40 @@
"""add memory path and meta
Revision ID: 42e2978c7933
Revises: 7b3f2a9c1d4e
Create Date: 2026-06-29 05:35:50.565887
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
revision: str = '42e2978c7933'
down_revision: Union[str, None] = '7b3f2a9c1d4e'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
conn = op.get_bind()
inspector = sa.inspect(conn)
columns = {column['name'] for column in inspector.get_columns('memory')}
if 'path' not in columns:
op.add_column('memory', sa.Column('path', sa.Text(), nullable=True))
if 'meta' not in columns:
op.add_column('memory', sa.Column('meta', sa.JSON(), nullable=True))
def downgrade() -> None:
conn = op.get_bind()
inspector = sa.inspect(conn)
columns = {column['name'] for column in inspector.get_columns('memory')}
if 'meta' in columns:
op.drop_column('memory', 'meta')
if 'path' in columns:
op.drop_column('memory', 'path')

View file

@ -0,0 +1,74 @@
"""add missing primary keys to legacy peewee tables
Revision ID: 461111b60977
Revises: 3c9b0ca343fd
Create Date: 2026-05-14 04:38:14.000000
"""
from typing import Sequence, Union
import sqlalchemy as sa
from alembic import op
# revision identifiers, used by Alembic.
revision: str = '461111b60977'
down_revision: Union[str, None] = '3c9b0ca343fd'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
# Tables bootstrapped by the old Peewee migration layer that may have
# UNIQUE(id) but no PRIMARY KEY constraint. Fresh Alembic installs
# already have correct PKs from 7e5b5dc7342b_init.py.
# 'tag' uses a composite PK since the same tag name can exist for multiple users.
LEGACY_TABLES = {
'auth': ['id'],
'chat': ['id'],
'chatidtag': ['id'],
'document': ['id'],
'file': ['id'],
'function': ['id'],
'memory': ['id'],
'model': ['id'],
'prompt': ['id'],
'tag': ['id', 'user_id'],
'tool': ['id'],
'user': ['id'],
}
def upgrade() -> None:
conn = op.get_bind()
inspector = sa.inspect(conn)
existing_tables = set(inspector.get_table_names())
for table_name, pk_columns in LEGACY_TABLES.items():
if table_name not in existing_tables:
continue
pk = inspector.get_pk_constraint(table_name)
pk_cols = pk.get('constrained_columns', [])
# Already has the correct PK — nothing to do
if sorted(pk_cols) == sorted(pk_columns):
continue
# Check that all PK columns exist
columns = {c['name'] for c in inspector.get_columns(table_name)}
if not all(c in columns for c in pk_columns):
continue
print(f"Promoting UNIQUE(id) -> PRIMARY KEY({', '.join(pk_columns)}) for '{table_name}'")
conn.execute(sa.text(f'DROP TABLE IF EXISTS _alembic_tmp_{table_name}'))
with op.batch_alter_table(table_name) as batch_op:
# Drop existing PK if any (e.g. on wrong column)
if pk_cols and pk.get('name'):
batch_op.drop_constraint(pk['name'], type_='primary')
batch_op.create_primary_key(f'pk_{table_name}', pk_columns)
def downgrade() -> None:
# Downgrade is a no-op — we don't want to remove PKs
pass

View file

@ -6,8 +6,8 @@ Create Date: 2024-10-23 03:00:00.000000
"""
from alembic import op
import sqlalchemy as sa
from alembic import op
revision = '4ace53fd72c8'
down_revision = 'af906e964978'
@ -16,6 +16,18 @@ depends_on = None
def upgrade():
conn = op.get_bind()
inspector = sa.inspect(conn)
columns = {c['name']: c for c in inspector.get_columns('folder')}
created_at_col = columns.get('created_at')
if not created_at_col:
return
# Only convert if still DateTime — skip if already BigInteger
if isinstance(created_at_col['type'], sa.BigInteger):
return
# Perform safe alterations using batch operation
with op.batch_alter_table('folder', schema=None) as batch_op:
# Step 1: Remove server defaults for created_at and updated_at
@ -48,20 +60,24 @@ def upgrade():
def downgrade():
# Downgrade: Convert columns back to DateTime and restore defaults
# Convert columns back to DateTime and restore defaults. Mirrors the
# upgrade's postgresql_using cast — without it, Postgres can't
# auto-cast BigInteger → timestamp and aborts with DatatypeMismatch.
with op.batch_alter_table('folder', schema=None) as batch_op:
batch_op.alter_column(
'created_at',
type_=sa.DateTime(),
existing_type=sa.BigInteger(),
existing_nullable=False,
server_default=sa.func.now(), # Restoring server default on downgrade
server_default=sa.func.now(),
postgresql_using='to_timestamp(created_at)::timestamp without time zone',
)
batch_op.alter_column(
'updated_at',
type_=sa.DateTime(),
existing_type=sa.BigInteger(),
existing_nullable=False,
server_default=sa.func.now(), # Restoring server default on downgrade
onupdate=sa.func.now(), # Restoring onupdate behavior if it was there
server_default=sa.func.now(),
onupdate=sa.func.now(),
postgresql_using='to_timestamp(updated_at)::timestamp without time zone',
)

View file

@ -0,0 +1,37 @@
"""add context summary to chat message
Revision ID: 4c5ce3d2f27f
Revises: 3ff2c63645b8
Create Date: 2026-06-18 23:48:08.310063
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = '4c5ce3d2f27f'
down_revision: Union[str, None] = '3ff2c63645b8'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
conn = op.get_bind()
inspector = sa.inspect(conn)
columns = {column['name'] for column in inspector.get_columns('chat_message')}
if 'context_summary' not in columns:
op.add_column('chat_message', sa.Column('context_summary', sa.Text(), nullable=True))
def downgrade() -> None:
conn = op.get_bind()
inspector = sa.inspect(conn)
columns = {column['name'] for column in inspector.get_columns('chat_message')}
if 'context_summary' in columns:
op.drop_column('chat_message', 'context_summary')

View file

@ -8,10 +8,9 @@ Create Date: 2026-05-09 04:29:27.651341
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
import open_webui.internal.db
import sqlalchemy as sa
from alembic import op
# revision identifiers, used by Alembic.
revision: str = '4de81c2a3af1'
@ -20,46 +19,55 @@ branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
import uuid
import time
from sqlalchemy import select, update, insert
from sqlalchemy.sql import table, column
import uuid
from sqlalchemy import insert, select, update
from sqlalchemy.sql import column, table
def upgrade() -> None:
op.create_table(
'pinned_note',
sa.Column('id', sa.Text(), nullable=False),
sa.Column('user_id', sa.Text(), nullable=False),
sa.Column('note_id', sa.Text(), sa.ForeignKey('note.id', ondelete='CASCADE'), nullable=False),
sa.Column('created_at', sa.BigInteger(), nullable=False),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('user_id', 'note_id', name='uq_pinned_note'),
)
conn = op.get_bind()
inspector = sa.inspect(conn)
existing_tables = set(inspector.get_table_names())
note_table = table('note', column('id', sa.Text), column('user_id', sa.Text), column('is_pinned', sa.Boolean))
pinned_note_table = table(
'pinned_note',
column('id', sa.Text),
column('user_id', sa.Text),
column('note_id', sa.Text),
column('created_at', sa.BigInteger),
)
notes = conn.execute(select(note_table.c.id, note_table.c.user_id).where(note_table.c.is_pinned == True)).fetchall()
if notes:
now = int(time.time_ns())
conn.execute(
insert(pinned_note_table),
[{'id': str(uuid.uuid4()), 'user_id': note[1], 'note_id': note[0], 'created_at': now} for note in notes],
if 'pinned_note' not in existing_tables:
op.create_table(
'pinned_note',
sa.Column('id', sa.Text(), nullable=False),
sa.Column('user_id', sa.Text(), nullable=False),
sa.Column('note_id', sa.Text(), sa.ForeignKey('note.id', ondelete='CASCADE'), nullable=False),
sa.Column('created_at', sa.BigInteger(), nullable=False),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('user_id', 'note_id', name='uq_pinned_note'),
)
with op.batch_alter_table('note', schema=None) as batch_op:
batch_op.drop_column('is_pinned')
note_table = table('note', column('id', sa.Text), column('user_id', sa.Text), column('is_pinned', sa.Boolean))
pinned_note_table = table(
'pinned_note',
column('id', sa.Text),
column('user_id', sa.Text),
column('note_id', sa.Text),
column('created_at', sa.BigInteger),
)
notes = conn.execute(
select(note_table.c.id, note_table.c.user_id).where(note_table.c.is_pinned == True)
).fetchall()
if notes:
now = int(time.time_ns())
conn.execute(
insert(pinned_note_table),
[
{'id': str(uuid.uuid4()), 'user_id': note[1], 'note_id': note[0], 'created_at': now}
for note in notes
],
)
with op.batch_alter_table('note', schema=None) as batch_op:
batch_op.drop_column('is_pinned')
def downgrade() -> None:

View file

@ -0,0 +1,36 @@
"""Add memory (id, user_id) covering index
Revision ID: 55f1302ac17c
Revises: b0018471bbbe
Create Date: 2026-07-24 00:00:00.000000
"""
from typing import Sequence, Union
import sqlalchemy as sa
from alembic import op
revision: str = '55f1302ac17c'
down_revision: Union[str, None] = 'b0018471bbbe'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
conn = op.get_bind()
inspector = sa.inspect(conn)
indexes = {index['name'] for index in inspector.get_indexes('memory')}
if 'ix_memory_id_user_id' not in indexes:
op.create_index('ix_memory_id_user_id', 'memory', ['id', 'user_id'])
def downgrade() -> None:
conn = op.get_bind()
inspector = sa.inspect(conn)
indexes = {index['name'] for index in inspector.get_indexes('memory')}
if 'ix_memory_id_user_id' in indexes:
op.drop_index('ix_memory_id_user_id', table_name='memory')

View file

@ -8,9 +8,8 @@ Create Date: 2026-04-19 16:20:58.162045
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from alembic import op
# revision identifiers, used by Alembic.
revision: str = '56359461a091'
@ -19,58 +18,86 @@ branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def _index_exists(inspector, index_name, table_name):
"""Check if an index already exists on the given table."""
indexes = inspector.get_indexes(table_name)
return any(idx['name'] == index_name for idx in indexes)
def upgrade() -> None:
op.create_table(
'calendar',
sa.Column('id', sa.Text(), nullable=False),
sa.Column('user_id', sa.Text(), nullable=False),
sa.Column('name', sa.Text(), nullable=False),
sa.Column('color', sa.Text(), nullable=True),
sa.Column('is_default', sa.Boolean(), nullable=False),
sa.Column('data', sa.JSON(), nullable=True),
sa.Column('meta', sa.JSON(), nullable=True),
sa.Column('created_at', sa.BigInteger(), nullable=False),
sa.Column('updated_at', sa.BigInteger(), nullable=False),
sa.PrimaryKeyConstraint('id'),
)
op.create_index('ix_calendar_user', 'calendar', ['user_id'], unique=False)
conn = op.get_bind()
inspector = sa.inspect(conn)
tables = inspector.get_table_names()
op.create_table(
'calendar_event',
sa.Column('id', sa.Text(), nullable=False),
sa.Column('calendar_id', sa.Text(), nullable=False),
sa.Column('user_id', sa.Text(), nullable=False),
sa.Column('title', sa.Text(), nullable=False),
sa.Column('description', sa.Text(), nullable=True),
sa.Column('start_at', sa.BigInteger(), nullable=False),
sa.Column('end_at', sa.BigInteger(), nullable=True),
sa.Column('all_day', sa.Boolean(), nullable=False),
sa.Column('rrule', sa.Text(), nullable=True),
sa.Column('color', sa.Text(), nullable=True),
sa.Column('location', sa.Text(), nullable=True),
sa.Column('data', sa.JSON(), nullable=True),
sa.Column('meta', sa.JSON(), nullable=True),
sa.Column('is_cancelled', sa.Boolean(), nullable=False),
sa.Column('created_at', sa.BigInteger(), nullable=False),
sa.Column('updated_at', sa.BigInteger(), nullable=False),
sa.PrimaryKeyConstraint('id'),
)
op.create_index('ix_calendar_event_calendar', 'calendar_event', ['calendar_id', 'start_at'], unique=False)
op.create_index('ix_calendar_event_user_date', 'calendar_event', ['user_id', 'start_at'], unique=False)
if 'calendar' not in tables:
op.create_table(
'calendar',
sa.Column('id', sa.Text(), nullable=False),
sa.Column('user_id', sa.Text(), nullable=False),
sa.Column('name', sa.Text(), nullable=False),
sa.Column('color', sa.Text(), nullable=True),
sa.Column('is_default', sa.Boolean(), nullable=False),
sa.Column('data', sa.JSON(), nullable=True),
sa.Column('meta', sa.JSON(), nullable=True),
sa.Column('created_at', sa.BigInteger(), nullable=False),
sa.Column('updated_at', sa.BigInteger(), nullable=False),
sa.PrimaryKeyConstraint('id'),
)
op.create_table(
'calendar_event_attendee',
sa.Column('id', sa.Text(), nullable=False),
sa.Column('event_id', sa.Text(), nullable=False),
sa.Column('user_id', sa.Text(), nullable=False),
sa.Column('status', sa.Text(), nullable=False),
sa.Column('meta', sa.JSON(), nullable=True),
sa.Column('created_at', sa.BigInteger(), nullable=False),
sa.Column('updated_at', sa.BigInteger(), nullable=False),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('event_id', 'user_id', name='uq_event_attendee'),
)
op.create_index('ix_calendar_event_attendee_user', 'calendar_event_attendee', ['user_id', 'status'], unique=False)
inspector.clear_cache()
if 'calendar' in inspector.get_table_names():
if not _index_exists(inspector, 'ix_calendar_user', 'calendar'):
op.create_index('ix_calendar_user', 'calendar', ['user_id'], unique=False)
if 'calendar_event' not in tables:
op.create_table(
'calendar_event',
sa.Column('id', sa.Text(), nullable=False),
sa.Column('calendar_id', sa.Text(), nullable=False),
sa.Column('user_id', sa.Text(), nullable=False),
sa.Column('title', sa.Text(), nullable=False),
sa.Column('description', sa.Text(), nullable=True),
sa.Column('start_at', sa.BigInteger(), nullable=False),
sa.Column('end_at', sa.BigInteger(), nullable=True),
sa.Column('all_day', sa.Boolean(), nullable=False),
sa.Column('rrule', sa.Text(), nullable=True),
sa.Column('color', sa.Text(), nullable=True),
sa.Column('location', sa.Text(), nullable=True),
sa.Column('data', sa.JSON(), nullable=True),
sa.Column('meta', sa.JSON(), nullable=True),
sa.Column('is_cancelled', sa.Boolean(), nullable=False),
sa.Column('created_at', sa.BigInteger(), nullable=False),
sa.Column('updated_at', sa.BigInteger(), nullable=False),
sa.PrimaryKeyConstraint('id'),
)
inspector.clear_cache()
if 'calendar_event' in inspector.get_table_names():
if not _index_exists(inspector, 'ix_calendar_event_calendar', 'calendar_event'):
op.create_index('ix_calendar_event_calendar', 'calendar_event', ['calendar_id', 'start_at'], unique=False)
if not _index_exists(inspector, 'ix_calendar_event_user_date', 'calendar_event'):
op.create_index('ix_calendar_event_user_date', 'calendar_event', ['user_id', 'start_at'], unique=False)
if 'calendar_event_attendee' not in tables:
op.create_table(
'calendar_event_attendee',
sa.Column('id', sa.Text(), nullable=False),
sa.Column('event_id', sa.Text(), nullable=False),
sa.Column('user_id', sa.Text(), nullable=False),
sa.Column('status', sa.Text(), nullable=False),
sa.Column('meta', sa.JSON(), nullable=True),
sa.Column('created_at', sa.BigInteger(), nullable=False),
sa.Column('updated_at', sa.BigInteger(), nullable=False),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('event_id', 'user_id', name='uq_event_attendee'),
)
inspector.clear_cache()
if 'calendar_event_attendee' in inspector.get_table_names():
if not _index_exists(inspector, 'ix_calendar_event_attendee_user', 'calendar_event_attendee'):
op.create_index(
'ix_calendar_event_attendee_user', 'calendar_event_attendee', ['user_id', 'status'], unique=False
)
def downgrade() -> None:

View file

@ -6,8 +6,8 @@ Create Date: 2024-12-22 03:00:00.000000
"""
from alembic import op
import sqlalchemy as sa
from alembic import op
revision = '57c599a3cb57'
down_revision = '922e7a387820'
@ -16,30 +16,36 @@ depends_on = None
def upgrade():
op.create_table(
'channel',
sa.Column('id', sa.Text(), nullable=False, primary_key=True, unique=True),
sa.Column('user_id', sa.Text()),
sa.Column('name', sa.Text()),
sa.Column('description', sa.Text(), nullable=True),
sa.Column('data', sa.JSON(), nullable=True),
sa.Column('meta', sa.JSON(), nullable=True),
sa.Column('access_control', sa.JSON(), nullable=True),
sa.Column('created_at', sa.BigInteger(), nullable=True),
sa.Column('updated_at', sa.BigInteger(), nullable=True),
)
conn = op.get_bind()
inspector = sa.inspect(conn)
existing_tables = set(inspector.get_table_names())
op.create_table(
'message',
sa.Column('id', sa.Text(), nullable=False, primary_key=True, unique=True),
sa.Column('user_id', sa.Text()),
sa.Column('channel_id', sa.Text(), nullable=True),
sa.Column('content', sa.Text()),
sa.Column('data', sa.JSON(), nullable=True),
sa.Column('meta', sa.JSON(), nullable=True),
sa.Column('created_at', sa.BigInteger(), nullable=True),
sa.Column('updated_at', sa.BigInteger(), nullable=True),
)
if 'channel' not in existing_tables:
op.create_table(
'channel',
sa.Column('id', sa.Text(), nullable=False, primary_key=True, unique=True),
sa.Column('user_id', sa.Text()),
sa.Column('name', sa.Text()),
sa.Column('description', sa.Text(), nullable=True),
sa.Column('data', sa.JSON(), nullable=True),
sa.Column('meta', sa.JSON(), nullable=True),
sa.Column('access_control', sa.JSON(), nullable=True),
sa.Column('created_at', sa.BigInteger(), nullable=True),
sa.Column('updated_at', sa.BigInteger(), nullable=True),
)
if 'message' not in existing_tables:
op.create_table(
'message',
sa.Column('id', sa.Text(), nullable=False, primary_key=True, unique=True),
sa.Column('user_id', sa.Text()),
sa.Column('channel_id', sa.Text(), nullable=True),
sa.Column('content', sa.Text()),
sa.Column('data', sa.JSON(), nullable=True),
sa.Column('meta', sa.JSON(), nullable=True),
sa.Column('created_at', sa.BigInteger(), nullable=True),
sa.Column('updated_at', sa.BigInteger(), nullable=True),
)
def downgrade():

View file

@ -8,9 +8,9 @@ Create Date: 2025-12-10 15:11:39.424601
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
import open_webui.internal.db
import sqlalchemy as sa
from alembic import op
# revision identifiers, used by Alembic.
revision: str = '6283dc0e4d8d'
@ -20,31 +20,38 @@ depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
op.create_table(
'channel_file',
sa.Column('id', sa.Text(), primary_key=True),
sa.Column('user_id', sa.Text(), nullable=False),
sa.Column(
'channel_id',
sa.Text(),
sa.ForeignKey('channel.id', ondelete='CASCADE'),
nullable=False,
),
sa.Column(
'file_id',
sa.Text(),
sa.ForeignKey('file.id', ondelete='CASCADE'),
nullable=False,
),
sa.Column('created_at', sa.BigInteger(), nullable=False),
sa.Column('updated_at', sa.BigInteger(), nullable=False),
# indexes
sa.Index('ix_channel_file_channel_id', 'channel_id'),
sa.Index('ix_channel_file_file_id', 'file_id'),
sa.Index('ix_channel_file_user_id', 'user_id'),
# unique constraints
sa.UniqueConstraint('channel_id', 'file_id', name='uq_channel_file_channel_file'), # prevent duplicate entries
)
conn = op.get_bind()
inspector = sa.inspect(conn)
existing_tables = set(inspector.get_table_names())
if 'channel_file' not in existing_tables:
op.create_table(
'channel_file',
sa.Column('id', sa.Text(), primary_key=True),
sa.Column('user_id', sa.Text(), nullable=False),
sa.Column(
'channel_id',
sa.Text(),
sa.ForeignKey('channel.id', ondelete='CASCADE'),
nullable=False,
),
sa.Column(
'file_id',
sa.Text(),
sa.ForeignKey('file.id', ondelete='CASCADE'),
nullable=False,
),
sa.Column('created_at', sa.BigInteger(), nullable=False),
sa.Column('updated_at', sa.BigInteger(), nullable=False),
# indexes
sa.Index('ix_channel_file_channel_id', 'channel_id'),
sa.Index('ix_channel_file_file_id', 'file_id'),
sa.Index('ix_channel_file_user_id', 'user_id'),
# unique constraints
sa.UniqueConstraint(
'channel_id', 'file_id', name='uq_channel_file_channel_file'
), # prevent duplicate entries
)
def downgrade() -> None:

View file

@ -6,11 +6,12 @@ Create Date: 2024-10-01 14:02:35.241684
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.sql import table, column, select
import json
import sqlalchemy as sa
from alembic import op
from sqlalchemy.sql import column, select, table
revision = '6a39f3d8e55c'
down_revision = 'c0fbf31ca0db'
branch_labels = None
@ -18,62 +19,67 @@ depends_on = None
def upgrade():
# Creating the 'knowledge' table
print('Creating knowledge table')
knowledge_table = op.create_table(
'knowledge',
sa.Column('id', sa.Text(), primary_key=True),
sa.Column('user_id', sa.Text(), nullable=False),
sa.Column('name', sa.Text(), nullable=False),
sa.Column('description', sa.Text(), nullable=True),
sa.Column('data', sa.JSON(), nullable=True),
sa.Column('meta', sa.JSON(), nullable=True),
sa.Column('created_at', sa.BigInteger(), nullable=False),
sa.Column('updated_at', sa.BigInteger(), nullable=True),
)
conn = op.get_bind()
inspector = sa.inspect(conn)
existing_tables = set(inspector.get_table_names())
print('Migrating data from document table to knowledge table')
# Representation of the existing 'document' table
document_table = table(
'document',
column('collection_name', sa.String()),
column('user_id', sa.String()),
column('name', sa.String()),
column('title', sa.Text()),
column('content', sa.Text()),
column('timestamp', sa.BigInteger()),
)
# Select all from existing document table
documents = op.get_bind().execute(
select(
document_table.c.collection_name,
document_table.c.user_id,
document_table.c.name,
document_table.c.title,
document_table.c.content,
document_table.c.timestamp,
if 'knowledge' not in existing_tables:
# Creating the 'knowledge' table
print('Creating knowledge table')
knowledge_table = op.create_table(
'knowledge',
sa.Column('id', sa.Text(), primary_key=True),
sa.Column('user_id', sa.Text(), nullable=False),
sa.Column('name', sa.Text(), nullable=False),
sa.Column('description', sa.Text(), nullable=True),
sa.Column('data', sa.JSON(), nullable=True),
sa.Column('meta', sa.JSON(), nullable=True),
sa.Column('created_at', sa.BigInteger(), nullable=False),
sa.Column('updated_at', sa.BigInteger(), nullable=True),
)
)
# Insert data into knowledge table from document table
for doc in documents:
op.get_bind().execute(
knowledge_table.insert().values(
id=doc.collection_name,
user_id=doc.user_id,
description=doc.name,
meta={
'legacy': True,
'document': True,
'tags': json.loads(doc.content or '{}').get('tags', []),
},
name=doc.title,
created_at=doc.timestamp,
updated_at=doc.timestamp, # using created_at for both created_at and updated_at in project
print('Migrating data from document table to knowledge table')
# Representation of the existing 'document' table
document_table = table(
'document',
column('collection_name', sa.String()),
column('user_id', sa.String()),
column('name', sa.String()),
column('title', sa.Text()),
column('content', sa.Text()),
column('timestamp', sa.BigInteger()),
)
# Select all from existing document table
documents = conn.execute(
select(
document_table.c.collection_name,
document_table.c.user_id,
document_table.c.name,
document_table.c.title,
document_table.c.content,
document_table.c.timestamp,
)
)
# Insert data into knowledge table from document table
for doc in documents:
conn.execute(
knowledge_table.insert().values(
id=doc.collection_name,
user_id=doc.user_id,
description=doc.name,
meta={
'legacy': True,
'document': True,
'tags': json.loads(doc.content or '{}').get('tags', []),
},
name=doc.title,
created_at=doc.timestamp,
updated_at=doc.timestamp,
)
)
def downgrade():
op.drop_table('knowledge')

View file

@ -0,0 +1,64 @@
"""repair double encoded user oauth
Revision ID: 6d09d1bf1f23
Revises: 1ce6ade7d93b
Create Date: 2026-08-10 23:20:20.374826
"""
import json
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
import open_webui.internal.db
# revision identifiers, used by Alembic.
revision: str = '6d09d1bf1f23'
down_revision: Union[str, None] = '1ce6ade7d93b'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
_user = sa.table(
'user',
sa.column('id', sa.Text),
sa.column('oauth', sa.JSON),
)
def _decode_json_object(value: str) -> dict | None:
try:
decoded = json.loads(value)
except Exception:
return None
return decoded if isinstance(decoded, dict) else None
def upgrade() -> None:
conn = op.get_bind()
inspector = sa.inspect(conn)
if 'user' not in inspector.get_table_names():
return
user_columns = {c['name'] for c in inspector.get_columns('user')}
if 'oauth' not in user_columns:
return
rows = conn.execute(sa.select(_user.c.id, _user.c.oauth).where(_user.c.oauth.is_not(None))).fetchall()
for uid, oauth in rows:
if not isinstance(oauth, str):
continue
decoded = _decode_json_object(oauth)
if decoded is None:
continue
conn.execute(sa.update(_user).where(_user.c.id == uid).values(oauth=decoded))
def downgrade() -> None:
pass

View file

@ -6,8 +6,8 @@ Create Date: 2024-12-23 03:00:00.000000
"""
from alembic import op
import sqlalchemy as sa
from alembic import op
revision = '7826ab40b532'
down_revision = '57c599a3cb57'
@ -16,10 +16,15 @@ depends_on = None
def upgrade():
op.add_column(
'file',
sa.Column('access_control', sa.JSON(), nullable=True),
)
conn = op.get_bind()
inspector = sa.inspect(conn)
file_cols = {c['name'] for c in inspector.get_columns('file')}
if 'access_control' not in file_cols:
op.add_column(
'file',
sa.Column('access_control', sa.JSON(), nullable=True),
)
def downgrade():

View file

@ -0,0 +1,44 @@
"""add memory type
Revision ID: 7b3f2a9c1d4e
Revises: 4c5ce3d2f27f
Create Date: 2026-06-25 00:00:00.000000
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
revision: str = '7b3f2a9c1d4e'
down_revision: Union[str, None] = '4c5ce3d2f27f'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
conn = op.get_bind()
inspector = sa.inspect(conn)
columns = {column['name'] for column in inspector.get_columns('memory')}
indexes = {index['name'] for index in inspector.get_indexes('memory')}
if 'type' not in columns:
op.add_column('memory', sa.Column('type', sa.String(), server_default='context', nullable=False))
if 'ix_memory_type' not in indexes:
op.create_index('ix_memory_type', 'memory', ['type'])
def downgrade() -> None:
conn = op.get_bind()
inspector = sa.inspect(conn)
columns = {column['name'] for column in inspector.get_columns('memory')}
indexes = {index['name'] for index in inspector.get_indexes('memory')}
if 'ix_memory_type' in indexes:
op.drop_index('ix_memory_type', table_name='memory')
if 'type' in columns:
op.drop_column('memory', 'type')

View file

@ -1,44 +1,34 @@
"""init
Revision ID: 7e5b5dc7342b
Revises:
Create Date: 2024-06-24 13:15:33.808998
"""
from typing import Sequence, Union
# Initial bootstrap migration version.
# Revision ID: 7e5b5dc7342b
# Revises: (none)
# Created on: 2024-06-24 13:15:33.808998
from __future__ import annotations
from typing import Sequence
import open_webui.internal.db # noqa: F401
import sqlalchemy as sa
from alembic import op
import open_webui.internal.db
from open_webui.internal.db import JSONField
from open_webui.migrations.util import get_existing_tables
# revision identifiers, used by Alembic.
revision: str = '7e5b5dc7342b'
down_revision: Union[str, None] = None
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
existing_tables = set(get_existing_tables())
# ### commands auto generated by Alembic - please adjust! ###
if 'auth' not in existing_tables:
op.create_table(
'auth',
down_revision: str | None = None
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None
# Initial schema table declarations
_INITIAL_TABLES: list[tuple[str, list[sa.Column], list]] = [
(
'auth',
[
sa.Column('id', sa.String(), nullable=False),
sa.Column('email', sa.String(), nullable=True),
sa.Column('password', sa.Text(), nullable=True),
sa.Column('active', sa.Boolean(), nullable=True),
sa.PrimaryKeyConstraint('id'),
)
if 'chat' not in existing_tables:
op.create_table(
'chat',
],
[sa.PrimaryKeyConstraint('id')],
),
(
'chat',
[
sa.Column('id', sa.String(), nullable=False),
sa.Column('user_id', sa.String(), nullable=True),
sa.Column('title', sa.Text(), nullable=True),
@ -47,24 +37,23 @@ def upgrade() -> None:
sa.Column('updated_at', sa.BigInteger(), nullable=True),
sa.Column('share_id', sa.Text(), nullable=True),
sa.Column('archived', sa.Boolean(), nullable=True),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('share_id'),
)
if 'chatidtag' not in existing_tables:
op.create_table(
'chatidtag',
],
[sa.PrimaryKeyConstraint('id'), sa.UniqueConstraint('share_id')],
),
(
'chatidtag',
[
sa.Column('id', sa.String(), nullable=False),
sa.Column('tag_name', sa.String(), nullable=True),
sa.Column('chat_id', sa.String(), nullable=True),
sa.Column('user_id', sa.String(), nullable=True),
sa.Column('timestamp', sa.BigInteger(), nullable=True),
sa.PrimaryKeyConstraint('id'),
)
if 'document' not in existing_tables:
op.create_table(
'document',
],
[sa.PrimaryKeyConstraint('id')],
),
(
'document',
[
sa.Column('collection_name', sa.String(), nullable=False),
sa.Column('name', sa.String(), nullable=True),
sa.Column('title', sa.Text(), nullable=True),
@ -72,24 +61,23 @@ def upgrade() -> None:
sa.Column('content', sa.Text(), nullable=True),
sa.Column('user_id', sa.String(), nullable=True),
sa.Column('timestamp', sa.BigInteger(), nullable=True),
sa.PrimaryKeyConstraint('collection_name'),
sa.UniqueConstraint('name'),
)
if 'file' not in existing_tables:
op.create_table(
'file',
],
[sa.PrimaryKeyConstraint('collection_name'), sa.UniqueConstraint('name')],
),
(
'file',
[
sa.Column('id', sa.String(), nullable=False),
sa.Column('user_id', sa.String(), nullable=True),
sa.Column('filename', sa.Text(), nullable=True),
sa.Column('meta', JSONField(), nullable=True),
sa.Column('created_at', sa.BigInteger(), nullable=True),
sa.PrimaryKeyConstraint('id'),
)
if 'function' not in existing_tables:
op.create_table(
'function',
],
[sa.PrimaryKeyConstraint('id')],
),
(
'function',
[
sa.Column('id', sa.String(), nullable=False),
sa.Column('user_id', sa.String(), nullable=True),
sa.Column('name', sa.Text(), nullable=True),
@ -101,23 +89,23 @@ def upgrade() -> None:
sa.Column('is_global', sa.Boolean(), nullable=True),
sa.Column('updated_at', sa.BigInteger(), nullable=True),
sa.Column('created_at', sa.BigInteger(), nullable=True),
sa.PrimaryKeyConstraint('id'),
)
if 'memory' not in existing_tables:
op.create_table(
'memory',
],
[sa.PrimaryKeyConstraint('id')],
),
(
'memory',
[
sa.Column('id', sa.String(), nullable=False),
sa.Column('user_id', sa.String(), nullable=True),
sa.Column('content', sa.Text(), nullable=True),
sa.Column('updated_at', sa.BigInteger(), nullable=True),
sa.Column('created_at', sa.BigInteger(), nullable=True),
sa.PrimaryKeyConstraint('id'),
)
if 'model' not in existing_tables:
op.create_table(
'model',
],
[sa.PrimaryKeyConstraint('id')],
),
(
'model',
[
sa.Column('id', sa.Text(), nullable=False),
sa.Column('user_id', sa.Text(), nullable=True),
sa.Column('base_model_id', sa.Text(), nullable=True),
@ -126,33 +114,33 @@ def upgrade() -> None:
sa.Column('meta', JSONField(), nullable=True),
sa.Column('updated_at', sa.BigInteger(), nullable=True),
sa.Column('created_at', sa.BigInteger(), nullable=True),
sa.PrimaryKeyConstraint('id'),
)
if 'prompt' not in existing_tables:
op.create_table(
'prompt',
],
[sa.PrimaryKeyConstraint('id')],
),
(
'prompt',
[
sa.Column('command', sa.String(), nullable=False),
sa.Column('user_id', sa.String(), nullable=True),
sa.Column('title', sa.Text(), nullable=True),
sa.Column('content', sa.Text(), nullable=True),
sa.Column('timestamp', sa.BigInteger(), nullable=True),
sa.PrimaryKeyConstraint('command'),
)
if 'tag' not in existing_tables:
op.create_table(
'tag',
],
[sa.PrimaryKeyConstraint('command')],
),
(
'tag',
[
sa.Column('id', sa.String(), nullable=False),
sa.Column('name', sa.String(), nullable=True),
sa.Column('user_id', sa.String(), nullable=True),
sa.Column('data', sa.Text(), nullable=True),
sa.PrimaryKeyConstraint('id'),
)
if 'tool' not in existing_tables:
op.create_table(
'tool',
],
[sa.PrimaryKeyConstraint('id')],
),
(
'tool',
[
sa.Column('id', sa.String(), nullable=False),
sa.Column('user_id', sa.String(), nullable=True),
sa.Column('name', sa.Text(), nullable=True),
@ -162,12 +150,12 @@ def upgrade() -> None:
sa.Column('valves', JSONField(), nullable=True),
sa.Column('updated_at', sa.BigInteger(), nullable=True),
sa.Column('created_at', sa.BigInteger(), nullable=True),
sa.PrimaryKeyConstraint('id'),
)
if 'user' not in existing_tables:
op.create_table(
'user',
],
[sa.PrimaryKeyConstraint('id')],
),
(
'user',
[
sa.Column('id', sa.String(), nullable=False),
sa.Column('name', sa.String(), nullable=True),
sa.Column('email', sa.String(), nullable=True),
@ -180,25 +168,25 @@ def upgrade() -> None:
sa.Column('settings', JSONField(), nullable=True),
sa.Column('info', JSONField(), nullable=True),
sa.Column('oauth_sub', sa.Text(), nullable=True),
],
[
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('api_key'),
sa.UniqueConstraint('oauth_sub'),
)
# ### end Alembic commands ###
],
),
]
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('user')
op.drop_table('tool')
op.drop_table('tag')
op.drop_table('prompt')
op.drop_table('model')
op.drop_table('memory')
op.drop_table('function')
op.drop_table('file')
op.drop_table('document')
op.drop_table('chatidtag')
op.drop_table('chat')
op.drop_table('auth')
# ### end Alembic commands ###
# --- migration execution ---
def upgrade() -> None: # deploy initial schema tables
existing_tables = set(get_existing_tables())
for name, columns, constraints in _INITIAL_TABLES:
if name not in existing_tables:
op.create_table(name, *columns, *constraints)
# --- rollback function ---
def downgrade() -> None: # rollback initial schema tables
for table_name, _, _ in reversed(_INITIAL_TABLES):
op.drop_table(table_name)

View file

@ -8,9 +8,9 @@ Create Date: 2025-12-10 16:07:58.001282
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
import open_webui.internal.db
import sqlalchemy as sa
from alembic import op
# revision identifiers, used by Alembic.
revision: str = '81cc2ce44d79'
@ -20,20 +20,27 @@ depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
conn = op.get_bind()
inspector = sa.inspect(conn)
# Add message_id column to channel_file table
with op.batch_alter_table('channel_file', schema=None) as batch_op:
batch_op.add_column(
sa.Column(
'message_id',
sa.Text(),
sa.ForeignKey('message.id', ondelete='CASCADE', name='fk_channel_file_message_id'),
nullable=True,
cf_cols = {c['name'] for c in inspector.get_columns('channel_file')}
if 'message_id' not in cf_cols:
with op.batch_alter_table('channel_file', schema=None) as batch_op:
batch_op.add_column(
sa.Column(
'message_id',
sa.Text(),
sa.ForeignKey('message.id', ondelete='CASCADE', name='fk_channel_file_message_id'),
nullable=True,
)
)
)
# Add data column to knowledge table
with op.batch_alter_table('knowledge', schema=None) as batch_op:
batch_op.add_column(sa.Column('data', sa.JSON(), nullable=True))
k_cols = {c['name'] for c in inspector.get_columns('knowledge')}
if 'data' not in k_cols:
with op.batch_alter_table('knowledge', schema=None) as batch_op:
batch_op.add_column(sa.Column('data', sa.JSON(), nullable=True))
def downgrade() -> None:

View file

@ -6,13 +6,13 @@ Create Date: 2026-02-01 04:00:00.000000
"""
import time
import json
import logging
import time
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from alembic import op
log = logging.getLogger(__name__)
@ -56,6 +56,13 @@ def _flush_batch(conn, table, batch):
def upgrade() -> None:
conn = op.get_bind()
inspector = sa.inspect(conn)
existing_tables = set(inspector.get_table_names())
if 'chat_message' in existing_tables:
return # Already created — skip everything
# Step 1: Create table
op.create_table(
'chat_message',
@ -85,8 +92,6 @@ def upgrade() -> None:
op.create_index('chat_message_user_created_idx', 'chat_message', ['user_id', 'created_at'])
# Step 2: Backfill from existing chats
conn = op.get_bind()
chat_table = sa.table(
'chat',
sa.column('id', sa.Text()),

View file

@ -0,0 +1,25 @@
"""add chat message meta
Revision ID: 856c5b02fb54
Revises: 42e2978c7933
Create Date: 2026-07-16 01:39:39.291935
"""
from typing import Sequence, Union
import sqlalchemy as sa
from alembic import op
revision: str = '856c5b02fb54'
down_revision: Union[str, None] = '42e2978c7933'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
op.add_column('chat_message', sa.Column('meta', sa.JSON(), nullable=True))
def downgrade() -> None:
op.drop_column('chat_message', 'meta')

View file

@ -8,9 +8,9 @@ Create Date: 2025-11-30 06:33:38.790341
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
import open_webui.internal.db
import sqlalchemy as sa
from alembic import op
# revision identifiers, used by Alembic.
revision: str = '90ef40d4714e'
@ -20,42 +20,53 @@ depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
conn = op.get_bind()
inspector = sa.inspect(conn)
existing_tables = set(inspector.get_table_names())
# Update 'channel' table
op.add_column('channel', sa.Column('is_private', sa.Boolean(), nullable=True))
op.add_column('channel', sa.Column('archived_at', sa.BigInteger(), nullable=True))
op.add_column('channel', sa.Column('archived_by', sa.Text(), nullable=True))
op.add_column('channel', sa.Column('deleted_at', sa.BigInteger(), nullable=True))
op.add_column('channel', sa.Column('deleted_by', sa.Text(), nullable=True))
op.add_column('channel', sa.Column('updated_by', sa.Text(), nullable=True))
channel_cols = {c['name'] for c in inspector.get_columns('channel')}
if 'is_private' not in channel_cols:
op.add_column('channel', sa.Column('is_private', sa.Boolean(), nullable=True))
if 'archived_at' not in channel_cols:
op.add_column('channel', sa.Column('archived_at', sa.BigInteger(), nullable=True))
if 'archived_by' not in channel_cols:
op.add_column('channel', sa.Column('archived_by', sa.Text(), nullable=True))
if 'deleted_at' not in channel_cols:
op.add_column('channel', sa.Column('deleted_at', sa.BigInteger(), nullable=True))
if 'deleted_by' not in channel_cols:
op.add_column('channel', sa.Column('deleted_by', sa.Text(), nullable=True))
if 'updated_by' not in channel_cols:
op.add_column('channel', sa.Column('updated_by', sa.Text(), nullable=True))
# Update 'channel_member' table
op.add_column('channel_member', sa.Column('role', sa.Text(), nullable=True))
op.add_column('channel_member', sa.Column('invited_by', sa.Text(), nullable=True))
op.add_column('channel_member', sa.Column('invited_at', sa.BigInteger(), nullable=True))
cm_cols = {c['name'] for c in inspector.get_columns('channel_member')}
if 'role' not in cm_cols:
op.add_column('channel_member', sa.Column('role', sa.Text(), nullable=True))
if 'invited_by' not in cm_cols:
op.add_column('channel_member', sa.Column('invited_by', sa.Text(), nullable=True))
if 'invited_at' not in cm_cols:
op.add_column('channel_member', sa.Column('invited_at', sa.BigInteger(), nullable=True))
# Create 'channel_webhook' table
op.create_table(
'channel_webhook',
sa.Column('id', sa.Text(), primary_key=True, unique=True, nullable=False),
sa.Column('user_id', sa.Text(), nullable=False),
sa.Column(
'channel_id',
sa.Text(),
sa.ForeignKey('channel.id', ondelete='CASCADE'),
nullable=False,
),
sa.Column('name', sa.Text(), nullable=False),
sa.Column('profile_image_url', sa.Text(), nullable=True),
sa.Column('token', sa.Text(), nullable=False),
sa.Column('last_used_at', sa.BigInteger(), nullable=True),
sa.Column('created_at', sa.BigInteger(), nullable=False),
sa.Column('updated_at', sa.BigInteger(), nullable=False),
)
pass
if 'channel_webhook' not in existing_tables:
op.create_table(
'channel_webhook',
sa.Column('id', sa.Text(), primary_key=True, unique=True, nullable=False),
sa.Column('user_id', sa.Text(), nullable=False),
sa.Column(
'channel_id',
sa.Text(),
sa.ForeignKey('channel.id', ondelete='CASCADE'),
nullable=False,
),
sa.Column('name', sa.Text(), nullable=False),
sa.Column('profile_image_url', sa.Text(), nullable=True),
sa.Column('token', sa.Text(), nullable=False),
sa.Column('last_used_at', sa.BigInteger(), nullable=True),
sa.Column('created_at', sa.BigInteger(), nullable=False),
sa.Column('updated_at', sa.BigInteger(), nullable=False),
)
def downgrade() -> None:
@ -74,5 +85,3 @@ def downgrade() -> None:
# Drop 'channel_webhook' table
op.drop_table('channel_webhook')
pass

View file

@ -6,8 +6,8 @@ Create Date: 2024-11-14 03:00:00.000000
"""
from alembic import op
import sqlalchemy as sa
from alembic import op
revision = '922e7a387820'
down_revision = '4ace53fd72c8'
@ -16,70 +16,55 @@ depends_on = None
def upgrade():
op.create_table(
'group',
sa.Column('id', sa.Text(), nullable=False, primary_key=True, unique=True),
sa.Column('user_id', sa.Text(), nullable=True),
sa.Column('name', sa.Text(), nullable=True),
sa.Column('description', sa.Text(), nullable=True),
sa.Column('data', sa.JSON(), nullable=True),
sa.Column('meta', sa.JSON(), nullable=True),
sa.Column('permissions', sa.JSON(), nullable=True),
sa.Column('user_ids', sa.JSON(), nullable=True),
sa.Column('created_at', sa.BigInteger(), nullable=True),
sa.Column('updated_at', sa.BigInteger(), nullable=True),
)
conn = op.get_bind()
inspector = sa.inspect(conn)
existing_tables = set(inspector.get_table_names())
if 'group' not in existing_tables:
op.create_table(
'group',
sa.Column('id', sa.Text(), nullable=False, primary_key=True, unique=True),
sa.Column('user_id', sa.Text(), nullable=True),
sa.Column('name', sa.Text(), nullable=True),
sa.Column('description', sa.Text(), nullable=True),
sa.Column('data', sa.JSON(), nullable=True),
sa.Column('meta', sa.JSON(), nullable=True),
sa.Column('permissions', sa.JSON(), nullable=True),
sa.Column('user_ids', sa.JSON(), nullable=True),
sa.Column('created_at', sa.BigInteger(), nullable=True),
sa.Column('updated_at', sa.BigInteger(), nullable=True),
)
# Add 'access_control' column to 'model' table
op.add_column(
'model',
sa.Column('access_control', sa.JSON(), nullable=True),
)
# Add 'is_active' column to 'model' table
op.add_column(
'model',
sa.Column(
'is_active',
sa.Boolean(),
nullable=False,
server_default=sa.sql.expression.true(),
),
)
model_cols = {c['name'] for c in inspector.get_columns('model')}
if 'access_control' not in model_cols:
op.add_column('model', sa.Column('access_control', sa.JSON(), nullable=True))
if 'is_active' not in model_cols:
op.add_column(
'model',
sa.Column('is_active', sa.Boolean(), nullable=False, server_default=sa.sql.expression.true()),
)
# Add 'access_control' column to 'knowledge' table
op.add_column(
'knowledge',
sa.Column('access_control', sa.JSON(), nullable=True),
)
knowledge_cols = {c['name'] for c in inspector.get_columns('knowledge')}
if 'access_control' not in knowledge_cols:
op.add_column('knowledge', sa.Column('access_control', sa.JSON(), nullable=True))
# Add 'access_control' column to 'prompt' table
op.add_column(
'prompt',
sa.Column('access_control', sa.JSON(), nullable=True),
)
prompt_cols = {c['name'] for c in inspector.get_columns('prompt')}
if 'access_control' not in prompt_cols:
op.add_column('prompt', sa.Column('access_control', sa.JSON(), nullable=True))
# Add 'access_control' column to 'tools' table
op.add_column(
'tool',
sa.Column('access_control', sa.JSON(), nullable=True),
)
tool_cols = {c['name'] for c in inspector.get_columns('tool')}
if 'access_control' not in tool_cols:
op.add_column('tool', sa.Column('access_control', sa.JSON(), nullable=True))
def downgrade():
op.drop_table('group')
# Drop 'access_control' column from 'model' table
op.drop_column('model', 'access_control')
# Drop 'is_active' column from 'model' table
op.drop_column('model', 'is_active')
# Drop 'access_control' column from 'knowledge' table
op.drop_column('knowledge', 'access_control')
# Drop 'access_control' column from 'prompt' table
op.drop_column('prompt', 'access_control')
# Drop 'access_control' column from 'tools' table
op.drop_column('tool', 'access_control')

View file

@ -0,0 +1,54 @@
"""add automation folder id
Revision ID: 959eaac8f909
Revises: 55f1302ac17c
Create Date: 2026-07-26 19:19:31.345756
"""
from collections.abc import Sequence
import sqlalchemy as sa
from alembic import context, op
# revision identifiers, used by Alembic.
revision: str = '959eaac8f909'
down_revision: str | None = '55f1302ac17c'
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None
def upgrade() -> None:
if context.is_offline_mode():
op.add_column('automation', sa.Column('folder_id', sa.Text(), nullable=True))
op.create_index('ix_automation_user_folder', 'automation', ['user_id', 'folder_id'])
return
conn = op.get_bind()
inspector = sa.inspect(conn)
columns = {col['name'] for col in inspector.get_columns('automation')}
indexes = {index['name'] for index in inspector.get_indexes('automation')}
if 'folder_id' not in columns:
op.add_column('automation', sa.Column('folder_id', sa.Text(), nullable=True))
if 'ix_automation_user_folder' not in indexes:
op.create_index('ix_automation_user_folder', 'automation', ['user_id', 'folder_id'])
def downgrade() -> None:
if context.is_offline_mode():
op.drop_index('ix_automation_user_folder', table_name='automation')
op.drop_column('automation', 'folder_id')
return
conn = op.get_bind()
inspector = sa.inspect(conn)
columns = {col['name'] for col in inspector.get_columns('automation')}
indexes = {index['name'] for index in inspector.get_indexes('automation')}
if 'ix_automation_user_folder' in indexes:
op.drop_index('ix_automation_user_folder', table_name='automation')
if 'folder_id' in columns:
op.drop_column('automation', 'folder_id')

View file

@ -0,0 +1,219 @@
"""add current_message_id to chat
Revision ID: 9a1b2c3d4e5f
Revises: 856c5b02fb54
Create Date: 2026-07-23 00:00:00.000000
"""
import json
from typing import Sequence, Union
import sqlalchemy as sa
from alembic import op
revision: str = '9a1b2c3d4e5f'
down_revision: Union[str, None] = '856c5b02fb54'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
BATCH_SIZE = 150
def upgrade() -> None:
conn = op.get_bind()
inspector = sa.inspect(conn)
columns = [col['name'] for col in inspector.get_columns('chat')]
if 'current_message_id' not in columns:
op.add_column('chat', sa.Column('current_message_id', sa.Text(), nullable=True))
chat = sa.table(
'chat',
sa.column('id', sa.String()),
sa.column('chat', sa.Text()),
sa.column('current_message_id', sa.Text()),
)
chat_message = sa.table(
'chat_message',
sa.column('id', sa.Text()),
sa.column('chat_id', sa.Text()),
sa.column('parent_id', sa.Text()),
sa.column('created_at', sa.BigInteger()),
)
has_chat_message = 'chat_message' in inspector.get_table_names()
result = conn.execute(
sa.select(chat.c.id, chat.c.chat, chat.c.current_message_id).execution_options(
yield_per=BATCH_SIZE,
stream_results=True,
)
)
while True:
rows = result.fetchmany(BATCH_SIZE)
if not rows:
break
batch_chat_ids: list[str] = []
candidates_by_chat: dict[str, list[str]] = {}
current_by_chat: dict[str, str | None] = {}
json_messages_by_chat: dict[str, dict[str, dict]] = {}
for row in rows:
values = row._mapping
chat_id = values['id']
prefix = f'{chat_id}-'
batch_chat_ids.append(chat_id)
current_by_chat[chat_id] = values['current_message_id']
chat_data = {}
if isinstance(values['chat'], dict):
chat_data = values['chat']
elif isinstance(values['chat'], str):
try:
parsed = json.loads(values['chat'])
chat_data = parsed if isinstance(parsed, dict) else {}
except (TypeError, ValueError, json.JSONDecodeError):
pass
history = chat_data.get('history') if isinstance(chat_data.get('history'), dict) else {}
candidates_by_chat[chat_id] = []
for candidate in (
values['current_message_id'],
history.get('currentId'),
chat_data.get('currentId'),
chat_data.get('branchPointMessageId'),
):
if not isinstance(candidate, str) or not candidate:
continue
candidate = candidate[len(prefix) :] if candidate.startswith(prefix) else candidate
if candidate not in candidates_by_chat[chat_id]:
candidates_by_chat[chat_id].append(candidate)
messages = history.get('messages') if isinstance(history.get('messages'), dict) else {}
if not messages and isinstance(chat_data.get('messages'), list):
messages = {
message['id']: message
for message in chat_data['messages']
if isinstance(message, dict) and message.get('id')
}
if messages:
json_messages_by_chat[chat_id] = {
message_id: {
'parent_id': message.get('parentId') if isinstance(message, dict) else None,
'created_at': message.get('timestamp', 0) if isinstance(message, dict) else 0,
}
for message_id, message in messages.items()
}
resolved: dict[str, str] = {}
if has_chat_message:
candidate_ids = {
f'{chat_id}-{candidate}'
for chat_id, candidates in candidates_by_chat.items()
for candidate in candidates
}
if candidate_ids:
valid_by_chat: dict[str, set[str]] = {}
for row in conn.execute(
sa.select(chat_message.c.chat_id, chat_message.c.id).where(
chat_message.c.chat_id.in_(batch_chat_ids),
chat_message.c.id.in_(candidate_ids),
)
):
values = row._mapping
chat_id = values['chat_id']
prefix = f'{chat_id}-'
message_id = values['id']
if message_id and message_id.startswith(prefix):
message_id = message_id[len(prefix) :]
if message_id:
valid_by_chat.setdefault(chat_id, set()).add(message_id)
for chat_id, candidates in candidates_by_chat.items():
valid_ids = valid_by_chat.get(chat_id, set())
for candidate in candidates:
if candidate in valid_ids:
resolved[chat_id] = candidate
break
unresolved_chat_ids = [chat_id for chat_id in batch_chat_ids if chat_id not in resolved]
messages_by_chat: dict[str, dict[str, dict]] = {}
if unresolved_chat_ids:
for row in conn.execute(
sa.select(
chat_message.c.chat_id,
chat_message.c.id,
chat_message.c.parent_id,
chat_message.c.created_at,
).where(chat_message.c.chat_id.in_(unresolved_chat_ids))
):
values = row._mapping
chat_id = values['chat_id']
prefix = f'{chat_id}-'
message_id = values['id']
if message_id and message_id.startswith(prefix):
message_id = message_id[len(prefix) :]
if not message_id:
continue
parent_id = values['parent_id']
if parent_id and parent_id.startswith(prefix):
parent_id = parent_id[len(prefix) :]
messages_by_chat.setdefault(chat_id, {})[message_id] = {
'parent_id': parent_id,
'created_at': values['created_at'] or 0,
}
for chat_id, messages in messages_by_chat.items():
parent_ids = {
message['parent_id'] for message in messages.values() if message.get('parent_id') in messages
}
leaf_ids = [message_id for message_id in messages if message_id not in parent_ids]
resolved[chat_id] = max(
leaf_ids or list(messages),
key=lambda message_id: messages[message_id].get('created_at') or 0,
)
for chat_id in batch_chat_ids:
if chat_id in resolved:
continue
messages = json_messages_by_chat.get(chat_id, {})
valid_candidate = next(
(candidate for candidate in candidates_by_chat[chat_id] if candidate in messages),
None,
)
if valid_candidate:
resolved[chat_id] = valid_candidate
elif messages:
parent_ids = {
message['parent_id'] for message in messages.values() if message.get('parent_id') in messages
}
leaf_ids = [message_id for message_id in messages if message_id not in parent_ids]
resolved[chat_id] = max(
leaf_ids or list(messages),
key=lambda message_id: messages[message_id].get('created_at') or 0,
)
updates = [
{'chat_id': chat_id, 'current_message_id': message_id}
for chat_id, message_id in resolved.items()
if message_id and message_id != current_by_chat.get(chat_id)
]
if updates:
conn.execute(
sa.update(chat)
.where(chat.c.id == sa.bindparam('update_chat_id'))
.values(current_message_id=sa.bindparam('update_current_message_id')),
[
{
'update_chat_id': row['chat_id'],
'update_current_message_id': row['current_message_id'],
}
for row in updates
],
)
def downgrade() -> None:
op.drop_column('chat', 'current_message_id')

View file

@ -6,8 +6,8 @@ Create Date: 2025-05-03 03:00:00.000000
"""
from alembic import op
import sqlalchemy as sa
from alembic import op
revision = '9f0c9cd09105'
down_revision = '3781e22d8b01'
@ -16,17 +16,22 @@ depends_on = None
def upgrade():
op.create_table(
'note',
sa.Column('id', sa.Text(), nullable=False, primary_key=True, unique=True),
sa.Column('user_id', sa.Text(), nullable=True),
sa.Column('title', sa.Text(), nullable=True),
sa.Column('data', sa.JSON(), nullable=True),
sa.Column('meta', sa.JSON(), nullable=True),
sa.Column('access_control', sa.JSON(), nullable=True),
sa.Column('created_at', sa.BigInteger(), nullable=True),
sa.Column('updated_at', sa.BigInteger(), nullable=True),
)
conn = op.get_bind()
inspector = sa.inspect(conn)
existing_tables = set(inspector.get_table_names())
if 'note' not in existing_tables:
op.create_table(
'note',
sa.Column('id', sa.Text(), nullable=False, primary_key=True, unique=True),
sa.Column('user_id', sa.Text(), nullable=True),
sa.Column('title', sa.Text(), nullable=True),
sa.Column('data', sa.JSON(), nullable=True),
sa.Column('meta', sa.JSON(), nullable=True),
sa.Column('access_control', sa.JSON(), nullable=True),
sa.Column('created_at', sa.BigInteger(), nullable=True),
sa.Column('updated_at', sa.BigInteger(), nullable=True),
)
def downgrade():

View file

@ -6,6 +6,7 @@ Create Date: 2025-09-15 03:00:00.000000
"""
import sqlalchemy as sa
from alembic import op
revision = 'a0b1c2d3e4f5'
@ -15,7 +16,12 @@ depends_on = None
def upgrade():
op.create_index('ix_memory_user_id', 'memory', ['user_id'])
conn = op.get_bind()
inspector = sa.inspect(conn)
existing_indexes = {idx['name'] for idx in inspector.get_indexes('memory')}
if 'ix_memory_user_id' not in existing_indexes:
op.create_index('ix_memory_user_id', 'memory', ['user_id'])
def downgrade():

View file

@ -8,9 +8,8 @@ Create Date: 2026-02-11 09:30:00.000000
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from alembic import op
from open_webui.migrations.util import get_existing_tables
revision: str = 'a1b2c3d4e5f6'

View file

@ -8,8 +8,8 @@ Create Date: 2026-03-29 22:15:00.000000
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from alembic import op
# revision identifiers, used by Alembic.
revision: str = 'a3dd5bedd151'
@ -19,8 +19,14 @@ depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
op.add_column('chat', sa.Column('tasks', sa.JSON(), nullable=True))
op.add_column('chat', sa.Column('summary', sa.Text(), nullable=True))
conn = op.get_bind()
inspector = sa.inspect(conn)
columns = [col['name'] for col in inspector.get_columns('chat')]
if 'tasks' not in columns:
op.add_column('chat', sa.Column('tasks', sa.JSON(), nullable=True))
if 'summary' not in columns:
op.add_column('chat', sa.Column('summary', sa.Text(), nullable=True))
def downgrade() -> None:

View file

@ -8,8 +8,8 @@ Create Date: 2025-09-27 02:24:18.058455
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from alembic import op
# revision identifiers, used by Alembic.
revision: str = 'a5c220713937'
@ -19,16 +19,18 @@ depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
conn = op.get_bind()
inspector = sa.inspect(conn)
msg_cols = {c['name'] for c in inspector.get_columns('message')}
# Add 'reply_to_id' column to the 'message' table for replying to messages
op.add_column(
'message',
sa.Column('reply_to_id', sa.Text(), nullable=True),
)
pass
if 'reply_to_id' not in msg_cols:
op.add_column(
'message',
sa.Column('reply_to_id', sa.Text(), nullable=True),
)
def downgrade() -> None:
# Remove 'reply_to_id' column from the 'message' table
op.drop_column('message', 'reply_to_id')
pass

View file

@ -6,8 +6,8 @@ Create Date: 2024-10-20 17:02:35.241684
"""
from alembic import op
import sqlalchemy as sa
from alembic import op
# Revision identifiers, used by Alembic.
revision = 'af906e964978'
@ -17,23 +17,28 @@ depends_on = None
def upgrade():
# ### Create feedback table ###
op.create_table(
'feedback',
sa.Column('id', sa.Text(), primary_key=True), # Unique identifier for each feedback (TEXT type)
sa.Column('user_id', sa.Text(), nullable=True), # ID of the user providing the feedback (TEXT type)
sa.Column('version', sa.BigInteger(), default=0), # Version of feedback (BIGINT type)
sa.Column('type', sa.Text(), nullable=True), # Type of feedback (TEXT type)
sa.Column('data', sa.JSON(), nullable=True), # Feedback data (JSON type)
sa.Column('meta', sa.JSON(), nullable=True), # Metadata for feedback (JSON type)
sa.Column('snapshot', sa.JSON(), nullable=True), # snapshot data for feedback (JSON type)
sa.Column(
'created_at', sa.BigInteger(), nullable=False
), # Feedback creation timestamp (BIGINT representing epoch)
sa.Column(
'updated_at', sa.BigInteger(), nullable=False
), # Feedback update timestamp (BIGINT representing epoch)
)
conn = op.get_bind()
inspector = sa.inspect(conn)
existing_tables = set(inspector.get_table_names())
if 'feedback' not in existing_tables:
# ### Create feedback table ###
op.create_table(
'feedback',
sa.Column('id', sa.Text(), primary_key=True), # Unique identifier for each feedback (TEXT type)
sa.Column('user_id', sa.Text(), nullable=True), # ID of the user providing the feedback (TEXT type)
sa.Column('version', sa.BigInteger(), default=0), # Version of feedback (BIGINT type)
sa.Column('type', sa.Text(), nullable=True), # Type of feedback (TEXT type)
sa.Column('data', sa.JSON(), nullable=True), # Feedback data (JSON type)
sa.Column('meta', sa.JSON(), nullable=True), # Metadata for feedback (JSON type)
sa.Column('snapshot', sa.JSON(), nullable=True), # snapshot data for feedback (JSON type)
sa.Column(
'created_at', sa.BigInteger(), nullable=False
), # Feedback creation timestamp (BIGINT representing epoch)
sa.Column(
'updated_at', sa.BigInteger(), nullable=False
), # Feedback update timestamp (BIGINT representing epoch)
)
def downgrade():

View file

@ -0,0 +1,32 @@
"""add user variables
Revision ID: b0018471bbbe
Revises: c49178636c78
Create Date: 2026-07-24 01:21:46.457057
"""
from typing import Sequence, Union
import sqlalchemy as sa
from alembic import op
# revision identifiers, used by Alembic.
revision: str = 'b0018471bbbe'
down_revision: Union[str, None] = 'c49178636c78'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
conn = op.get_bind()
inspector = sa.inspect(conn)
columns = [col['name'] for col in inspector.get_columns('user')]
if 'variables' not in columns:
op.add_column('user', sa.Column('variables', sa.JSON(), nullable=True))
def downgrade() -> None:
op.drop_column('user', 'variables')

View file

@ -6,15 +6,13 @@ Create Date: 2025-11-28 04:55:31.737538
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
import open_webui.internal.db
import json
import time
from typing import Sequence, Union
import open_webui.internal.db
import sqlalchemy as sa
from alembic import op
# revision identifiers, used by Alembic.
revision: str = 'b10670c03dd5'
@ -22,20 +20,49 @@ down_revision: Union[str, None] = '2f1211949ecc'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
# ── Ad-hoc table references for Core DML ─────────────────────────────────
# These are lightweight table() / column() references used only inside this
# migration for SELECT / UPDATE / INSERT — they do NOT create or alter
# anything on disk.
_user = sa.table(
'user',
sa.column('id', sa.Text),
sa.column('oauth_sub', sa.Text),
sa.column('oauth', sa.JSON),
sa.column('api_key', sa.Text),
sa.column('info', sa.Text),
sa.column('settings', sa.Text),
)
_api_key = sa.table(
'api_key',
sa.column('id', sa.Text),
sa.column('user_id', sa.Text),
sa.column('key', sa.Text),
sa.column('created_at', sa.BigInteger),
sa.column('updated_at', sa.BigInteger),
)
def _drop_sqlite_indexes_for_column(table_name, column_name, conn):
"""
SQLite requires manual removal of any indexes referencing a column
before ALTER TABLE ... DROP COLUMN can succeed.
SQLite requires manual removal of any user-created indexes referencing
a column before ALTER TABLE ... DROP COLUMN can succeed.
NOTE: PRAGMAs have no Core equivalent raw text is unavoidable here.
"""
indexes = conn.execute(sa.text(f"PRAGMA index_list('{table_name}')")).fetchall()
for idx in indexes:
index_name = idx[1] # index name
# Get indexed columns
# Skip system-managed autoindexes (PK / UNIQUE constraints) — they
# cannot be dropped directly and will disappear when the column is
# removed via batch_alter_table.
if index_name.startswith('sqlite_autoindex_'):
continue
idx_info = conn.execute(sa.text(f"PRAGMA index_info('{index_name}')")).fetchall()
indexed_cols = [row[2] for row in idx_info] # col names
indexed_cols = [row[2] for row in idx_info]
if column_name in indexed_cols:
conn.execute(sa.text(f'DROP INDEX IF EXISTS {index_name}'))
@ -44,33 +71,30 @@ def _convert_column_to_json(table: str, column: str):
conn = op.get_bind()
dialect = conn.dialect.name
t = sa.table(table, sa.column('id', sa.Text), sa.column(column, sa.Text))
# SQLite cannot ALTER COLUMN → must recreate column
if dialect == 'sqlite':
# 1. Add temporary column
op.add_column(table, sa.Column(f'{column}_json', sa.JSON(), nullable=True))
# 2. Load old data
rows = conn.execute(sa.text(f'SELECT id, {column} FROM "{table}"')).fetchall()
rows = conn.execute(sa.select(t.c.id, t.c[column])).fetchall()
for row in rows:
uid, raw = row
for uid, raw in rows:
if raw is None:
parsed = None
else:
try:
parsed = json.loads(raw)
except Exception:
parsed = None # fallback safe behavior
parsed = None
conn.execute(
sa.text(f'UPDATE "{table}" SET {column}_json = :val WHERE id = :id'),
{'val': json.dumps(parsed) if parsed else None, 'id': uid},
sa.update(sa.table(table, sa.column('id'), sa.column(f'{column}_json', sa.JSON)))
.where(sa.column('id') == uid)
.values({f'{column}_json': parsed})
)
# 3. Drop old TEXT column
op.drop_column(table, column)
# 4. Rename new JSON column → original name
op.alter_column(table, f'{column}_json', new_column_name=column)
else:
@ -87,15 +111,18 @@ def _convert_column_to_text(table: str, column: str):
conn = op.get_bind()
dialect = conn.dialect.name
t = sa.table(table, sa.column('id', sa.Text), sa.column(column, sa.JSON))
if dialect == 'sqlite':
op.add_column(table, sa.Column(f'{column}_text', sa.Text(), nullable=True))
rows = conn.execute(sa.text(f'SELECT id, {column} FROM "{table}"')).fetchall()
rows = conn.execute(sa.select(t.c.id, t.c[column])).fetchall()
for uid, raw in rows:
conn.execute(
sa.text(f'UPDATE "{table}" SET {column}_text = :val WHERE id = :id'),
{'val': json.dumps(raw) if raw else None, 'id': uid},
sa.update(sa.table(table, sa.column('id'), sa.column(f'{column}_text', sa.Text)))
.where(sa.column('id') == uid)
.values({f'{column}_text': json.dumps(raw) if raw is not None else None})
)
op.drop_column(table, column)
@ -111,115 +138,110 @@ def _convert_column_to_text(table: str, column: str):
def upgrade() -> None:
op.add_column('user', sa.Column('profile_banner_image_url', sa.Text(), nullable=True))
op.add_column('user', sa.Column('timezone', sa.String(), nullable=True))
op.add_column('user', sa.Column('presence_state', sa.String(), nullable=True))
op.add_column('user', sa.Column('status_emoji', sa.String(), nullable=True))
op.add_column('user', sa.Column('status_message', sa.Text(), nullable=True))
op.add_column('user', sa.Column('status_expires_at', sa.BigInteger(), nullable=True))
op.add_column('user', sa.Column('oauth', sa.JSON(), nullable=True))
# Convert info (TEXT/JSONField) → JSON
_convert_column_to_json('user', 'info')
# Convert settings (TEXT/JSONField) → JSON
_convert_column_to_json('user', 'settings')
op.create_table(
'api_key',
sa.Column('id', sa.Text(), primary_key=True, unique=True),
sa.Column('user_id', sa.Text(), sa.ForeignKey('user.id', ondelete='CASCADE')),
sa.Column('key', sa.Text(), unique=True, nullable=False),
sa.Column('data', sa.JSON(), nullable=True),
sa.Column('expires_at', sa.BigInteger(), nullable=True),
sa.Column('last_used_at', sa.BigInteger(), nullable=True),
sa.Column('created_at', sa.BigInteger(), nullable=False),
sa.Column('updated_at', sa.BigInteger(), nullable=False),
)
conn = op.get_bind()
users = conn.execute(sa.text('SELECT id, oauth_sub FROM "user" WHERE oauth_sub IS NOT NULL')).fetchall()
inspector = sa.inspect(conn)
existing_tables = set(inspector.get_table_names())
user_columns = {c['name'] for c in inspector.get_columns('user')}
for uid, oauth_sub in users:
if oauth_sub:
# Example formats supported:
# provider@sub
# plain sub (stored as {"oidc": {"sub": sub}})
if '@' in oauth_sub:
provider, sub = oauth_sub.split('@', 1)
else:
provider, sub = 'oidc', oauth_sub
# ── Add new columns (idempotent) ──────────────────────────────────
for col_name, col_type in [
('profile_banner_image_url', sa.Text()),
('timezone', sa.String()),
('presence_state', sa.String()),
('status_emoji', sa.String()),
('status_message', sa.Text()),
('status_expires_at', sa.BigInteger()),
('oauth', sa.JSON()),
]:
if col_name not in user_columns:
op.add_column('user', sa.Column(col_name, col_type, nullable=True))
oauth_json = json.dumps({provider: {'sub': sub}})
conn.execute(
sa.text('UPDATE "user" SET oauth = :oauth WHERE id = :id'),
{'oauth': oauth_json, 'id': uid},
)
# Convert info (TEXT/JSONField) → JSON (skip if already JSON)
user_col_types = {c['name']: c['type'] for c in inspector.get_columns('user')}
if isinstance(user_col_types.get('info'), sa.Text):
_convert_column_to_json('user', 'info')
# Convert settings (TEXT/JSONField) → JSON (skip if already JSON)
if isinstance(user_col_types.get('settings'), sa.Text):
_convert_column_to_json('user', 'settings')
users_with_keys = conn.execute(sa.text('SELECT id, api_key FROM "user" WHERE api_key IS NOT NULL')).fetchall()
now = int(time.time())
# ── Create api_key table (idempotent) ─────────────────────────────
if 'api_key' not in existing_tables:
op.create_table(
'api_key',
sa.Column('id', sa.Text(), primary_key=True, unique=True),
sa.Column('user_id', sa.Text(), sa.ForeignKey('user.id', ondelete='CASCADE')),
sa.Column('key', sa.Text(), unique=True, nullable=False),
sa.Column('data', sa.JSON(), nullable=True),
sa.Column('expires_at', sa.BigInteger(), nullable=True),
sa.Column('last_used_at', sa.BigInteger(), nullable=True),
sa.Column('created_at', sa.BigInteger(), nullable=False),
sa.Column('updated_at', sa.BigInteger(), nullable=False),
)
for uid, api_key in users_with_keys:
if api_key:
conn.execute(
sa.text("""
INSERT INTO api_key (id, user_id, key, created_at, updated_at)
VALUES (:id, :user_id, :key, :created_at, :updated_at)
"""),
{
'id': f'key_{uid}',
'user_id': uid,
'key': api_key,
'created_at': now,
'updated_at': now,
},
)
# ── Migrate oauth_sub → oauth JSON (only if old column still exists)
if 'oauth_sub' in user_columns:
rows = conn.execute(sa.select(_user.c.id, _user.c.oauth_sub).where(_user.c.oauth_sub.is_not(None))).fetchall()
if conn.dialect.name == 'sqlite':
_drop_sqlite_indexes_for_column('user', 'api_key', conn)
_drop_sqlite_indexes_for_column('user', 'oauth_sub', conn)
for uid, oauth_sub in rows:
if oauth_sub:
provider, sub = oauth_sub.split('@', 1) if '@' in oauth_sub else ('oidc', oauth_sub)
conn.execute(sa.update(_user).where(_user.c.id == uid).values(oauth={provider: {'sub': sub}}))
with op.batch_alter_table('user') as batch_op:
batch_op.drop_column('api_key')
batch_op.drop_column('oauth_sub')
# ── Migrate api_key column → api_key table (only if old column still exists)
if 'api_key' in user_columns:
rows = conn.execute(sa.select(_user.c.id, _user.c.api_key).where(_user.c.api_key.is_not(None))).fetchall()
now = int(time.time())
for uid, key_val in rows:
if key_val:
conn.execute(
sa.insert(_api_key).values(
id=f'key_{uid}',
user_id=uid,
key=key_val,
created_at=now,
updated_at=now,
)
)
# ── Drop legacy columns (idempotent) ──────────────────────────────
cols_to_drop = {'api_key', 'oauth_sub'} & user_columns
if cols_to_drop:
if conn.dialect.name == 'sqlite':
for col in cols_to_drop:
_drop_sqlite_indexes_for_column('user', col, conn)
with op.batch_alter_table('user') as batch_op:
for col in cols_to_drop:
batch_op.drop_column(col)
def downgrade() -> None:
# --- 1. Restore old oauth_sub column ---
op.add_column('user', sa.Column('oauth_sub', sa.Text(), nullable=True))
conn = op.get_bind()
users = conn.execute(sa.text('SELECT id, oauth FROM "user" WHERE oauth IS NOT NULL')).fetchall()
rows = conn.execute(sa.select(_user.c.id, _user.c.oauth).where(_user.c.oauth.is_not(None))).fetchall()
for uid, oauth in users:
for uid, oauth in rows:
try:
data = json.loads(oauth)
data = oauth if isinstance(oauth, dict) else json.loads(oauth)
provider = list(data.keys())[0]
sub = data[provider].get('sub')
oauth_sub = f'{provider}@{sub}'
except Exception:
oauth_sub = None
conn.execute(
sa.text('UPDATE "user" SET oauth_sub = :oauth_sub WHERE id = :id'),
{'oauth_sub': oauth_sub, 'id': uid},
)
conn.execute(sa.update(_user).where(_user.c.id == uid).values(oauth_sub=oauth_sub))
op.drop_column('user', 'oauth')
# --- 2. Restore api_key field ---
# --- Restore api_key field ---
op.add_column('user', sa.Column('api_key', sa.String(), nullable=True))
# Restore values from api_key
keys = conn.execute(sa.text('SELECT user_id, key FROM api_key')).fetchall()
keys = conn.execute(sa.select(_api_key.c.user_id, _api_key.c.key)).fetchall()
for uid, key in keys:
conn.execute(
sa.text('UPDATE "user" SET api_key = :key WHERE id = :id'),
{'key': key, 'id': uid},
)
conn.execute(sa.update(_user).where(_user.c.id == uid).values(api_key=key))
# Drop new table
op.drop_table('api_key')
with op.batch_alter_table('user') as batch_op:

View file

@ -8,8 +8,8 @@ Create Date: 2026-02-13 14:19:00.000000
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from alembic import op
# revision identifiers, used by Alembic.
revision: str = 'b2c3d4e5f6a7'
@ -19,7 +19,12 @@ depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
op.add_column('user', sa.Column('scim', sa.JSON(), nullable=True))
conn = op.get_bind()
inspector = sa.inspect(conn)
user_cols = {c['name'] for c in inspector.get_columns('user')}
if 'scim' not in user_cols:
op.add_column('user', sa.Column('scim', sa.JSON(), nullable=True))
def downgrade() -> None:

View file

@ -6,9 +6,8 @@ Create Date: 2026-04-01 04:00:00.000000
"""
from alembic import op
import sqlalchemy as sa
from alembic import op
# revision identifiers, used by Alembic.
revision = 'b7c8d9e0f1a2'
@ -18,9 +17,14 @@ depends_on = None
def upgrade():
op.add_column('chat', sa.Column('last_read_at', sa.BigInteger(), nullable=True))
# Set existing chats to be marked as read
op.execute('UPDATE chat SET last_read_at = updated_at')
conn = op.get_bind()
inspector = sa.inspect(conn)
columns = [col['name'] for col in inspector.get_columns('chat')]
if 'last_read_at' not in columns:
op.add_column('chat', sa.Column('last_read_at', sa.BigInteger(), nullable=True))
# Set existing chats to be marked as read
op.execute('UPDATE chat SET last_read_at = updated_at')
def downgrade():

View file

@ -19,10 +19,17 @@ depends_on: Union[str, Sequence[str], None] = None
def upgrade():
conn = op.get_bind()
inspector = sa.inspect(conn)
file_cols = {c['name'] for c in inspector.get_columns('file')}
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('file', sa.Column('hash', sa.Text(), nullable=True))
op.add_column('file', sa.Column('data', sa.JSON(), nullable=True))
op.add_column('file', sa.Column('updated_at', sa.BigInteger(), nullable=True))
if 'hash' not in file_cols:
op.add_column('file', sa.Column('hash', sa.Text(), nullable=True))
if 'data' not in file_cols:
op.add_column('file', sa.Column('data', sa.JSON(), nullable=True))
if 'updated_at' not in file_cols:
op.add_column('file', sa.Column('updated_at', sa.BigInteger(), nullable=True))
def downgrade():

View file

@ -9,8 +9,8 @@ Create Date: 2026-04-16 23:00:00.000000
import time
import uuid
from alembic import op
import sqlalchemy as sa
from alembic import op
revision = 'c1d2e3f4a5b6'
down_revision = 'e1f2a3b4c5d6'
@ -61,18 +61,21 @@ access_grant_t = sa.table(
def upgrade():
conn = op.get_bind()
inspector = sa.inspect(conn)
tables = inspector.get_table_names()
# 1. Create shared_chat table
op.create_table(
'shared_chat',
sa.Column('id', sa.Text(), primary_key=True),
sa.Column('chat_id', sa.Text(), sa.ForeignKey('chat.id', ondelete='CASCADE'), nullable=False),
sa.Column('user_id', sa.Text(), nullable=False),
sa.Column('title', sa.Text(), nullable=True),
sa.Column('chat', sa.JSON(), nullable=True),
sa.Column('created_at', sa.BigInteger(), nullable=True),
sa.Column('updated_at', sa.BigInteger(), nullable=True),
)
# 1. Create shared_chat table (idempotent)
if 'shared_chat' not in tables:
op.create_table(
'shared_chat',
sa.Column('id', sa.Text(), primary_key=True),
sa.Column('chat_id', sa.Text(), sa.ForeignKey('chat.id', ondelete='CASCADE'), nullable=False),
sa.Column('user_id', sa.Text(), nullable=False),
sa.Column('title', sa.Text(), nullable=True),
sa.Column('chat', sa.JSON(), nullable=True),
sa.Column('created_at', sa.BigInteger(), nullable=True),
sa.Column('updated_at', sa.BigInteger(), nullable=True),
)
# 2. Migrate existing shared-* rows
shared_rows = conn.execute(
@ -96,31 +99,51 @@ def upgrade():
if not original:
continue
# Insert snapshot into shared_chat
conn.execute(
shared_chat_t.insert().values(
id=share_token,
chat_id=original_chat_id,
user_id=original.user_id,
title=row.title,
chat=row.chat,
created_at=row.created_at,
updated_at=row.updated_at,
)
)
# Check if shared_chat record already exists (idempotent)
existing_shared = conn.execute(
sa.select(shared_chat_t.c.id).where(shared_chat_t.c.id == share_token)
).fetchone()
# Create user:*:read grant for backward compat
conn.execute(
access_grant_t.insert().values(
id=str(uuid.uuid4()),
resource_type='shared_chat',
resource_id=original_chat_id,
principal_type='user',
principal_id='*',
permission='read',
created_at=row.created_at or int(time.time()),
if not existing_shared:
# Insert snapshot into shared_chat
conn.execute(
shared_chat_t.insert().values(
id=share_token,
chat_id=original_chat_id,
user_id=original.user_id,
title=row.title,
chat=row.chat,
created_at=row.created_at,
updated_at=row.updated_at,
)
)
# Check if access_grant record already exists (idempotent)
existing_grant = conn.execute(
sa.select(access_grant_t.c.id).where(
sa.and_(
access_grant_t.c.resource_type == 'shared_chat',
access_grant_t.c.resource_id == original_chat_id,
access_grant_t.c.principal_type == 'user',
access_grant_t.c.principal_id == '*',
access_grant_t.c.permission == 'read',
)
)
).fetchone()
if not existing_grant:
# Create user:*:read grant for backward compat
conn.execute(
access_grant_t.insert().values(
id=str(uuid.uuid4()),
resource_type='shared_chat',
resource_id=original_chat_id,
principal_type='user',
principal_id='*',
permission='read',
created_at=row.created_at or int(time.time()),
)
)
)
# 3. Clean up old phantom rows
conn.execute(

View file

@ -6,11 +6,12 @@ Create Date: 2024-10-20 17:02:35.241684
"""
from alembic import op
import sqlalchemy as sa
import json
from sqlalchemy.sql import table, column
from sqlalchemy import String, Text, JSON, and_
import sqlalchemy as sa
from alembic import op
from sqlalchemy import JSON, String, Text, and_
from sqlalchemy.sql import column, table
revision = 'c29facfe716b'
down_revision = 'c69f45358db4'
@ -19,8 +20,13 @@ depends_on = None
def upgrade():
conn = op.get_bind()
inspector = sa.inspect(conn)
file_cols = {c['name'] for c in inspector.get_columns('file')}
# 1. Add the `path` column to the "file" table.
op.add_column('file', sa.Column('path', sa.Text(), nullable=True))
if 'path' not in file_cols:
op.add_column('file', sa.Column('path', sa.Text(), nullable=True))
# 2. Convert the `meta` column from Text/JSONField to `JSON()`
# Use Alembic's default batch_op for dialect compatibility.

Some files were not shown because too many files have changed in this diff Show more